//
// Syd: rock-solid application kernel
// src/mount/api.rs: Interface to new Linux mount API
//
// Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

//! Interface to new Linux mount API

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

use bitflags::bitflags;
use nix::{
    errno::Errno,
    fcntl::{AtFlags, OFlag},
    NixPath,
};

use crate::compat::with_opt_nix_path;

/// mount_setattr(2) flag to change the mount properties of the entire mount tree.
// This is not defined by nix yet!
pub const AT_RECURSIVE: AtFlags = AtFlags::from_bits_retain(0x8000);

bitflags! {
    /// Flags for `fsopen(2)`
    #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    #[repr(transparent)]
    pub struct FsOpenFlags: libc::c_uint {
        /// Close the returned fd on `execve(2)`.
        const FSOPEN_CLOEXEC = 0x00000001;
    }
}

/// Representation of the `enum fsconfig_command` from `<linux/mount.h>`.
#[repr(u32)]
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum FsConfigCmd {
    /// Set parameter, supplying no value
    SetFlag = 0,
    /// Set parameter, supplying a string value
    SetString = 1,
    /// Set parameter, supplying a binary blob value
    SetBinary = 2,
    /// Set parameter, supplying an object by path
    SetPath = 3,
    /// Set parameter, supplying an object by (empty) path
    SetPathEmpty = 4,
    /// Set parameter, supplying an object by fd
    SetFd = 5,
    /// Create new or reuse existing superblock
    CmdCreate = 6,
    /// Invoke superblock reconfiguration
    CmdReconfigure = 7,
    /// Create new superblock, fail if reusing existing superblock
    CmdCreateExcl = 8,
}

bitflags! {
    /// Flags for `fsmount(2)`
    #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    #[repr(transparent)]
    pub struct FsMountFlags: libc::c_uint {
        /// Set `FD_CLOEXEC` on the returned mount fd.
        const FSMOUNT_CLOEXEC = 0x00000001;
    }
}

bitflags! {
    /// MOUNT_ATTR_* bits used with `fsmount(2)`'s `attr_flags` argument.
    ///
    /// `MOUNT_ATTR_RELATIME` is effectively "no bits set" (the default).
    #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    #[repr(transparent)]
    pub struct MountAttrFlags: libc::c_uint {
        /// Mount read-only
        const MOUNT_ATTR_RDONLY = 0x00000001;
        /// Ignore suid and sgid bits
        const MOUNT_ATTR_NOSUID = 0x00000002;
        /// Disallow access to device special files
        const MOUNT_ATTR_NODEV  = 0x00000004;
        /// Disallow program execution
        const MOUNT_ATTR_NOEXEC = 0x00000008;
        /// Do not update access times
        const MOUNT_ATTR_NOATIME = 0x00000010;
        /// Access time change bit, should be set manually if STRICTATIME or NODIRATIME is set.
        const MOUNT_ATTR__ATIME = 0x00000070;
        /// Always perform atime updates
        const MOUNT_ATTR_STRICTATIME = 0x00000020;
        /// Do not update directory access times
        const MOUNT_ATTR_NODIRATIME = 0x00000080;
        /// Idmap mount to @userns_fd in struct mount_attr
        const MOUNT_ATTR_IDMAP = 0x00100000;
        /// Do not follow symlinks
        const MOUNT_ATTR_NOSYMFOLLOW = 0x00200000;
    }
}

bitflags! {
    /// Flags for `move_mount(2)`
    #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    #[repr(transparent)]
    pub struct MoveMountFlags: libc::c_uint {
        /// Follow symlinks on from path
        const MOVE_MOUNT_F_SYMLINKS = 0x00000001;
        /// Follow automounts on from path
        const MOVE_MOUNT_F_AUTOMOUNTS = 0x00000002;
        /// Empty from path permitted
        const MOVE_MOUNT_F_EMPTY_PATH = 0x00000004;
        /// Follow symlinks on to path
        const MOVE_MOUNT_T_SYMLINKS  = 0x00000010;
        /// Follow automounts on to path
        const MOVE_MOUNT_T_AUTOMOUNTS = 0x00000020;
        /// Empty to path permitted
        const MOVE_MOUNT_T_EMPTY_PATH = 0x00000040;
        /// Set sharing group instead
        const MOVE_MOUNT_SET_GROUP = 0x00000100;
        /// Mount beneath top mount
        const MOVE_MOUNT_BENEATH = 0x00000200;
    }
}

bitflags! {
    /// Flags for `open_tree(2)`
    #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    #[repr(transparent)]
    pub struct OpenTreeFlags: libc::c_uint {
        /// Clone the mount tree instead of opening the mount's root directory.
        const OPEN_TREE_CLONE = 0x00000001;
        /// Treat path as empty; operate on dirfd directly.
        #[expect(clippy::cast_sign_loss)]
        const OPEN_TREE_CLOEXEC = OFlag::O_CLOEXEC.bits() as libc::c_uint;
        /// If path is an empty string, operate on the file referred to by dirfd.
        #[expect(clippy::cast_sign_loss)]
        const AT_EMPTY_PATH = AtFlags::AT_EMPTY_PATH.bits() as libc::c_uint;
        /// Do not automount the terminal ("basename") component of path.
        #[expect(clippy::cast_sign_loss)]
        const AT_NO_AUTOMOUNT = AtFlags::AT_NO_AUTOMOUNT.bits() as libc::c_uint;
        /// If path is a symbolic link, do not dereference it.
        #[expect(clippy::cast_sign_loss)]
        const AT_SYMLINK_NOFOLLOW = AtFlags::AT_SYMLINK_NOFOLLOW.bits() as libc::c_uint;
        /// Create a recursive bind-mount of the path (akin to mount --rbind)
        const AT_RECURSIVE = 0x8000;
    }
}

bitflags! {
    /// Flags for the `fspick(2)`
    #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    #[repr(transparent)]
    pub struct FsPickFlags: libc::c_uint {
        /// Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.
        const FSPICK_CLOEXEC = 0x00000001;
        /// Do not follow symbolic links in the terminal component of path.
        const FSPICK_SYMLINK_NOFOLLOW = 0x00000002;
        /// Do not automount the terminal ("basename") component of path.
        const FSPICK_NO_AUTOMOUNT = 0x00000004;
        /// If path is an empty string, operate on the file referred by dirfd.
        const FSPICK_EMPTY_PATH = 0x00000008;
    }
}

/// Rust representation of `struct mount_attr` from `<linux/mount.h>`.
#[repr(C)]
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Default)]
pub struct MountAttr {
    /// Mount properties to set
    pub attr_set: u64,
    /// Mount properties to clear
    pub attr_clr: u64,
    /// Mount propagation type
    pub propagation: u64,
    /// User namespace file descriptor
    pub userns_fd: u64,
}

/// Create a new filesystem context.
///
/// This system call is new in Linux-5.2.
pub fn fsopen<P: ?Sized + NixPath>(fsname: &P, flags: FsOpenFlags) -> Result<OwnedFd, Errno> {
    fsname.with_nix_path(|cstr| {
        // SAFETY: In libc we trust.
        #[expect(clippy::cast_possible_truncation)]
        Errno::result(unsafe { libc::syscall(libc::SYS_fsopen, cstr.as_ptr(), flags.bits()) }).map(
            |fd| {
                // SAFETY: fsopen returns a valid fd on success.
                unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
            },
        )
    })?
}

/// Select filesystem for reconfiguration.
///
/// This system call is new in Linux-5.2.
pub fn fspick<Fd, P>(dirfd: Fd, path: &P, flags: FsPickFlags) -> Result<OwnedFd, Errno>
where
    Fd: AsFd,
    P: ?Sized + NixPath,
{
    path.with_nix_path(|cstr| {
        // SAFETY: In libc we trust.
        #[expect(clippy::cast_possible_truncation)]
        Errno::result(unsafe {
            libc::syscall(
                libc::SYS_fspick,
                dirfd.as_fd().as_raw_fd(),
                cstr.as_ptr(),
                flags.bits(),
            )
        })
        .map(|fd| {
            // SAFETY: fspick returns a valid fd on success.
            unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
        })
    })?
}

/// Configure new or existing filesystem context.
///
/// This system call is new in Linux-5.2.
pub fn fsconfig<Fd, P>(
    fd: Fd,
    cmd: FsConfigCmd,
    key: Option<&P>,
    value: Option<&[u8]>,
    aux: libc::c_int,
) -> Result<(), Errno>
where
    Fd: AsFd,
    P: ?Sized + NixPath,
{
    let fd = fd.as_fd().as_raw_fd();
    let cmd = cmd as libc::c_uint;
    let value: *const libc::c_void = value.map(|v| v.as_ptr().cast()).unwrap_or(std::ptr::null());

    // SAFETY: In libc we trust.
    let res = with_opt_nix_path(key, |key| unsafe {
        libc::syscall(libc::SYS_fsconfig, fd, cmd, key, value, aux)
    })?;

    Errno::result(res).map(|_| ())
}

/// Instantiate mount object from filesystem context.
///
/// This system call is new in Linux-5.2.
pub fn fsmount<Fd: AsFd>(
    fsfd: Fd,
    flags: FsMountFlags,
    attr_flags: MountAttrFlags,
) -> Result<OwnedFd, Errno> {
    // SAFETY: In libc we trust.
    #[expect(clippy::cast_possible_truncation)]
    Errno::result(unsafe {
        libc::syscall(
            libc::SYS_fsmount,
            fsfd.as_fd().as_raw_fd(),
            flags.bits(),
            attr_flags.bits(),
        )
    })
    .map(|fd| {
        // SAFETY: fsopen returns a valid fd on success.
        unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
    })
}

/// Moves the mount object indicated by `from_dirfd` and `from_path` to
/// the path indicated by `to_dirfd` and `to_path`. The mount object
/// being moved can be an existing mount point in the current mount
/// namespace or a detached mount object created by `fsmount` or
/// `open_tree` with `OPEN_TREE_CLONE`.
///
/// This system call is new in Linux-5.2.
pub fn move_mount<Fd1, Fd2, P1, P2>(
    from_dirfd: Fd1,
    from_path: &P1,
    to_dirfd: Fd2,
    to_path: &P2,
    flags: MoveMountFlags,
) -> Result<(), Errno>
where
    Fd1: AsFd,
    Fd2: AsFd,
    P1: ?Sized + NixPath,
    P2: ?Sized + NixPath,
{
    from_path.with_nix_path(|from_cstr| {
        to_path.with_nix_path(|to_cstr| {
            // SAFETY: In libc we trust.
            Errno::result(unsafe {
                libc::syscall(
                    libc::SYS_move_mount,
                    from_dirfd.as_fd().as_raw_fd(),
                    from_cstr.as_ptr(),
                    to_dirfd.as_fd().as_raw_fd(),
                    to_cstr.as_ptr(),
                    flags.bits(),
                )
            })
            .map(drop)
        })
    })??
}

/// Open the mount tree rooted at `dirfd` + `path`.
///
/// This system call is new in Linux-5.2.
pub fn open_tree<Fd, P>(dirfd: Fd, path: &P, flags: OpenTreeFlags) -> Result<OwnedFd, Errno>
where
    Fd: AsFd,
    P: ?Sized + NixPath,
{
    path.with_nix_path(|cstr| {
        // SAFETY: In libc we trust.
        #[expect(clippy::cast_possible_truncation)]
        Errno::result(unsafe {
            libc::syscall(
                libc::SYS_open_tree,
                dirfd.as_fd().as_raw_fd(),
                cstr.as_ptr(),
                flags.bits(),
            )
        })
        .map(|fd| {
            // SAFETY: open_tree returns a valid fd on success.
            unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
        })
    })?
}

/// Open a mount tree with attributes applied atomically.
///
/// This system call is new in Linux-6.15.
pub fn open_tree_attr<Fd, P>(
    dirfd: Fd,
    path: &P,
    flags: OpenTreeFlags,
    attr: &MountAttr,
) -> Result<OwnedFd, Errno>
where
    Fd: AsFd,
    P: ?Sized + NixPath,
{
    path.with_nix_path(|cstr| {
        // SAFETY: In libc we trust.
        #[expect(clippy::cast_possible_truncation)]
        Errno::result(unsafe {
            libc::syscall(
                SYS_OPEN_TREE_ATTR,
                dirfd.as_fd().as_raw_fd(),
                cstr.as_ptr(),
                flags.bits(),
                &raw const attr,
                size_of::<MountAttr>(),
            )
        })
        .map(|fd| {
            // SAFETY: open_tree_attr returns a valid fd on success.
            unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
        })
    })?
}

/// Change properties of a mount or mount tree
///
/// This system call is new in Linux-5.12.
pub fn mount_setattr<Fd, P>(
    dirfd: Fd,
    path: &P,
    flags: AtFlags,
    attr: MountAttr,
) -> Result<(), Errno>
where
    Fd: AsFd,
    P: ?Sized + NixPath,
{
    path.with_nix_path(|cstr| {
        // SAFETY: In libc we trust.
        Errno::result(unsafe {
            libc::syscall(
                libc::SYS_mount_setattr,
                dirfd.as_fd().as_raw_fd(),
                cstr.as_ptr(),
                flags.bits(),
                &raw const attr,
                size_of::<MountAttr>(),
            )
        })
        .map(drop)
    })?
}

// 32-bit MIPS, o32 ABI (covers mips + mips32r6, big & little endian).
#[cfg(any(target_arch = "mips", target_arch = "mips32r6"))]
const SYS_OPEN_TREE_ATTR: libc::c_long = 467 + 4000;

// 64-bit MIPS, n64 ABI (covers mips64 + mips64r6, big & little endian).
//
// Rust's `mips64*` Linux targets use the n64 ABI, so there is no
// separate Rust target for the n32 ABI (`467 + 6000`).
#[cfg(any(target_arch = "mips64", target_arch = "mips64r6"))]
const SYS_OPEN_TREE_ATTR: libc::c_long = 467 + 5000;

#[cfg(not(any(
    target_arch = "mips",
    target_arch = "mips32r6",
    target_arch = "mips64",
    target_arch = "mips64r6",
)))]
const SYS_OPEN_TREE_ATTR: libc::c_long = 467;
