//! Code mirroring rpmostree-core.cxx which is the shared "core"
//! binding of rpm and ostree, used by both client-side layering/overrides
//! and server side composes.
// SPDX-License-Identifier: Apache-2.0 OR MIT

use crate::cxxrsutil::*;
use crate::ffiutil;
use crate::kernel_install::is_ostree_layout;
use crate::progress::progress_task;
use anyhow::Context;
use anyhow::{anyhow, Result};
use camino::Utf8Path;
use cap_std::fs::{Dir, Permissions, PermissionsExt};
use cap_std_ext::cap_std;
use cap_std_ext::prelude::CapStdExtDirExt;
use ffiutil::*;
use fn_error_context::context;
use glib::prelude::StaticVariantType;
use libdnf_sys::*;
use ostree_ext::container::OstreeImageReference;
use ostree_ext::glib;
use ostree_ext::oci_spec::distribution::Reference as OciReference;
use ostree_ext::ostree;
use std::fs::File;
use std::io::{BufReader, Read};
use std::os::unix::io::{AsRawFd, FromRawFd};

/// The binary forked from useradd that pokes the sss cache.
/// It spews warnings (and sometimes fatal errors) when used
/// in a non-systemd container (default treecompose side) so
/// we temporarily remove it.  https://github.com/SSSD/sssd/issues/5687
const SSS_CACHE_PATH: &str = "usr/sbin/sss_cache";
// Also neuter systemctl - at least glusterfs for example calls `systemctl start`
// in its %post which both violates Fedora policy and also will not
// work with the rpm-ostree model.
// See also https://github.com/projectatomic/rpm-ostree/issues/550
// See also the SYSTEMD_OFFLINE bits in rpmostree-scripts.c; at some
// point in the far future when we don't support RHEL/CentOS7 we can drop
// our wrapper script.  If we remember.
const SYSTEMCTL_PATH: &str = "usr/bin/systemctl";
const SYSTEMCTL_WRAPPER: &[u8] = include_bytes!("../../src/libpriv/systemctl-wrapper.sh");

// Intercept commands for automatic sysusers.d fragment generation.
const GROUPADD_PATH: &str = "usr/sbin/groupadd";
const GROUPADD_WRAPPER: &[u8] = include_bytes!("../../src/libpriv/groupadd-wrapper.sh");
const USERADD_PATH: &str = "usr/sbin/useradd";
const USERADD_WRAPPER: &[u8] = include_bytes!("../../src/libpriv/useradd-wrapper.sh");
const USERMOD_PATH: &str = "usr/sbin/usermod";
const USERMOD_WRAPPER: &[u8] = include_bytes!("../../src/libpriv/usermod-wrapper.sh");
const KERNEL_INSTALL_PATH: &str = "usr/bin/kernel-install";
const KERNEL_INSTALL_WRAPPER: &[u8] = include_bytes!("../../src/libpriv/kernel-install-wrapper.sh");

const RPMOSTREE_CORE_STAGED_RPMS_DIR: &str = "rpm-ostree/staged-rpms";

pub(crate) const OSTREE_BOOTED: &str = "/run/ostree-booted";

/// Guard for running logic in a context with temporary /etc.
///
/// We have a messy dance in dealing with /usr/etc and /etc; the
/// current model is basically to have it be /etc whenever we're running
/// any code.
#[derive(Debug)]
pub struct TempEtcGuard {
    rootfs: Dir,
    renamed_etc: bool,
}

/// Detect if we have /usr/etc and no /etc, and rename if so.
pub(crate) fn prepare_tempetc_guard(rootfs: i32) -> CxxResult<Box<TempEtcGuard>> {
    let rootfs = unsafe { ffiutil::ffi_dirfd(rootfs)? };
    let has_etc = rootfs.try_exists("etc")?;
    let mut renamed_etc = false;
    if !has_etc && rootfs.try_exists("usr/etc")? {
        // In general now, we place contents in /etc when running scripts
        rootfs.rename("usr/etc", &rootfs, "etc")?;
        // But leave a compat symlink, as we used to bind mount, so scripts
        // could still use that too.
        rootfs.symlink("../etc", "usr/etc")?;
        renamed_etc = true;
    }
    Ok(Box::new(TempEtcGuard {
        rootfs,
        renamed_etc,
    }))
}

impl TempEtcGuard {
    /// Remove the temporary /etc, and destroy the guard.
    pub(crate) fn undo(&self) -> CxxResult<()> {
        if self.renamed_etc {
            /* Remove the symlink and swap back */
            self.rootfs.remove_file("usr/etc")?;
            self.rootfs.rename("etc", &self.rootfs, "usr/etc")?;
        }
        Ok(())
    }
}

/// Run the standard `depmod` utility.
pub(crate) fn run_depmod(rootfs_dfd: i32, kver: &str, unified_core: bool) -> CxxResult<()> {
    let args: Vec<_> = vec!["depmod", "-a", kver]
        .into_iter()
        .map(|s| s.to_string())
        .collect();
    let _ = crate::bwrap::bubblewrap_run_sync(
        rootfs_dfd,
        &args,
        false,
        crate::ffi::BubblewrapMutability::for_unified_core(unified_core),
    )?;
    Ok(())
}

/// Infer whether string is a container image reference.
pub(crate) fn is_container_image_reference(refspec: &str) -> bool {
    // this is slightly less efficient than calling just try_from(), but meh...
    refspec_classify(refspec) == crate::ffi::RefspecType::Container
}

/// Infer whether string is a container image reference.
pub(crate) fn is_container_image_digest_reference(refspec: &str) -> bool {
    OstreeImageReference::try_from(refspec)
        .and_then(|ostree_imgref| {
            ostree_imgref
                .imgref
                .name
                .parse::<OciReference>()
                .map_err(anyhow::Error::msg)
        })
        .map(|oci_ref| oci_ref.digest().is_some())
        .ok()
        .unwrap_or_default()
}

/// Given a refspec, infer its type and return it.
pub(crate) fn refspec_classify(refspec: &str) -> crate::ffi::RefspecType {
    if OstreeImageReference::try_from(refspec).is_ok() {
        crate::ffi::RefspecType::Container
    } else if ostree::validate_checksum_string(refspec).is_ok() {
        crate::ffi::RefspecType::Checksum
    } else {
        // fall back to Ostree if we cannot infer type
        crate::ffi::RefspecType::Ostree
    }
}

/// If the %__systemd_sysusers macro is defined, that _very likely_
/// means that we're using an rpm with built-in sysusers support:
/// https://fedoraproject.org/wiki/Changes/RPMSuportForSystemdSysusers Ideally,
/// librpm would expose that part as a public API but it's currently just
/// done during the transaction. So here we just do it ourselves, though a bit
/// differently: librpm checks for the user/group provides before proceeding to
/// call sysusers _just_ for that package. Here we do it for all of them. See
/// also: https://github.com/coreos/rpm-ostree/issues/5333.
pub(crate) fn run_sysusers(rootfs_dfd: i32) -> CxxResult<()> {
    let args: Vec<_> = ["rpm", "--eval=%{?__systemd_sysusers}"]
        .into_iter()
        .map(|s| s.to_string())
        .collect();
    let path = crate::bwrap::bubblewrap_run_sync(
        rootfs_dfd,
        &args,
        true,
        crate::ffi::BubblewrapMutability::Immutable,
    )?;
    let tmp = String::from_utf8(path).context("Could not read %__systemd_sysusers as UTF8")?;
    let cmd = tmp.trim();
    if !cmd.is_empty() {
        progress_task("Running systemd-sysusers", || -> CxxResult<()> {
            let args: Vec<_> = vec![cmd.into()];
            let _ = crate::bwrap::bubblewrap_run_sync(
                rootfs_dfd,
                &args,
                false,
                crate::ffi::BubblewrapMutability::MutateFreely,
            )?;
            Ok(())
        })?;
    }
    Ok(())
}

/// Perform reversible filesystem transformations necessary before we execute scripts.
pub(crate) struct FilesystemScriptPrep {
    rootfs: Dir,
    enabled: bool,
}

pub(crate) fn prepare_filesystem_script_prep(rootfs: i32) -> CxxResult<Box<FilesystemScriptPrep>> {
    let rootfs = unsafe { ffi_dirfd(rootfs)? };
    Ok(FilesystemScriptPrep::new(rootfs)?)
}

/// Using the Rust log infrastructure, print the treefile.
pub(crate) fn log_treefile(tf: &crate::treefile::Treefile) {
    tracing::debug!("Using treefile:\n{}", tf.get_json_string());
}

impl FilesystemScriptPrep {
    /// Filesystem paths that we rename out of the way if present
    const OPTIONAL_PATHS: &'static [&'static str] = &[SSS_CACHE_PATH];
    const REPLACE_OPTIONAL_PATHS: &'static [(&'static str, &'static [u8])] = &[
        (GROUPADD_PATH, GROUPADD_WRAPPER),
        (SYSTEMCTL_PATH, SYSTEMCTL_WRAPPER),
        (USERADD_PATH, USERADD_WRAPPER),
        (USERMOD_PATH, USERMOD_WRAPPER),
    ];

    // Separate const as is used only to wrap kernel-install if !is_ostree_layout()
    const REPLACE_KERNEL_PATHS: &'static [(&'static str, &'static [u8])] =
        &[(KERNEL_INSTALL_PATH, KERNEL_INSTALL_WRAPPER)];

    fn saved_name(name: &str) -> String {
        format!("{}.rpmostreesave", name)
    }

    #[context("Preparing filesystem for scripts")]
    pub(crate) fn new(rootfs: Dir) -> Result<Box<Self>> {
        for &path in Self::OPTIONAL_PATHS {
            if rootfs.try_exists(path)? {
                rootfs.rename(path, &rootfs, Self::saved_name(path))?;
            }
        }
        for &(path, contents) in Self::REPLACE_OPTIONAL_PATHS {
            let mode = Permissions::from_mode(0o755);
            let saved = &Self::saved_name(path);
            if rootfs.try_exists(path)? {
                rootfs.rename(path, &rootfs, saved)?;
                rootfs.atomic_write_with_perms(path, contents, mode)?;
            }
        }
        if !is_ostree_layout(&rootfs)? {
            for &(path, contents) in Self::REPLACE_KERNEL_PATHS {
                let mode = Permissions::from_mode(0o755);
                let saved = &Self::saved_name(path);
                if rootfs.try_exists(path)? {
                    rootfs.rename(path, &rootfs, saved)?;
                    rootfs.atomic_write_with_perms(path, contents, mode)?;
                }
            }
        }
        Ok(Box::new(Self {
            rootfs,
            enabled: true,
        }))
    }

    /// Undo the filesystem changes.
    #[context("Undoing prep filesystem for scripts")]
    pub(crate) fn undo(&mut self) -> CxxResult<()> {
        if !self.enabled {
            return Ok(());
        }
        for &path in Self::OPTIONAL_PATHS
            .iter()
            .chain(Self::REPLACE_OPTIONAL_PATHS.iter().map(|x| &x.0))
            .chain(Self::REPLACE_KERNEL_PATHS.iter().map(|x| &x.0))
        {
            let saved = &Self::saved_name(path);
            if self.rootfs.try_exists(saved)? {
                self.rootfs.rename(saved, &self.rootfs, path)?;
            }
        }
        self.enabled = false;
        Ok(())
    }
}

impl Drop for FilesystemScriptPrep {
    fn drop(&mut self) {
        let _ = self.undo();
    }
}

/// Some Fedora/RHEL kernels ship .hmac files with absolute paths inside,
/// which breaks when we relocate them into ostree/.  This function
/// changes them to be relative.
///
/// This is fixed in:
/// https://gitlab.com/cki-project/kernel-ark/-/merge_requests/1725
/// Until this lands everywhere we care about, we need this hack.
fn verify_kernel_hmac_impl(moddir: &Dir) -> Result<()> {
    // FIXME: in 2023
    // This method is intentionally a misnomer because it should eventually
    // be changed to the "sanity check" (below). It currently patches absolute
    // paths to give kernel package maintainers time to update their .spec files.

    const SEPARATOR: &str = "  ";

    let hmac_path = ".vmlinuz.hmac";

    let hmac_contents = if let Some(mut f) = moddir.open_optional(hmac_path)?.map(BufReader::new) {
        let mut s = String::new();
        f.read_to_string(&mut s)?;
        s
    } else {
        return Ok(());
    };

    // If the path is already relative, we're good.
    if !hmac_contents.contains('/') {
        return Ok(());
    }

    let (hmac, path) = hmac_contents
        .split_once(SEPARATOR)
        .ok_or_else(|| anyhow!("Missing path in .vmlinuz.hmac: {}", hmac_contents))?;
    let path = Utf8Path::new(path);

    let file_name = path
        .file_name()
        .ok_or_else(|| anyhow!("Missing filename in .vmlinuz.hmac: {}", hmac_contents))?;

    let new_contents = [hmac, SEPARATOR, file_name].concat();
    // sanity check
    if new_contents.contains('/') {
        return Err(anyhow!("Unexpected '/' in .vmlinuz.hmac: {}", new_contents));
    }

    let perms = Permissions::from_mode(0o644);
    moddir.atomic_write_with_perms(hmac_path, new_contents, perms)?;

    Ok(())
}

pub(crate) fn verify_kernel_hmac(rootfs: i32, moddir: &str) -> CxxResult<()> {
    let d = unsafe { &ffi_dirfd(rootfs)? };
    let moddir = d.open_dir(moddir)?;
    verify_kernel_hmac_impl(&moddir).map_err(Into::into)
}

/// Check if the commit has a serialized selinux policy sha256 that matches
/// the target policy's sha256.
pub(crate) fn commit_has_matching_sepolicy(
    commit: &crate::FFIGVariant,
    policy: &crate::FFIOstreeSePolicy,
) -> CxxResult<bool> {
    let commit = commit.glib_reborrow();
    let policy = policy.glib_reborrow();

    let sepolicy_csum = policy
        .csum()
        .ok_or_else(|| anyhow!("SELinux enabled, but no policy found"))?;

    let commitmeta = commit.child_value(0);
    let commitmeta = &glib::VariantDict::new(Some(&commitmeta));
    let key = "rpmostree.sepolicy";
    let v = commitmeta
        .lookup::<String>(key)
        .map_err(anyhow::Error::msg)?
        .ok_or_else(|| anyhow!("Missing metadata key {}", key))?;
    Ok(sepolicy_csum.as_str() == v.as_str())
}

/// Extract the rpm header as a GVariant of type ay (byte array)
pub(crate) fn get_header_variant(
    repo: &crate::FFIOstreeRepo,
    cachebranch: &str,
) -> CxxResult<*mut crate::FFIGVariant> {
    let repo = repo.glib_reborrow();

    let cached_rev = repo.require_rev(cachebranch)?;
    let cached_rev = cached_rev.as_str();
    let commit = repo.load_commit(cached_rev)?.0;
    let commitmeta = commit.child_value(0);
    let commitmeta = &glib::VariantDict::new(Some(&commitmeta));
    let key = "rpmostree.metadata";
    let r = commitmeta
        .lookup_value(key, Some(&*Vec::<u8>::static_variant_type()))
        .ok_or_else(|| anyhow!("Missing metadata key {}", key))
        .with_context(|| {
            let nevra = crate::rpmutils::cache_branch_to_nevra(cachebranch);
            format!("In commit {cached_rev} for {nevra}")
        })?;
    let p = r.as_ptr();
    std::mem::forget(r);
    Ok(p as *mut _)
}

pub(crate) fn stage_container_rpms(rpms: Vec<String>) -> CxxResult<Vec<String>> {
    let rpms: Result<Vec<File>> = rpms
        .into_iter()
        .map(|path| File::open(path).map_err(Into::into))
        .collect();
    stage_container_rpm_files(rpms?)
}

pub(crate) fn stage_container_rpm_raw_fds(fds: Vec<i32>) -> CxxResult<Vec<String>> {
    stage_container_rpm_files(
        fds.into_iter()
            .map(|fd| unsafe { File::from_raw_fd(fd) })
            .collect(),
    )
}

fn stage_container_rpm_files(rpms: Vec<File>) -> CxxResult<Vec<String>> {
    let mut r = Vec::new();
    let mut sack = dnf_sack_new();
    // XXX: This is really ugly: libdnf enforces that the filename ends in `.rpm`. So we use this
    // tempdir to hold symlinks to the fdpaths to fool it. Yuck. And we can't use cap_tempfile here
    // because the symlinks we create lead outside.
    let d = tempfile::tempdir()?;
    for mut rpm in rpms.into_iter() {
        let fdpath = format!("/proc/self/fd/{}", rpm.as_raw_fd());
        let symlink = format!("{}/{}.rpm", d.path().to_str().unwrap(), rpm.as_raw_fd());
        std::os::unix::fs::symlink(&fdpath, &symlink)?;
        let mut pkg = sack.pin_mut().add_cmdline_package(symlink)?;
        let chksum = crate::ffi::get_repodata_chksum_repr(&mut pkg.pin_mut().get_ref())?;
        let (alg, digest) = chksum
            .split_once(':')
            .ok_or_else(|| anyhow!("Missing ':' in chksum repr: {}", &chksum))?;
        if alg != "sha256" {
            return Err(anyhow!("expected sha256 hash, got {}", alg).into());
        }
        let staged_fn = format!("{}.rpm", digest);
        let run = cap_std::fs::Dir::open_ambient_dir("/run", cap_std::ambient_authority())?;
        run.create_dir_all(RPMOSTREE_CORE_STAGED_RPMS_DIR)?;
        let staged_rpms_dir = run.open_dir(RPMOSTREE_CORE_STAGED_RPMS_DIR)?;
        staged_rpms_dir.atomic_replace_with(&staged_fn, |f| -> std::io::Result<_> {
            std::io::copy(&mut rpm, f)
        })?;
        r.push(format!("{}:{}", digest, pkg.pin_mut().get_nevra()));
    }
    Ok(r)
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::capstdext::dirbuilder_from_mode;
    use anyhow::Result;
    use cap_std_ext::cap_tempfile;

    #[test]
    fn etcguard() -> Result<()> {
        let d = cap_tempfile::tempdir(cap_std::ambient_authority())?;
        let g = super::prepare_tempetc_guard(d.as_raw_fd())?;
        g.undo()?;
        let mut db = dirbuilder_from_mode(0o755);
        db.recursive(true);
        d.ensure_dir_with("usr/etc/foo", &db)?;
        assert!(!d.try_exists("etc/foo")?);
        let g = super::prepare_tempetc_guard(d.as_raw_fd())?;
        assert!(d.try_exists("etc/foo")?);
        g.undo()?;
        assert!(!d.try_exists("etc")?);
        assert!(d.try_exists("usr/etc/foo")?);
        Ok(())
    }

    #[test]
    fn rootfs() -> Result<()> {
        let d = cap_tempfile::tempdir(cap_std::ambient_authority())?;
        // The no-op case
        {
            let mut g = super::prepare_filesystem_script_prep(d.as_raw_fd())?;
            g.undo()?;
        }
        let mut db = dirbuilder_from_mode(0o755);
        let mode = Permissions::from_mode(0o755);
        db.recursive(true);
        d.ensure_dir_with("usr/bin", &db)?;
        d.ensure_dir_with("usr/sbin", &db)?;
        d.atomic_write_with_perms(super::SSS_CACHE_PATH, "sss binary", mode.clone())?;
        // Neutered sss_cache.
        {
            assert!(d.try_exists(super::SSS_CACHE_PATH)?);
            let mut g = super::prepare_filesystem_script_prep(d.as_raw_fd())?;
            assert!(!d.try_exists(super::SSS_CACHE_PATH)?);
            g.undo()?;
            assert!(d.try_exists(super::SSS_CACHE_PATH)?);
        }
        // Replaced systemctl.
        {
            let original_systemctl = "original systemctl";
            d.atomic_write_with_perms(super::SYSTEMCTL_PATH, original_systemctl, mode.clone())?;
            let contents = d.read_to_string(super::SYSTEMCTL_PATH)?;
            assert_eq!(contents, original_systemctl);
            let mut g = super::prepare_filesystem_script_prep(d.as_raw_fd())?;
            let contents = d.read_to_string(super::SYSTEMCTL_PATH)?;
            assert_eq!(contents.as_bytes(), super::SYSTEMCTL_WRAPPER);
            g.undo()?;
            let contents = d.read_to_string(super::SYSTEMCTL_PATH)?;
            assert_eq!(contents, original_systemctl);
        }
        // Replaced groupadd.
        {
            let original_groupadd = "original groupadd";
            d.atomic_write_with_perms(super::GROUPADD_PATH, original_groupadd, mode.clone())?;
            let contents = d.read_to_string(super::GROUPADD_PATH)?;
            assert_eq!(contents, original_groupadd);
            let mut g = super::prepare_filesystem_script_prep(d.as_raw_fd())?;
            let contents = d.read_to_string(super::GROUPADD_PATH)?;
            assert_eq!(contents.as_bytes(), super::GROUPADD_WRAPPER);
            g.undo()?;
            let contents = d.read_to_string(super::GROUPADD_PATH)?;
            assert_eq!(contents, original_groupadd);
        }
        // Replaced useradd.
        {
            let original_useradd = "original useradd";
            d.atomic_write_with_perms(super::USERADD_PATH, original_useradd, mode.clone())?;
            let contents = d.read_to_string(super::USERADD_PATH)?;
            assert_eq!(contents, original_useradd);
            let mut g = super::prepare_filesystem_script_prep(d.as_raw_fd())?;
            let contents = d.read_to_string(super::USERADD_PATH)?;
            assert_eq!(contents.as_bytes(), super::USERADD_WRAPPER);
            g.undo()?;
            let contents = d.read_to_string(super::USERADD_PATH)?;
            assert_eq!(contents, original_useradd);
        }
        // Replaced usermod.
        {
            let original_usermod = "original usermod";
            d.atomic_write_with_perms(super::USERMOD_PATH, original_usermod, mode.clone())?;
            let contents = d.read_to_string(super::USERMOD_PATH)?;
            assert_eq!(contents, original_usermod);
            let mut g = super::prepare_filesystem_script_prep(d.as_raw_fd())?;
            let contents = d.read_to_string(super::USERMOD_PATH)?;
            assert_eq!(contents.as_bytes(), super::USERMOD_WRAPPER);
            g.undo()?;
            let contents = d.read_to_string(super::USERMOD_PATH)?;
            assert_eq!(contents, original_usermod);
        }
        // Replaced kernel-install.
        {
            if !is_ostree_layout(&d)? {
                let original_kernel_install = "original kernel_install";
                d.atomic_write_with_perms(
                    super::KERNEL_INSTALL_PATH,
                    original_kernel_install,
                    mode,
                )?;
                let contents = d.read_to_string(super::KERNEL_INSTALL_PATH)?;
                assert_eq!(contents, original_kernel_install);
                let mut g = super::prepare_filesystem_script_prep(d.as_raw_fd())?;
                let contents = d.read_to_string(super::KERNEL_INSTALL_PATH)?;
                assert_eq!(contents.as_bytes(), super::KERNEL_INSTALL_WRAPPER);
                g.undo()?;
                let contents = d.read_to_string(super::KERNEL_INSTALL_PATH)?;
                assert_eq!(contents, original_kernel_install);
            }
        }
        Ok(())
    }

    #[test]
    fn test_refspecs() -> Result<()> {
        use super::is_container_image_reference;
        use super::refspec_classify;

        let refspec_type_checksum =
            "ee10f8e7ef638d78ba9a9596665067f58021624118875cc4079568da6c63efb0";
        assert!(!is_container_image_reference(refspec_type_checksum));
        assert_eq!(
            refspec_classify(refspec_type_checksum),
            crate::ffi::RefspecType::Checksum
        );

        let refspec_type_ostree_with_remote = "fedora:fedora/x86_64/coreos/testing-devel";
        assert!(!is_container_image_reference(
            refspec_type_ostree_with_remote
        ));
        assert_eq!(
            refspec_classify(refspec_type_ostree_with_remote),
            crate::ffi::RefspecType::Ostree
        );
        let refspec_type_ostree = "fedora/x86_64/coreos/foo-branch";
        assert!(!is_container_image_reference(refspec_type_ostree));
        assert_eq!(
            refspec_classify(refspec_type_ostree),
            crate::ffi::RefspecType::Ostree
        );

        const REFSPEC_TYPE_CONTAINER: &[&str] = &[
            "containers-storage:localhost/fcos:latest",
            "docker://quay.io/test-repository/os:version1",
            "registry:docker.io/test-repository/os:latest",
            "registry:customhostname.com:8080/test-repository/os:latest",
        ];

        for refspec in REFSPEC_TYPE_CONTAINER {
            let refspec = format!("ostree-unverified-image:{}", refspec);
            assert!(is_container_image_reference(&refspec));
            assert!(!is_container_image_digest_reference(&refspec));
            assert_eq!(
                refspec_classify(&refspec),
                crate::ffi::RefspecType::Container
            );
        }
        let refspec_type_container_digest = "docker://quay.io/test-repository/os@sha256:6006dca86c2dc549c123ff4f1dcbe60105fb05886531c93a3351ebe81dbe772f";
        let refspec = format!("ostree-unverified-image:{}", refspec_type_container_digest);
        assert!(is_container_image_reference(&refspec));
        assert!(is_container_image_digest_reference(&refspec));
        assert_eq!(
            refspec_classify(&refspec),
            crate::ffi::RefspecType::Container
        );

        Ok(())
    }

    #[test]
    fn verify_hmac() -> Result<()> {
        let d = cap_tempfile::tempdir(cap_std::ambient_authority())?;

        // No file is no-op
        verify_kernel_hmac_impl(&d).unwrap();

        // When the file is relative expect the function to be identity
        d.write(".vmlinuz.hmac", "abc123  a-relative-filename")?;
        verify_kernel_hmac_impl(&d).unwrap();
        assert_eq!(
            d.read_to_string(".vmlinuz.hmac")?,
            "abc123  a-relative-filename"
        );

        // Backwards compatability behavior
        d.write(".vmlinuz.hmac", "abc123  /an/absolute/filename.txt")?;
        verify_kernel_hmac_impl(&d).unwrap();
        assert_eq!(d.read_to_string(".vmlinuz.hmac")?, "abc123  filename.txt");

        // Sanity check compatability behavior
        d.write(".vmlinuz.hmac", "abc/123  /an/absolute/filename.txt")?;
        assert!(verify_kernel_hmac_impl(&d).is_err());

        Ok(())
    }
}
