ia-sandbox 0.4.0

A CLI to sandbox (jail) and collect usage of applications.
Documentation
use std::collections::HashMap;
use std::{fmt, path::PathBuf};

use anyhow::{anyhow, Context};
use clap::ValueEnum;
use serde::de::MapAccess;
use serde::{
    de::{self, Visitor},
    Deserialize, Deserializer,
};

use ia_sandbox::config::{Mount, MountOptions, SpaceUsage};

#[derive(Debug, Clone, ValueEnum, Deserialize, Default)]
#[allow(clippy::doc_markdown)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum OutputType {
    /// multiline string describing everything, not suitable for parsing
    #[default]
    Human,
    /// USER_TIME MEMORY VERDICT
    Oneline,
    /// a single json objects with 4 fields describing the run
    Json,
}

pub(crate) fn parse_space_usage(string: &str) -> anyhow::Result<SpaceUsage> {
    let number_index = string
        .find(|c: char| !c.is_ascii_digit())
        .with_context(|| {
            anyhow!("Could not find space usage suffix (b/kb/mb/gb/kib/mib/gib): {string}",)
        })?;

    let (number, suffix) = string.split_at(number_index);
    let number = number
        .parse::<u64>()
        .with_context(|| anyhow!("Could not parse number {number}"))?;
    match suffix {
        "b" => Ok(SpaceUsage::from_bytes(number)),
        "kb" => Ok(SpaceUsage::from_kilobytes(number)),
        "mb" => Ok(SpaceUsage::from_megabytes(number)),
        "gb" => Ok(SpaceUsage::from_gigabytes(number)),
        "kib" => Ok(SpaceUsage::from_kibibytes(number)),
        "mib" => Ok(SpaceUsage::from_mebibytes(number)),
        "gib" => Ok(SpaceUsage::from_gibibytes(number)),
        suffix => Err(anyhow!("Unrecognized suffix: {suffix}")),
    }
}

pub(crate) fn parse_space_usage_serde<'de, D: Deserializer<'de>>(
    d: D,
) -> Result<Option<SpaceUsage>, D::Error> {
    struct Inner(SpaceUsage);

    impl<'de> Deserialize<'de> for Inner {
        fn deserialize<D>(d: D) -> Result<Self, D::Error>
        where
            D: Deserializer<'de>,
        {
            struct V;

            impl<'de> Visitor<'de> for V {
                type Value = SpaceUsage;

                fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
                    fmt.write_str("valid space usage string")
                }

                fn visit_str<E: de::Error>(self, v: &str) -> Result<SpaceUsage, E> {
                    parse_space_usage(v).map_err(E::custom)
                }
            }
            d.deserialize_str(V).map(Inner)
        }
    }
    Ok(Option::<Inner>::deserialize(d)?.map(|Inner(usage)| usage))
}

fn parse_mount_options(string: &str) -> anyhow::Result<MountOptions> {
    let mut mount_options = MountOptions::default();

    for option in string.split(',') {
        match option {
            "rw" => mount_options.set_read_only(false),
            "dev" => mount_options.set_dev(true),
            "exec" => mount_options.set_exec(true),
            _ => {
                return Err(anyhow!(
                    "Could not parse mount option, unrecognized `{option}`",
                ));
            }
        }
    }
    Ok(mount_options)
}

fn parse_mount_options_serde<'de, D: Deserializer<'de>>(d: D) -> Result<MountOptions, D::Error> {
    struct V;

    impl<'de2> Visitor<'de2> for V {
        type Value = MountOptions;

        fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
            fmt.write_str("a mount option as a string")
        }

        fn visit_str<E>(self, s: &str) -> Result<MountOptions, E>
        where
            E: de::Error,
        {
            parse_mount_options(s).map_err(E::custom)
        }
    }
    d.deserialize_str(V)
}

pub(crate) fn parse_mount(string: &str) -> anyhow::Result<Mount> {
    let parts: Vec<&str> = string.split(':').collect();

    match parts.as_slice() {
        [source] => Ok(Mount::new(
            PathBuf::from(source),
            PathBuf::from(source),
            MountOptions::default(),
        )),
        [source, destination] => Ok(Mount::new(
            PathBuf::from(source),
            PathBuf::from(destination),
            MountOptions::default(),
        )),
        [source, destination, options] => Ok(Mount::new(
            PathBuf::from(source),
            PathBuf::from(destination),
            parse_mount_options(options)?,
        )),
        _ => Err(anyhow!("Could not parse mount")),
    }
}

pub(crate) fn parse_mounts_serde<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Mount>, D::Error> {
    struct MountWrapper(Mount);

    impl<'de> Deserialize<'de> for MountWrapper {
        fn deserialize<D>(d: D) -> Result<Self, D::Error>
        where
            D: Deserializer<'de>,
        {
            #[derive(Deserialize)]
            struct InnerMount {
                source: PathBuf,
                destination: PathBuf,
                #[serde(deserialize_with = "parse_mount_options_serde")]
                options: MountOptions,
            }
            struct V;

            impl<'de> Visitor<'de> for V {
                type Value = Mount;

                fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
                    fmt.write_str("a mount as a string or a source/destination/options table")
                }

                fn visit_str<E>(self, s: &str) -> Result<Mount, E>
                where
                    E: de::Error,
                {
                    parse_mount(s).map_err(E::custom)
                }

                fn visit_map<M>(self, map: M) -> Result<Mount, M::Error>
                where
                    M: MapAccess<'de>,
                {
                    Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(
                        |InnerMount {
                             source,
                             destination,
                             options,
                         }| Mount::new(source, destination, options),
                    )
                }
            }
            d.deserialize_any(V).map(MountWrapper)
        }
    }

    struct V;

    impl<'de> Visitor<'de> for V {
        type Value = Vec<Mount>;

        fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
            fmt.write_str("a list of mounts")
        }

        fn visit_seq<A>(self, mut seq: A) -> Result<Vec<Mount>, A::Error>
        where
            A: de::SeqAccess<'de>,
        {
            let mut result = Vec::new();
            if let Some(size_hint) = seq.size_hint() {
                result.reserve(size_hint);
            }
            while let Some(MountWrapper(next)) = seq.next_element()? {
                result.push(next);
            }
            Ok(result)
        }
    }
    d.deserialize_any(V)
}

pub(crate) fn parse_environment(string: &str) -> anyhow::Result<(String, String)> {
    string
        .split_once('=')
        .with_context(|| anyhow!("Could not parse env `{string}`"))
        .map(|(key, value)| (key.to_string(), value.to_string()))
}

pub(crate) fn parse_environment_serde<'de, D: Deserializer<'de>>(
    d: D,
) -> Result<Vec<(String, String)>, D::Error> {
    Ok(HashMap::<String, String>::deserialize(d)?
        .into_iter()
        .collect())
}