//! The generic catchall "utils" file that every
//! project has.

/*
 * Copyright (C) 2018 Red Hat, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 */

use crate::cxxrsutil::*;
use crate::variant_utils;
use anyhow::{bail, Context, Result};
use camino::Utf8Path;
use cap_std::io_lifetimes::AsFilelike;
use glib::Variant;
use once_cell::sync::Lazy;
use ostree_ext::prelude::*;
use ostree_ext::{glib, ostree};
use regex::Regex;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::io::prelude::*;
use std::os::fd::OwnedFd;
use std::os::unix::io::IntoRawFd;
use std::path::Path;
use std::{fs, io};

#[derive(Debug, PartialEq)]
/// Supported config serialization used by treefile and lockfile
pub enum InputFormat {
    YAML,
    JSON,
}

impl InputFormat {
    pub fn detect_from_filename<P: AsRef<Path>>(filename: P) -> Result<Self> {
        let filename = filename.as_ref();
        let basename = filename
            .file_name()
            .map(|s| s.to_string_lossy())
            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Expected a filename"))?;
        if basename.ends_with(".yaml") || basename.ends_with(".yml") {
            Ok(Self::YAML)
        } else {
            Ok(Self::JSON)
        }
    }
}

/// Given a lockfile/treefile config definition, parse it
pub fn parse_stream<T, R: io::Read>(fmt: &InputFormat, input: &mut R) -> Result<T>
where
    T: serde::de::DeserializeOwned,
{
    let parsed: T = match fmt {
        InputFormat::JSON => {
            let pf: T = serde_json::from_reader(input).map_err(|e| {
                io::Error::new(io::ErrorKind::InvalidInput, format!("serde-json: {e}"))
            })?;
            pf
        }
        InputFormat::YAML => {
            let pf: T = serde_yaml::from_reader(input).map_err(|e| {
                io::Error::new(io::ErrorKind::InvalidInput, format!("serde-yaml: {e}"))
            })?;
            pf
        }
    };
    Ok(parsed)
}

/// Given a URL, download it to an O_TMPFILE (temporary file descriptor).
/// This is a thin wrapper for `download_urls_to_tmpfiles`.
pub(crate) fn download_url_to_tmpfile(url: &str, progress: bool) -> Result<fs::File> {
    // Unwrap safety: multiple case returns the same number of values
    Ok(download_urls_to_tmpfiles(vec![url], progress)?
        .into_iter()
        .next()
        .expect("file"))
}

/// Given multiple URLs, download them to an O_TMPFILE (temporary file descriptor).
/// This uses sane defaults for fetching files, such as following the location
/// and making HTTP level errors also return an error rather than the error page HTML.
pub(crate) fn download_urls_to_tmpfiles<S: AsRef<str>>(
    urls: Vec<S>,
    progress: bool,
) -> Result<Vec<fs::File>> {
    let handle = reqwest::blocking::Client::new();
    urls.iter()
        .map(|url| {
            let url = url.as_ref();
            let mut tmpf = tempfile::tempfile()?;
            if progress {
                print!("Downloading {}...", url);
            }
            // Create an internally invoked closure so we can
            // capture the success/error case to complete the progress output line.
            let mut dl = || -> Result<()> {
                let mut output = io::BufWriter::new(&mut tmpf);
                let req = handle.get(url).build()?;
                handle
                    .execute(req)?
                    .error_for_status()?
                    .copy_to(&mut output)?;
                Ok(())
            };
            if progress {
                match dl() {
                    Ok(()) => println!("done"),
                    Err(e) => {
                        println!("failed");
                        return Err(e);
                    }
                }
            } else {
                dl()?;
            }
            tmpf.seek(io::SeekFrom::Start(0))?;
            Ok(tmpf)
        })
        .collect()
}

/// Open file for reading and provide context containing filename on failures.
pub fn open_file<P: AsRef<Path>>(filename: P) -> Result<fs::File> {
    fs::File::open(filename.as_ref()).with_context(|| {
        format!(
            "Can't open file {:?} for reading",
            filename.as_ref().display()
        )
    })
}

/// Open file for writing and provide context containing filename on failures.
pub fn create_file<P: AsRef<Path>>(filename: P) -> Result<fs::File> {
    fs::File::create(filename.as_ref()).with_context(|| {
        format!(
            "Can't open file {:?} for writing",
            filename.as_ref().display()
        )
    })
}

// Surprising we need a wrapper for this... parent() returns a slice of its buffer, so doesn't
// handle going up relative paths well: https://github.com/rust-lang/rust/issues/36861
pub fn parent_dir(filename: &Path) -> Option<&Path> {
    filename
        .parent()
        .map(|p| if p.as_os_str() == "" { ".".as_ref() } else { p })
}

/// Like [`parent_dir`] above but for UTF-8 paths.
pub fn parent_dir_utf8(filename: &Utf8Path) -> Option<&Utf8Path> {
    filename
        .parent()
        .map(|p| if p.as_os_str() == "" { ".".as_ref() } else { p })
}

/// Call a faillible future, while monitoring `cancellable` and return an error if cancelled.
pub(crate) async fn run_with_cancellable<F, R>(
    f: F,
    cancellable: &ostree_ext::gio::Cancellable,
) -> Result<R>
where
    F: futures::Future<Output = Result<R>>,
{
    // Bridge GCancellable to a tokio notification
    let notify = std::sync::Arc::new(tokio::sync::Notify::new());
    let notify2 = notify.clone();
    cancellable.connect_cancelled(move |_| notify2.notify_one());
    cancellable.set_error_if_cancelled()?;
    tokio::select! {
       r = f => r,
       _ = notify.notified() => {
           Err(anyhow::anyhow!("Operation was cancelled"))
       }
    }
}

/// Parse the 2-tuple `<sha256>:<nevra>` string into a tuple of (nevra, sha256).
/// Note the order is inverted, as it's expected to use the nevra as a key in
/// a mapping.
pub fn decompose_sha256_nevra(v: &str) -> Result<(&str, &str)> {
    let parts: Vec<&str> = v.splitn(2, ':').collect();
    match (parts.first(), parts.get(1)) {
        (Some(_), None) => bail!("Missing : in {}", v),
        (Some(sha256), Some(nevra)) => {
            ostree::validate_checksum_string(sha256)?;
            Ok((nevra, sha256))
        }
        (_, _) => unreachable!(),
    }
}

/// Given an input string `s`, replace variables of the form `${foo}` with
/// values provided in `vars`.  No quoting syntax is available, so it is
/// not possible to have a literal `${` in the string.
fn varsubst(instr: &str, vars: &HashMap<String, String>) -> Result<String> {
    let mut buf = instr;
    let mut s = String::with_capacity(instr.len());
    while !buf.is_empty() {
        if let Some(start) = buf.find("${") {
            let (prefix, rest) = buf.split_at(start);
            let rest = &rest[2..];
            s.push_str(prefix);
            if let Some(end) = rest.find('}') {
                let (varname, remainder) = rest.split_at(end);
                let remainder = &remainder[1..];
                if let Some(val) = vars.get(varname) {
                    s.push_str(val);
                } else {
                    bail!("Unknown variable reference ${{{}}}", varname);
                }
                buf = remainder;
            } else {
                bail!("Unclosed variable");
            }
        } else {
            s.push_str(buf);
            break;
        }
    }
    Ok(s)
}

static RUNNING_IN_SYSTEMD: Lazy<bool> = Lazy::new(|| {
    // See https://www.freedesktop.org/software/systemd/man/systemd.exec.html#%24INVOCATION_ID
    std::env::var_os("INVOCATION_ID")
        .filter(|s| !s.is_empty())
        .is_some()
});

/// Checks if the current process is (apparently at least)
/// running under systemd.  We use this in various places
/// to e.g. log to the journal instead of printing to stdout.
pub fn running_in_systemd() -> bool {
    *RUNNING_IN_SYSTEMD
}

pub fn maybe_shell_quote(input: &str) -> String {
    shellsafe_quote(Cow::Borrowed(input)).into_owned()
}

pub(crate) fn shellsafe_quote(input: Cow<str>) -> Cow<str> {
    static SHELLSAFE: Lazy<Regex> = Lazy::new(|| Regex::new("^[[:alnum:]-._/=:]+$").unwrap());

    if SHELLSAFE.is_match(&input) {
        input
    } else {
        let quoted = glib::shell_quote(input.as_ref());
        // SAFETY: if input is UTF-8, so is the quoted string.
        Cow::Owned(quoted.into_string().expect("non UTF-8 quoted output"))
    }
}

/// Translate a relative unprefixed path according to ostree rules, if needed.
///
/// This returns an ostree-compatible path, or an empty string if no translation
/// was performed.
pub fn translate_path_for_ostree(path: &str) -> String {
    translate_path_for_ostree_impl(path).unwrap_or_default()
}

/// Translate a relative unprefixed path according to ostree rules, if needed.
pub(crate) fn translate_path_for_ostree_impl(path: &str) -> Option<String> {
    assert!(!path.starts_with('/'));
    assert!(!path.starts_with("./"));

    // etc/foo -> usr/etc/foo
    if path == "etc" || path.starts_with("etc/") {
        return Some("usr/".to_string() + path);
    }

    // boot/foo -> usr/lib/ostree-boot/foo
    if let Some(prefixless) = path.strip_prefix("boot/") {
        return Some("usr/lib/ostree-boot/".to_string() + prefixless);
    }

    /* Special hack for https://bugzilla.redhat.com/show_bug.cgi?id=1290659
     * See also commit 4a86bdd19665700fa308461510c9decd63e31a03
     * and rpmostree_postprocess_selinux_policy_store_location().
     */
    if let Some(prefixless) = path.strip_prefix("var/lib/selinux/targeted/") {
        return Some("usr/etc/selinux/targeted/".to_string() + prefixless);
    }

    // opt/foo -> usr/lib/opt/foo
    if path == "opt" || path.starts_with("opt/") {
        return Some("usr/lib/".to_string() + path);
    }

    // var/lib/{special}/foo -> usr/lib/{special}/foo
    if let Some(prefixless) = path.strip_prefix("var/lib/") {
        let varlib_cases = &["alternatives", "vagrant"];
        for entry in varlib_cases {
            if prefixless.starts_with(entry) {
                return Some("usr/lib/".to_string() + prefixless);
            }
        }
    }

    // All remaining cases do not need translation.
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    fn subs() -> HashMap<String, String> {
        let mut h = HashMap::new();
        h.insert("basearch".to_string(), "ppc64le".to_string());
        h.insert("osvendor".to_string(), "fedora".to_string());
        h
    }

    fn test_noop(s: &str, subs: &HashMap<String, String>) {
        let r = varsubst(s, subs).unwrap();
        assert_eq!(r, s);
    }

    #[test]
    fn varsubst_noop() {
        let subs = subs();
        test_noop("", &subs);
        test_noop("foo bar baz", &subs);
        test_noop("}", &subs);
        test_noop("$} blah$ }}", &subs);
    }

    #[test]
    fn varsubsts() {
        let subs = subs();
        let r = varsubst(
            "ostree/${osvendor}/${basearch}/blah/${basearch}/whee",
            &subs,
        )
        .unwrap();
        assert_eq!(r, "ostree/fedora/ppc64le/blah/ppc64le/whee");
        let r = varsubst("${osvendor}${basearch}", &subs).unwrap();
        assert_eq!(r, "fedorappc64le");
        let r = varsubst("${osvendor}", &subs).unwrap();
        assert_eq!(r, "fedora");
    }

    #[test]
    fn test_decompose_sha256_nevra() -> Result<()> {
        assert!(decompose_sha256_nevra("").is_err());
        assert!(decompose_sha256_nevra("foo:bar").is_err());
        // Has a Q in the middle
        assert!(decompose_sha256_nevra(
            "foo:41af286dc0b172ed2f1ca934fd2278de4a119Q302ffa07087cea2682e7d372e3"
        )
        .is_err());
        assert!(decompose_sha256_nevra("foo:bar:baz").is_err());
        let c = "41af286dc0b172ed2f1ca934fd2278de4a1199302ffa07087cea2682e7d372e3";
        let foo = format!("{}:foo", c);
        let (n, p) = decompose_sha256_nevra(&foo).context("testing foo")?;
        assert_eq!(n, "foo");
        assert_eq!(p, c);
        Ok(())
    }

    #[test]
    fn more_varsubsts() -> Result<()> {
        let mut subs = HashMap::new();
        subs.insert("basearch".to_string(), "bacon".to_string());
        subs.insert("v".to_string(), "42".to_string());
        let subs = &subs;

        assert_eq!(varsubst("${basearch}", subs)?, "bacon");
        assert_eq!(varsubst("foo/${basearch}/bar", subs)?, "foo/bacon/bar");
        assert_eq!(varsubst("${basearch}/bar", subs)?, "bacon/bar");
        assert_eq!(varsubst("foo/${basearch}", subs)?, "foo/bacon");
        assert_eq!(
            varsubst("foo/${basearch}/${v}/bar", subs)?,
            "foo/bacon/42/bar"
        );
        assert_eq!(varsubst("${v}", subs)?, "42");

        let subs = HashMap::new();
        assert!(varsubst("${v}", &subs).is_err());
        assert!(varsubst("foo/${v}/bar", &subs).is_err());

        assert!(varsubst("${", &subs).is_err());
        assert!(varsubst("foo/${", &subs).is_err());
        Ok(())
    }

    #[test]
    fn test_open_file_nonexistent() {
        let path = "/usr/share/empty/manifest.yaml";
        match open_file(path) {
            Err(ref e) => assert!(e
                .to_string()
                .starts_with(format!("Can't open file {:?} for reading", path).as_str())),
            Ok(_) => panic!("Expected nonexistent treefile error for {}", path),
        }
    }

    // Note this is testing C++ code defined in rpmostree-util.cxx
    #[test]
    fn test_next_version() {
        use crate::ffi::util_next_version;
        fn assert_ver(prefix: &str, v: &str, res: &str) {
            assert_eq!(util_next_version(prefix, "", v).expect("version"), res);
        }
        for v in &["", "xyz", "9", "11"] {
            assert_ver("10", v, "10");
        }

        assert_ver("10", "10", "10.1");
        assert_ver("10.1", "10.1", "10.1.1");
        assert_ver("10", "10.0", "10.1");
        assert_ver("10", "10.1", "10.2");
        assert_ver("10", "10.2", "10.3");
        assert_ver("10", "10.3", "10.4");
        assert_ver("10", "10.1.5", "10.2");
        assert_ver("10.1", "10.1.5", "10.1.6");
        assert_ver("10.1", "10.1.1.5", "10.1.2");
        assert_ver("10", "10001", "10");
        assert_ver("10", "101.1", "10");
        assert_ver("10", "10x.1", "10");
        assert_ver("10.1", "10", "10.1");
        assert_ver("10.1", "10.", "10.1");
        assert_ver("10.1", "10.0", "10.1");
        assert_ver("10.1", "10.2", "10.1");
        assert_ver("10.1", "10.12", "10.1");
        assert_ver("10.1", "10.1x", "10.1");
        assert_ver("10.1", "10.1.x", "10.1.1");
        assert_ver("10.1", "10.1.2x", "10.1.3");

        assert_eq!(util_next_version("10", "-", "10").unwrap(), "10-1");
        assert_eq!(util_next_version("10", "-", "10-1").unwrap(), "10-2");
        assert_eq!(util_next_version("10.1", "-", "10.1-5").unwrap(), "10.1-6");

        fn t(pre: &str, last: &str, final_datefmt: &str) {
            let now = glib::DateTime::now_utc().unwrap();
            let final_version = now.format(final_datefmt).unwrap();
            let ver = util_next_version(pre, "", last).expect("version");
            assert_eq!(ver, final_version);
        }
        // Test date updates
        t("10.<date:%Y%m%d>", "10.20001010", "10.%Y%m%d.0");

        // Test increment reset when date changed.
        t("10.<date:%Y%m%d>", "10.20001010.5", "10.%Y%m%d.0");

        let now = glib::DateTime::now_utc().unwrap();
        // Test increment up when same date.
        let prev_version = now.format("10.%Y%m%d.1").unwrap();
        t("10.<date:%Y%m%d>", prev_version.as_str(), "10.%Y%m%d.2");

        // Test append version number.
        t("10.<date:%Y%m%d>", "", "10.%Y%m%d.0");
        let prev_version = now.format("10.%Y%m%d").unwrap();
        t("10.<date:%Y%m%d>.0", prev_version.as_str(), "10.%Y%m%d.0.0");
        let prev_version = now.format("10.%Y%m%d.0").unwrap();
        t("10.<date:%Y%m%d>.0", prev_version.as_str(), "10.%Y%m%d.0.0");
        let prev_version = now.format("10.%Y%m%d.x").unwrap();
        t("10.<date:%Y%m%d>", prev_version.as_str(), "10.%Y%m%d.1");
        let prev_version = now.format("10.%Y%m%d.2.x").unwrap();
        t("10.<date:%Y%m%d>.2", prev_version.as_str(), "10.%Y%m%d.2.1");
        let prev_version = now.format("10.%Y%m%d.1.2x").unwrap();
        t("10.<date:%Y%m%d>.1", prev_version.as_str(), "10.%Y%m%d.1.3");

        // Test variations to the formatting.
        t("10.<date: %Y%m%d>", "10.20001010", "10. %Y%m%d.0");
        t("10.<date:%Y%m%d>.", "10.20001010.", "10.%Y%m%d..0");
        t("10.<date:%Y%m%d>abc", "10.20001010abc", "10.%Y%m%dabc.0");
        t("10.<date:%Y%m%d >", "10.20001010", "10.%Y%m%d .0");
        t(
            "10.<date:text%Y%m%dhere>",
            "10.20001010",
            "10.text%Y%m%dhere.0",
        );
        t(
            "10.<date:text %Y%m%d here>",
            "10.20001010",
            "10.text %Y%m%d here.0",
        );
        t("10.<date:%Y%m%d here>", "10.20001010", "10.%Y%m%d here.0");

        // Test equal last version and prefix.
        let prev_version = now.format("10.%Y%m%d").unwrap();
        t("10.<date:%Y%m%d>", prev_version.as_str(), "10.%Y%m%d.0");

        // Test different prefix from last version.
        t("10.<date:%Y%m%d>", "10", "10.%Y%m%d.0");

        // Test no field given.
        t("10.<date: >", "10.20001010", "10. .0");
        t("10.<date:>", "10.20001010", "10..0");
        t("10.<wrongtag: >", "10.20001010", "10.<wrongtag: >");

        // Test invalid format
        assert!(util_next_version("10.<date:%E>", "", "10.20001010").is_err());
    }

    #[test]
    fn test_translate_path_for_ostree() {
        let cases = [
            ("usr/lib/foo", None),
            ("var/lib/foo", None),
            ("etc", Some("usr/etc")),
            ("etc/foo", Some("usr/etc/foo")),
            ("boot/foo", Some("usr/lib/ostree-boot/foo")),
            (
                "var/lib/selinux/targeted/foo",
                Some("usr/etc/selinux/targeted/foo"),
            ),
            ("opt", Some("usr/lib/opt")),
            ("opt/foo", Some("usr/lib/opt/foo")),
            ("var/lib/alternatives/foo", Some("usr/lib/alternatives/foo")),
            ("var/lib/vagrant/foo", Some("usr/lib/vagrant/foo")),
        ];

        for (input, expected) in cases {
            let translated = translate_path_for_ostree_impl(input);
            assert_eq!(
                translated,
                expected.map(|v| v.to_string()),
                "Input: {input}"
            );
        }
    }
}

/// TODO: cxx-rs doesn't support maps yet
pub(crate) fn varsubstitute(s: &str, subs: &Vec<crate::ffi::StringMapping>) -> CxxResult<String> {
    let m = subs.iter().cloned().map(|i| (i.k, i.v)).collect();
    Ok(varsubst(s, &m)?)
}

pub(crate) fn get_features() -> Vec<String> {
    // These constant features were originally set in configure.ac, but have migrated to
    // Rust in the interest in having less logic in autoconf.
    let defaults = ["rust", "compose", "container"].into_iter().map(Some);
    let conditionals = [
        cfg!(feature = "fedora-integration").then(|| "fedora-integration"),
        cfg!(feature = "bin-unit-tests").then(|| "bin-unit-tests"),
        cfg!(feature = "rhsm").then(|| "rhsm"),
    ]
    .into_iter();
    defaults
        .chain(conditionals)
        .flatten()
        .map(String::from)
        .collect()
}

pub(crate) fn impl_sealed_memfd(description: &str, content: &[u8]) -> Result<OwnedFd> {
    use rustix::fs::{MemfdFlags, SealFlags};
    let mfd =
        rustix::fs::memfd_create(description, MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING)?;

    {
        let mfd_file = mfd.as_filelike_view::<std::fs::File>();
        mfd_file.set_len(content.len() as u64)?;
        (&*mfd_file).write_all(content)?;
        (&*mfd_file).seek(std::io::SeekFrom::Start(0))?;
    }

    rustix::fs::fcntl_add_seals(
        &mfd,
        SealFlags::WRITE | SealFlags::GROW | SealFlags::SHRINK | SealFlags::SEAL,
    )?;
    Ok(mfd)
}

/// Create a fully sealed "memfd" (memory file descriptor) from an array of bytes.
/// For more information see `man memfd_create`.
pub(crate) fn sealed_memfd(description: &str, content: &[u8]) -> CxxResult<i32> {
    let mfd = impl_sealed_memfd(description, content)?;
    Ok(mfd.into_raw_fd())
}

/// Map Rust architecture constants to the ones used by DNF.
///
/// Rust architecture constants: https://doc.rust-lang.org/std/env/consts/constant.ARCH.html
/// DNF mapping: https://github.com/rpm-software-management/dnf/blob/4.5.2/dnf/rpm/__init__.py#L88
pub fn get_rpm_basearch() -> String {
    match std::env::consts::ARCH {
        "arm" => "armhfp".to_string(),
        "powerpc" => "ppc".to_string(),
        "powerpc64" if cfg!(target_endian = "big") => "ppc64".to_string(),
        "powerpc64" if cfg!(target_endian = "little") => "ppc64le".to_string(),
        "sparc64" => "sparc".to_string(),
        "x86" => "i386".to_string(),
        s => s.to_string(),
    }
}

// oddly, AFAICT there isn't an easy way to use GV_ADVISORIES_TYPE_STR in this definition
const GV_ADVISORIES_TYPE_STR: &str = "a(suuasa{sv})";

static GV_ADVISORIES_TYPE: Lazy<&'static glib::VariantTy> =
    Lazy::new(|| glib::VariantTy::new(GV_ADVISORIES_TYPE_STR).unwrap());

fn get_commit_advisories(repo: &ostree::Repo, checksum: &str) -> Result<Option<glib::Variant>> {
    let (commit, _) = repo.load_commit(checksum)?;
    let metadata = &commit.child_value(0);
    let metadata = variant_utils::byteswap_be_to_native(metadata);
    let dict = &glib::VariantDict::new(Some(&metadata));
    Ok(dict.lookup_value("rpmostree.advisories", Some(&GV_ADVISORIES_TYPE)))
}

/// Compares advisories between two checksums and returns new ones in checksum_to ("diff" is a bit
/// misleading here; we don't actually say which advisories were removed because normally we only
/// really care about *new* advisories).
pub(crate) fn calculate_advisories_diff(
    repo: &crate::FFIOstreeRepo,
    checksum_from: &str,
    checksum_to: &str,
) -> Result<*mut crate::FFIGVariant> {
    let repo = repo.glib_reborrow();
    let mut new_advisories: Vec<glib::Variant> = Vec::new();
    if let Some(ref advisories_to) = get_commit_advisories(&repo, checksum_to)? {
        let mut previous_advisories: HashSet<String> = HashSet::new();
        if let Some(ref advisories_from) = get_commit_advisories(&repo, checksum_from)? {
            for child in advisories_from.iter() {
                let advisory_id = child.child_value(0).str().unwrap().to_string();
                previous_advisories.insert(advisory_id);
            }
        }

        for child in advisories_to.iter() {
            if !previous_advisories.contains(child.child_value(0).str().unwrap()) {
                new_advisories.push(child);
            }
        }
    }

    let r = Variant::array_from_iter::<(&str, u32, u32, &[&str], HashMap<&str, glib::Variant>)>(
        new_advisories,
    );
    let p = r.as_ptr();
    std::mem::forget(r);
    Ok(p as *mut _)
}

/// Implementation of https://github.com/rust-lang/rust/issues/82901 until it's stable.
pub(crate) trait OptionExtGetOrInsertDefault<T> {
    fn ext_get_or_insert_default(&mut self) -> &mut T;
}

impl<T: Default> OptionExtGetOrInsertDefault<T> for Option<T> {
    fn ext_get_or_insert_default(&mut self) -> &mut T {
        match self {
            Some(ref mut t) => t,
            None => self.insert(Default::default()),
        }
    }
}

#[test]
fn test_sealed_memfd() -> Result<()> {
    let contents = "some contents here";
    let mfd = impl_sealed_memfd("foo", contents.as_bytes()).unwrap();
    {
        let mfd = mfd.as_filelike_view::<std::fs::File>();
        let mut buf = String::new();
        (&*mfd).read_to_string(&mut buf)?;
        assert_eq!(buf, contents);
    }
    Ok(())
}
