//! CLI handler for intercepted `groupadd`.

// SPDX-License-Identifier: Apache-2.0 OR MIT

use super::common::{self, SYSUSERS_DIR};
use anyhow::{anyhow, Context, Result};
use cap_std::fs::{Dir, Permissions, PermissionsExt};
use cap_std_ext::prelude::CapStdExtDirExt;
use clap::{Arg, ArgAction, Command};
use fn_error_context::context;
use std::io::Write;

/// Entrypoint for (the rpm-ostree implementation of) `groupadd`.
#[context("Intercepting groupadd")]
pub(crate) fn entrypoint(args: &[&str]) -> Result<()> {
    fail::fail_point!("intercept_groupadd_ok", |_| Ok(()));

    // This parses the same CLI surface as the real `groupadd`,
    // but in the end we only extract the group name and (if
    // present) the static GID.
    let matches = cli_cmd().get_matches_from(args);
    let gid = matches
        .get_one::<String>("gid")
        .map(|s| s.parse::<u32>())
        .transpose()
        .context("Parsing GID")?;
    let groupname = matches
        .get_one::<String>("groupname")
        .ok_or_else(|| anyhow!("missing required groupname argument"))?;
    if !matches.contains_id("system") {
        crate::client::warn_future_incompatibility(
            format!("Trying to create non-system group '{groupname}'; this will become an error in the future.")
        );
    }

    let rootdir = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
    generate_sysusers_fragment(&rootdir, groupname, gid)?;

    Ok(())
}

/// CLI parser, matches <https://linux.die.net/man/8/groupadd>.
fn cli_cmd() -> Command {
    let name = "groupadd";
    Command::new(name)
        .bin_name(name)
        .about("create a new group")
        .arg(
            Arg::new("force")
                .short('f')
                .long("force")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("gid")
                .short('g')
                .long("gid")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("key")
                .short('K')
                .long("key")
                .action(ArgAction::Set),
        )
        .arg(Arg::new("allow_duplicates").short('o').long("non-unique"))
        .arg(
            Arg::new("password")
                .short('p')
                .long("password")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("system")
                .short('r')
                .long("system")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("chroot_dir")
                .short('R')
                .long("root")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("prefix_dir")
                .short('P')
                .long("prefix")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("users")
                .short('U')
                .long("users")
                .action(ArgAction::Set),
        )
        .arg(Arg::new("groupname").required(true))
}

/// Write a sysusers.d configuration fragment for the given group.
///
/// This returns whether a new fragment has been actually written
/// to disk.
#[context("Generating sysusers.d fragment adding group '{}'", groupname)]
fn generate_sysusers_fragment(rootdir: &Dir, groupname: &str, gid: Option<u32>) -> Result<bool> {
    // The filename of the configuration fragment is in fact a public
    // API, because users may have masked it in /etc. Do not change this.
    let filename = format!("30-rpmostree-pkg-group-{groupname}.conf");

    let conf_dir = common::open_create_sysusers_dir(rootdir)?;
    if conf_dir.try_exists(&filename)? {
        return Ok(false);
    }

    let gid_value = gid
        .map(|id| id.to_string())
        .unwrap_or_else(|| "-".to_string());
    conf_dir
        .atomic_replace_with(&filename, |fragment| -> Result<()> {
            let perms = Permissions::from_mode(0o644);
            fragment.get_mut().as_file_mut().set_permissions(perms)?;

            fragment.write_all(b"# Generated by rpm-ostree\n")?;
            let entry = format!("g {groupname} {gid_value}\n");
            fragment.write_all(entry.as_bytes())?;

            Ok(())
        })
        .with_context(|| format!("Writing /{SYSUSERS_DIR}/{filename}"))?;

    Ok(true)
}

#[cfg(test)]
mod test {
    use super::*;
    use cap_std_ext::cap_tempfile;
    use std::io::Read;

    #[test]
    fn test_clap_cmd() {
        cli_cmd().debug_assert();

        let cmd = cli_cmd();
        let static_gid = ["/usr/sbin/groupadd", "-g", "23", "squid"];
        let matches = cmd.try_get_matches_from(static_gid).unwrap();
        assert_eq!(matches.get_one::<String>("gid").unwrap(), "23");
        assert_eq!(matches.get_one::<String>("groupname").unwrap(), "squid");

        let cmd = cli_cmd();
        let dynamic_gid = ["/usr/sbin/groupadd", "-r", "chrony"];
        let matches = cmd.try_get_matches_from(dynamic_gid).unwrap();
        assert!(matches.contains_id("system"));
        assert_eq!(matches.get_one::<String>("gid"), None);
        assert_eq!(matches.get_one::<String>("groupname").unwrap(), "chrony");

        let err_cases = [vec!["/usr/sbin/groupadd"]];
        for input in err_cases {
            let cmd = cli_cmd();
            cmd.try_get_matches_from(input).unwrap_err();
        }
    }

    #[test]
    fn test_fragment_generation() {
        let tmpdir = cap_tempfile::tempdir(cap_tempfile::ambient_authority()).unwrap();

        let groups = [
            ("foo", Some(42), true, "42"),
            ("foo", None, false, "42"),
            ("bar", None, true, "-"),
        ];
        for entry in groups {
            let generated = generate_sysusers_fragment(&tmpdir, entry.0, entry.1).unwrap();
            assert_eq!(generated, entry.2, "{:?}", entry);

            let path = format!("usr/lib/sysusers.d/30-rpmostree-pkg-group-{}.conf", entry.0);
            assert!(tmpdir.is_file(&path));

            let mut fragment = tmpdir.open(&path).unwrap();
            let mut content = String::new();
            fragment.read_to_string(&mut content).unwrap();
            let expected = format!("# Generated by rpm-ostree\ng {} {}\n", entry.0, entry.3);
            assert_eq!(content, expected)
        }
    }
}
