//
// Syd: rock-solid application kernel
// src/kernel/inotify.rs: inotify_add_watch(2) handler
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::os::fd::{AsFd, AsRawFd, RawFd};

use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, NixPath};

use crate::{
    compat::AddWatchFlags,
    fs::inotify_add_watch,
    kernel::syscall_path_handler,
    lookup::{file_type, FsFlags},
    path::XPathBuf,
    req::{SysArg, UNotifyEventRequest},
};

pub(crate) fn sys_inotify_add_watch(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // SAFETY: Reject undefined masks.
    let mask: u32 = match req.data.args[2].try_into() {
        Ok(mask) => mask,
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

    // SAFETY: Reject invalid masks.
    let mask = match AddWatchFlags::from_bits(mask) {
        Some(mask) => mask,
        None => return request.fail_syscall(Errno::EINVAL),
    };

    // inotify(7) requires read access to the file or directory,
    // with the exception of symbolic links. However, due to
    // the `/proc' indirection in the handler we must not pass
    // WANT_READ here. See LTP inotify tests.
    let mut fsflags = FsFlags::MUST_PATH;
    if mask.contains(AddWatchFlags::IN_DONT_FOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let argv = &[SysArg {
        dirfd: None,
        path: Some(1),
        fsflags,
        ..Default::default()
    }];
    syscall_path_handler(
        request,
        "inotify_add_watch",
        argv,
        |path_args, request, sandbox| {
            let restrict_notify_bdev = !sandbox.flags.allow_unsafe_notify_bdev();
            let restrict_notify_cdev = !sandbox.flags.allow_unsafe_notify_cdev();
            drop(sandbox); // release the read-lock.

            // SAFETY: SysArg has one element.
            #[expect(clippy::disallowed_methods)]
            let path = path_args.0.as_ref().unwrap();

            if !path.base().is_empty() {
                // SAFETY: Ensure we have a direct FD.
                unreachable!("BUG: inotify_add_watch with base path!");
            }

            // SAFETY: ^^ empty base asserts dir is Some.
            #[expect(clippy::disallowed_methods)]
            let dfd = path.dir.as_ref().map(|fd| fd.as_fd()).unwrap();

            let mut mask = mask & !AddWatchFlags::IN_DONT_FOLLOW;
            if restrict_notify_bdev || restrict_notify_cdev {
                // SAFETY:
                // 1. Strip IN_{ACCESS,MODIFY} if we're marking a sidechannel device.
                // 2. Strip IN_DONT_FOLLOW which has already been handled during canonicalization.
                let filetype = file_type(dfd, None, false)?;
                if (restrict_notify_bdev && filetype.is_block_device())
                    || (restrict_notify_cdev && filetype.is_char_device())
                {
                    mask.remove(AddWatchFlags::IN_ACCESS);
                    mask.remove(AddWatchFlags::IN_MODIFY);
                }
            }

            // SAFETY: We open a FD to the path and then use the
            // proc path /proc/thread-self/fd/$fd in address' path
            // argument to avoid symlink TOCTOU.
            let mut pfd = XPathBuf::from("/proc/thread-self/fd");
            pfd.push_fd(dfd.as_raw_fd());

            // Get the INotify FD.
            #[expect(clippy::cast_possible_truncation)]
            let fd = request.get_fd(req.data.args[0] as RawFd)?;

            inotify_add_watch(&fd, &pfd, mask)
                .map(|retval| request.return_syscall(i64::from(retval)))
        },
    )
}
