//
// Syd: rock-solid application kernel
// src/mount/util.rs: Utilities using the new Linux mount API
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

//! Utilities using the new Linux mount API

use std::{
    ffi::CString,
    os::{fd::AsFd, unix::ffi::OsStrExt},
};

use nix::{errno::Errno, fcntl::AtFlags, mount::MsFlags, NixPath};

use crate::{
    error,
    fd::AT_BADFD,
    info,
    mount::api::{
        fsconfig, fsmount, fsopen, mount_setattr, move_mount, open_tree, FsConfigCmd, FsMountFlags,
        FsOpenFlags, MountAttr, MountAttrFlags, MoveMountFlags, OpenTreeFlags, AT_RECURSIVE,
    },
};

/// Perform a filesystem mount.
pub fn mount_fs<Fd, P>(
    fsname: &P,
    dst: Fd,
    flags: MountAttrFlags,
    opts: Option<&str>,
) -> Result<(), Errno>
where
    Fd: AsFd,
    P: ?Sized + NixPath + OsStrExt,
{
    let ctx = fsopen(fsname, FsOpenFlags::FSOPEN_CLOEXEC)?;

    fsname.with_nix_path(|cstr| {
        fsconfig(
            &ctx,
            FsConfigCmd::SetString,
            Some("source"),
            Some(cstr.to_bytes_with_nul()),
            0,
        )
    })??;
    if let Some(opts) = opts {
        for opt in opts.split(',') {
            if opt.is_empty() {
                continue; // convenience
            }
            let (key, val) = if let Some((key, val)) = opt.split_once('=') {
                let val = CString::new(val)
                    .or(Err(Errno::EINVAL))?
                    .into_bytes_with_nul();
                (key, Some(val))
            } else {
                (opt, None)
            };
            let cmd = if val.is_none() {
                FsConfigCmd::SetFlag
            } else {
                FsConfigCmd::SetString
            };
            fsconfig(&ctx, cmd, Some(key), val.as_deref(), 0)?;
        }
    }

    fsconfig(
        &ctx,
        FsConfigCmd::CmdCreate,
        None::<&[u8]>,
        None::<&[u8]>,
        0,
    )?;
    fsmount(ctx, FsMountFlags::FSMOUNT_CLOEXEC, flags).and_then(|mnt| {
        move_mount(
            mnt,
            c"",
            dst,
            c"",
            MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH,
        )
    })
}

/// Perform a recursive bind mount, optionally setting the given propagation type.
pub fn mount_bind<Fd1, Fd2>(src: Fd1, dst: Fd2, flags: MountAttrFlags) -> Result<(), Errno>
where
    Fd1: AsFd,
    Fd2: AsFd,
{
    let clr_flags = mountattr_fixup(flags);
    let attr = MountAttr {
        attr_set: flags.bits().into(),
        attr_clr: clr_flags.bits().into(),
        propagation: 0,
        userns_fd: 0,
    };

    let src = open_tree(
        src,
        c"",
        OpenTreeFlags::OPEN_TREE_CLOEXEC
            | OpenTreeFlags::OPEN_TREE_CLONE
            | OpenTreeFlags::AT_EMPTY_PATH
            | OpenTreeFlags::AT_RECURSIVE,
    )?;
    mount_setattr(&src, c"", AtFlags::AT_EMPTY_PATH, attr)?;
    move_mount(
        src,
        c"",
        dst,
        c"",
        MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH,
    )
}

/// Change propagation type of rootfs.
///
/// `proptype` must be one of `MsFlags::MS_SHARED`, `MsFlags::MS_SLAVE`,
/// `MsFlags::MS_PRIVATE`, or `MsFlags::MS_UNBINDABLE`.
pub fn set_root_mount_propagation(proptype: MsFlags) -> Result<(), Errno> {
    // The into conversion is necessary on 32-bit.
    #[expect(clippy::useless_conversion)]
    let attr = MountAttr {
        attr_set: 0,
        attr_clr: 0,
        propagation: proptype.bits().into(),
        userns_fd: 0,
    };

    open_tree(AT_BADFD, "/", OpenTreeFlags::OPEN_TREE_CLOEXEC)
        .and_then(|fd| mount_setattr(fd, c"", AtFlags::AT_EMPTY_PATH | AT_RECURSIVE, attr))
        .inspect(|_| {
            let propname = propagation_name(proptype);
            info!("ctx": "run", "op": "set_root_mount_propagation",
                "type": propname, "bits": proptype.bits(),
                "msg": format!("set root mount propagation type to {propname}"));
        })
        .inspect_err(|errno| {
            let propname = propagation_name(proptype);
            error!("ctx": "run", "op": "set_root_mount_propagation",
                "type": propname, "bits": proptype.bits(), "err": *errno as i32,
                "msg": format!("set root mount propagation type to {propname} failed: {errno}"));
        })
}

const fn propagation_name(proptype: MsFlags) -> &'static str {
    match proptype {
        MsFlags::MS_SHARED => "shared",
        MsFlags::MS_SLAVE => "slave",
        MsFlags::MS_PRIVATE => "private",
        MsFlags::MS_UNBINDABLE => "unbindable",
        _ => "unknown",
    }
}

// If MOUNT_ATTR_NOATIME or MOUNT_ATTR_STRICTATIME is set,
// we should add the flag MOUNT_ATTR__ATIME to ensure the
// kernel can perform correct validation.
fn mountattr_fixup(flags: MountAttrFlags) -> MountAttrFlags {
    if flags.intersects(MountAttrFlags::MOUNT_ATTR__ATIME) {
        MountAttrFlags::MOUNT_ATTR__ATIME
    } else {
        MountAttrFlags::empty()
    }
}
