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

use std::os::fd::AsRawFd;

use libc::c_int;
use libseccomp::{ScmpArch, ScmpNotifResp};
use nix::errno::Errno;

use crate::{
    compat::FallocateFlags,
    confine::{scmp_arch_bits, scmp_big_endian},
    cookie::{safe_ftruncate, safe_ftruncate64, safe_truncate, safe_truncate64},
    fd::is_valid_fd,
    fs::fallocate64,
    kernel::syscall_path_handler,
    path::XPathBuf,
    req::{SysArg, UNotifyEventRequest},
};

pub(crate) fn sys_truncate(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY:
    // 1. If first argument is NULL, return EFAULT.
    // 2. If second argument is negative, return EINVAL.
    let req = request.scmpreq;
    let is32 = scmp_arch_bits(req.data.arch) == 32;

    #[expect(clippy::cast_possible_truncation)]
    #[expect(clippy::cast_possible_wrap)]
    let len = if is32 {
        libc::off_t::from(req.data.args[1] as i32)
    } else {
        req.data.args[1] as libc::off_t
    };

    if len < 0 {
        return request.fail_syscall(Errno::EINVAL);
    } else if req.data.args[0] == 0 {
        return request.fail_syscall(Errno::EFAULT);
    }

    let argv = &[SysArg {
        path: Some(0),
        ..Default::default()
    }];

    syscall_path_handler(request, "truncate", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        // SAFETY:
        // 1. SysArg has one element.
        // 2. `/` is not a regular file -> EINVAL.
        #[expect(clippy::disallowed_methods)]
        let fd = path_args
            .0
            .as_ref()
            .map(|path| path.dir.as_ref().unwrap().as_raw_fd())
            .ok_or(Errno::EINVAL)?;

        // SAFETY: ftruncate(2) requires write fds.
        // We fallback to `/proc` indirection to avoid opening the file as write.
        //
        // path to fd is open already!
        let mut pfd = XPathBuf::from("/proc/thread-self/fd");
        pfd.push_fd(fd);

        // SAFETY: Record blocking call so it can get invalidated.
        request.cache.add_sys_block(req, false)?;

        let result = safe_truncate(&pfd, len).map(|_| request.return_syscall(0));

        // Remove invalidation record unless interrupted.
        request
            .cache
            .del_sys_block(req.id, matches!(result, Err(Errno::EINTR)))?;

        result
    })
}

pub(crate) fn sys_truncate64(request: UNotifyEventRequest) -> ScmpNotifResp {
    // SAFETY:
    // 1. If first argument is NULL, return EFAULT.
    // 2. If second argument is negative, return EINVAL.
    let req = request.scmpreq;
    let is32 = scmp_arch_bits(req.data.arch) == 32;

    // truncate64 needs argument translation on some architectures.
    #[expect(clippy::arithmetic_side_effects)]
    #[expect(clippy::cast_possible_truncation)]
    #[expect(clippy::cast_possible_wrap)]
    let len = if is32 {
        // Align arg_no to the next even number for specific 32-bit architectures.
        let arg_no = match req.data.arch {
            ScmpArch::Arm | ScmpArch::Mipsel | ScmpArch::Ppc => 2,
            _ => 1,
        };

        let len_low = req.data.args[arg_no] as u32;
        let len_high = req.data.args[arg_no + 1] as u32;

        if req.data.arch == ScmpArch::Ppc && cfg!(target_pointer_width = "32") {
            // PowerPC32 passes high word first, then low word.
            (libc::off64_t::from(len_high) << 32) | libc::off64_t::from(len_low)
        } else if scmp_big_endian(req.data.arch) {
            (libc::off64_t::from(len_low) << 32) | libc::off64_t::from(len_high)
        } else {
            (libc::off64_t::from(len_high) << 32) | libc::off64_t::from(len_low)
        }
    } else {
        // Align arg_no to the next even number for specific 64-bit architectures.
        let arg_no = match req.data.arch {
            ScmpArch::Aarch64 | ScmpArch::Ppc64 | ScmpArch::Ppc64Le => 2,
            _ => 1,
        };

        req.data.args[arg_no] as libc::off64_t
    };

    if len < 0 {
        return request.fail_syscall(Errno::EINVAL);
    } else if req.data.args[0] == 0 {
        return request.fail_syscall(Errno::EFAULT);
    }

    let argv = &[SysArg {
        path: Some(0),
        ..Default::default()
    }];

    syscall_path_handler(
        request,
        "truncate64",
        argv,
        |path_args, request, sandbox| {
            drop(sandbox); // release the read-lock.

            // SAFETY:
            // 1. SysArg has one element.
            // 2. `/` is not a regular file -> EINVAL.
            #[expect(clippy::disallowed_methods)]
            let fd = path_args
                .0
                .as_ref()
                .unwrap()
                .dir
                .as_ref()
                .ok_or(Errno::EINVAL)?;

            // SAFETY: ftruncate64(2) requires write fds.
            // We fallback to `/proc` indirection to avoid opening the file as write.
            //
            // path to fd is open already!
            let mut pfd = XPathBuf::from("/proc/thread-self/fd");
            pfd.push_fd(fd.as_raw_fd());

            // SAFETY: Record blocking call so it can get invalidated.
            request.cache.add_sys_block(req, false)?;

            let result = safe_truncate64(&pfd, len).map(|_| request.return_syscall(0));

            // Remove invalidation record unless interrupted.
            request
                .cache
                .del_sys_block(req.id, matches!(result, Err(Errno::EINTR)))?;

            result
        },
    )
}

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

    // SAFETY: If second argument is negative, return EINVAL.
    let is32 = scmp_arch_bits(req.data.arch) == 32;

    #[expect(clippy::cast_possible_truncation)]
    #[expect(clippy::cast_possible_wrap)]
    let len = if is32 {
        libc::off_t::from(req.data.args[1] as i32)
    } else {
        req.data.args[1] as libc::off_t
    };

    if len < 0 {
        return request.fail_syscall(Errno::EINVAL);
    }

    // SAFETY: FD-only call:
    // 1. Assert valid fd before other arguments.
    // 2. AT_FDCWD is an invalid fd argument.
    if !is_valid_fd(req.data.args[0]) {
        return request.fail_syscall(Errno::EBADF);
    }

    let argv = &[SysArg {
        dirfd: Some(0),
        ..Default::default()
    }];

    syscall_path_handler(request, "ftruncate", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        // SAFETY:
        // 1. SysArg has one element.
        // 2. SysArg.path is None asserting dir is Some.
        #[expect(clippy::disallowed_methods)]
        let fd = path_args
            .0
            .as_ref()
            .map(|path| path.dir.as_ref().unwrap())
            .ok_or(Errno::EINVAL)?;

        // SAFETY: Record blocking call so it can get invalidated.
        request.cache.add_sys_block(req, false)?;

        let result = safe_ftruncate(fd, len).map(|_| request.return_syscall(0));

        // Remove invalidation record unless interrupted.
        request
            .cache
            .del_sys_block(req.id, matches!(result, Err(Errno::EINTR)))?;

        result
    })
}

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

    // SAFETY: If second argument is negative, return EINVAL.
    let is32 = scmp_arch_bits(req.data.arch) == 32;

    // truncate64 needs argument translation on some architectures.
    #[expect(clippy::arithmetic_side_effects)]
    #[expect(clippy::cast_possible_truncation)]
    #[expect(clippy::cast_possible_wrap)]
    let len = if is32 {
        // Align arg_no to the next even number for specific 32-bit architectures.
        let arg_no = match req.data.arch {
            ScmpArch::Arm | ScmpArch::Mipsel | ScmpArch::Ppc => 2,
            _ => 1,
        };

        let len_low = req.data.args[arg_no] as u32;
        let len_high = req.data.args[arg_no + 1] as u32;

        if req.data.arch == ScmpArch::Ppc && cfg!(target_pointer_width = "32") {
            // PowerPC32 passes high word first, then low word.
            (libc::off64_t::from(len_high) << 32) | libc::off64_t::from(len_low)
        } else if scmp_big_endian(req.data.arch) {
            (libc::off64_t::from(len_low) << 32) | libc::off64_t::from(len_high)
        } else {
            (libc::off64_t::from(len_high) << 32) | libc::off64_t::from(len_low)
        }
    } else {
        req.data.args[1] as libc::off64_t
    };

    if len < 0 {
        return request.fail_syscall(Errno::EINVAL);
    }

    // SAFETY: FD-only call:
    // 1. Assert valid fd before other arguments.
    // 2. AT_FDCWD is an invalid fd argument.
    if !is_valid_fd(req.data.args[0]) {
        return request.fail_syscall(Errno::EBADF);
    }

    let argv = &[SysArg {
        dirfd: Some(0),
        ..Default::default()
    }];

    syscall_path_handler(
        request,
        "ftruncate64",
        argv,
        |path_args, request, sandbox| {
            drop(sandbox); // release the read-lock.

            // SAFETY:
            // 1. SysArg has one element.
            // 2. SysArg.path is None asserting dir is Some.
            #[expect(clippy::disallowed_methods)]
            let fd = path_args.0.as_ref().unwrap().dir.as_ref().unwrap();

            // SAFETY: Record blocking call so it can get invalidated.
            request.cache.add_sys_block(req, false)?;

            let result = safe_ftruncate64(fd, len).map(|_| request.return_syscall(0));

            // Remove invalidation record unless interrupted.
            request
                .cache
                .del_sys_block(req.id, matches!(result, Err(Errno::EINTR)))?;

            result
        },
    )
}

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

    // SAFETY: FD-only call:
    // 1. Assert valid fd before other arguments.
    // 2. AT_FDCWD is an invalid fd argument.
    if !is_valid_fd(req.data.args[0]) {
        return request.fail_syscall(Errno::EBADF);
    }

    // SAFETY: EINVAL offset was less than 0, or len was less than or equal to 0.
    let is32 = scmp_arch_bits(req.data.arch) == 32;

    // fallocate needs argument translation for offset argument on some architectures.
    #[expect(clippy::cast_possible_truncation)]
    #[expect(clippy::cast_possible_wrap)]
    let off = if is32 {
        let len_low = req.data.args[2] as u32;
        let len_high = req.data.args[3] as u32;

        if req.data.arch == ScmpArch::Ppc && cfg!(target_pointer_width = "32") {
            // PowerPC32 passes high word first, then low word.
            (libc::off64_t::from(len_high) << 32) | libc::off64_t::from(len_low)
        } else if scmp_big_endian(req.data.arch) {
            (libc::off64_t::from(len_low) << 32) | libc::off64_t::from(len_high)
        } else {
            (libc::off64_t::from(len_high) << 32) | libc::off64_t::from(len_low)
        }
    } else {
        req.data.args[2] as libc::off64_t
    };

    if off < 0 {
        return request.fail_syscall(Errno::EINVAL);
    }

    // fallocate needs argument translation for length argument on some architectures.
    #[expect(clippy::cast_possible_truncation)]
    #[expect(clippy::cast_possible_wrap)]
    let len = if is32 {
        let len_low = req.data.args[4] as u32;
        let len_high = req.data.args[5] as u32;

        if scmp_big_endian(req.data.arch) {
            (libc::off64_t::from(len_low) << 32) | libc::off64_t::from(len_high)
        } else {
            (libc::off64_t::from(len_high) << 32) | libc::off64_t::from(len_low)
        }
    } else {
        req.data.args[3] as libc::off64_t
    };

    if len <= 0 {
        return request.fail_syscall(Errno::EINVAL);
    }

    // SAFETY: Reject undefined/invalid mode.
    let mode = match to_fallocate_flags(req.data.args[1]) {
        Ok(mode) => mode,
        Err(errno) => return request.fail_syscall(errno),
    };

    let argv = &[SysArg {
        dirfd: Some(0),
        ..Default::default()
    }];
    syscall_path_handler(request, "fallocate", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        // SAFETY:
        // 1. SysArg has one element.
        // 2. SysArg.path is None asserting dir is Some.
        #[expect(clippy::disallowed_methods)]
        let fd = path_args.0.as_ref().unwrap().dir.as_ref().unwrap();

        // SAFETY: Record blocking call so it can get invalidated.
        request.cache.add_sys_block(req, false)?;

        let result = fallocate64(fd, mode, off, len);

        // Remove invalidation record unless interrupted.
        request
            .cache
            .del_sys_block(req.id, matches!(result, Err(Errno::EINTR)))?;

        result.map(|_| request.return_syscall(0))
    })
}

fn to_fallocate_flags(arg: u64) -> Result<FallocateFlags, Errno> {
    // FALLOC_FL_MODE_MASK: FL_ALLOCATE_RANGE is encoded as 0, so it doesn't affect the mask.
    const FALLOC_FL_MODE_MASK: c_int = FallocateFlags::FALLOC_FL_PUNCH_HOLE.bits()
        | FallocateFlags::FALLOC_FL_COLLAPSE_RANGE.bits()
        | FallocateFlags::FALLOC_FL_ZERO_RANGE.bits()
        | FallocateFlags::FALLOC_FL_INSERT_RANGE.bits()
        | FallocateFlags::FALLOC_FL_UNSHARE_RANGE.bits()
        | FallocateFlags::FALLOC_FL_WRITE_ZEROES.bits();

    let arg: c_int = arg.try_into().or(Err(Errno::EINVAL))?;
    if (arg & !FallocateFlags::all().bits()) != 0 {
        return Err(Errno::EINVAL);
    }
    let flags = FallocateFlags::from_bits_truncate(arg);

    // See Linux' vfs_fallocate() in fs/open.c for the validations.
    if (arg & !(FALLOC_FL_MODE_MASK | FallocateFlags::FALLOC_FL_KEEP_SIZE.bits())) != 0 {
        return Err(Errno::EOPNOTSUPP);
    }

    match arg & FALLOC_FL_MODE_MASK {
        0 => { /* FALLOC_FL_ALLOCATE_RANGE */ }
        x if x == FallocateFlags::FALLOC_FL_UNSHARE_RANGE.bits() => {}
        x if x == FallocateFlags::FALLOC_FL_ZERO_RANGE.bits() => {}
        x if x == FallocateFlags::FALLOC_FL_PUNCH_HOLE.bits() => {
            if (arg & FallocateFlags::FALLOC_FL_KEEP_SIZE.bits()) == 0 {
                return Err(Errno::EOPNOTSUPP);
            }
        }
        x if x == FallocateFlags::FALLOC_FL_COLLAPSE_RANGE.bits()
            || x == FallocateFlags::FALLOC_FL_INSERT_RANGE.bits()
            || x == FallocateFlags::FALLOC_FL_WRITE_ZEROES.bits() =>
        {
            if (arg & FallocateFlags::FALLOC_FL_KEEP_SIZE.bits()) != 0 {
                return Err(Errno::EOPNOTSUPP);
            }
        }
        _ => return Err(Errno::EOPNOTSUPP),
    }

    Ok(flags)
}
