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

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

use std::os::fd::AsFd;

use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    fcntl::{fcntl, FcntlArg, OFlag},
};

use crate::{
    confine::scmp_arch_bits,
    fd::to_fd,
    lookup::{CanonicalPath, FileInfo},
    req::UNotifyEventRequest,
    sandbox::Capability,
};

const F_SETFL: u64 = libc::F_SETFL as u64;
const F_OFD_SETLK: u64 = libc::F_OFD_SETLK as u64;
const F_OFD_SETLKW: u64 = libc::F_OFD_SETLKW as u64;
const O_APPEND: u64 = libc::O_APPEND as u64;

pub(crate) fn sys_fcntl(request: UNotifyEventRequest) -> ScmpNotifResp {
    let is32 = scmp_arch_bits(request.scmpreq.data.arch) == 32;
    handle_fcntl(request, is32)
}

pub(crate) fn sys_fcntl64(request: UNotifyEventRequest) -> ScmpNotifResp {
    handle_fcntl(request, false)
}

fn handle_fcntl(request: UNotifyEventRequest, is32: bool) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;
        let args = req.data.args;

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let cmd = u64::from(args[1] as u32);
        let arg = args[2];

        // Assert invariants:
        // 1. We only hook into F_SETFL requests which do NOT have O_APPEND set.
        // 2. We hook into F_OFD_SETLK{,W} additionally and nothing else.
        assert!(
            matches!(cmd, F_SETFL | F_OFD_SETLK | F_OFD_SETLKW),
            "BUG: called fcntl(2) handler with invalid command {cmd:#x}, report a bug!"
        );
        assert!(
            cmd != F_SETFL || arg & O_APPEND == 0,
            "BUG: called fcntl(2) handler with F_SETFL command and O_APPEND set, report a bug!"
        );

        let fd = to_fd(args[0])?;
        let fd = request.get_fd(fd)?;

        let path = CanonicalPath::new_fd(fd.into(), req.pid()).or(Err(Errno::EBADF))?;
        if !request.is_valid() {
            return Err(Errno::ESRCH);
        }

        let sandbox = request.get_sandbox();
        let is_append = sandbox.is_append(path.abs());
        let is_crypt = sandbox.enabled(Capability::CAP_CRYPT);
        drop(sandbox); // release the read-lock.

        if is_append && cmd == F_SETFL {
            return Err(Errno::EPERM);
        }

        let fd = path.dir();
        if is_crypt {
            if let Ok(info) = FileInfo::from_fd(fd) {
                #[expect(clippy::disallowed_methods)]
                let files = request.cache.crypt_map.as_ref().unwrap();

                let deny = {
                    let files = files.0.lock().unwrap_or_else(|err| err.into_inner());
                    files.values().any(|map| map.info == info)
                }; // Lock is released here.

                if deny {
                    return Err(Errno::EPERM);
                }
            }
        }

        // Perform the allowed fcntl(2) call.
        match cmd {
            F_SETFL => handle_fcntl_setfl(fd, arg),
            F_OFD_SETLK => handle_fcntl_ofd_setlk(&request, fd, arg, is32),
            F_OFD_SETLKW => handle_fcntl_ofd_setlkw(&request, fd, arg, is32),
            _ => unreachable!(
                "BUG: called fcntl(2) handler with invalid command {cmd:#x}, report a bug!"
            ),
        }
        .map(|ret| request.return_syscall(ret.into()))
    })
}

fn handle_fcntl_setfl<Fd: AsFd>(fd: Fd, arg: u64) -> Result<i32, Errno> {
    #[expect(clippy::cast_possible_truncation)]
    let flags = OFlag::from_bits_truncate(arg as i32);
    fcntl(fd, FcntlArg::F_SETFL(flags))
}

fn handle_fcntl_ofd_setlk<Fd: AsFd>(
    request: &UNotifyEventRequest,
    fd: Fd,
    addr: u64,
    is32: bool,
) -> Result<i32, Errno> {
    let flock = request.remote_flock(addr, is32)?;
    fcntl(fd, FcntlArg::F_OFD_SETLK(&flock))
}

fn handle_fcntl_ofd_setlkw<Fd: AsFd>(
    request: &UNotifyEventRequest,
    fd: Fd,
    addr: u64,
    is32: bool,
) -> Result<i32, Errno> {
    let flock = request.remote_flock(addr, is32)?;
    fcntl(fd, FcntlArg::F_OFD_SETLKW(&flock))
}
