fission-command-core 0.14.1

Shared project, manifest, target, and diagnostic model for the fission command
Documentation
use crate::Target;
use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;

#[derive(Debug, Default, Deserialize)]
struct PackageManifest {
    package: Option<PackageRoot>,
}

#[derive(Debug, Default, Deserialize)]
struct PackageRoot {
    linux: Option<DesktopPackageConfig>,
    macos: Option<DesktopPackageConfig>,
    windows: Option<DesktopPackageConfig>,
}

#[derive(Debug, Default, Deserialize)]
struct DesktopPackageConfig {
    #[serde(default)]
    cargo_features: Vec<String>,
    #[serde(default)]
    cargo_no_default_features: bool,
    #[serde(default)]
    variants: BTreeMap<String, DesktopPackageVariant>,
}

#[derive(Debug, Default, Deserialize)]
struct DesktopPackageVariant {
    cargo_features: Option<Vec<String>>,
    cargo_no_default_features: Option<bool>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DesktopCargoOptions {
    pub features: Vec<String>,
    pub no_default_features: bool,
}

pub fn read_desktop_cargo_options(
    project_dir: &Path,
    target: Target,
    variant: Option<&str>,
) -> Result<DesktopCargoOptions> {
    let path = project_dir.join("fission.toml");
    let data =
        fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
    let manifest: PackageManifest =
        toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))?;
    let config = match target {
        Target::Linux => manifest.package.and_then(|package| package.linux),
        Target::Macos => manifest.package.and_then(|package| package.macos),
        Target::Windows => manifest.package.and_then(|package| package.windows),
        _ => None,
    };
    let Some(config) = config else {
        return Ok(DesktopCargoOptions::default());
    };

    let selected_variant = variant.and_then(|variant| config.variants.get(variant));
    let configured_features = selected_variant
        .and_then(|variant| variant.cargo_features.as_ref())
        .unwrap_or(&config.cargo_features);
    let mut features = BTreeSet::new();
    for feature in configured_features {
        validate_cargo_feature(feature)?;
        features.insert(feature.clone());
    }
    Ok(DesktopCargoOptions {
        features: features.into_iter().collect(),
        no_default_features: selected_variant
            .and_then(|variant| variant.cargo_no_default_features)
            .unwrap_or(config.cargo_no_default_features),
    })
}

fn validate_cargo_feature(feature: &str) -> Result<()> {
    let valid = !feature.is_empty()
        && feature.len() <= 128
        && !feature.starts_with('-')
        && !feature.contains(',')
        && feature
            .bytes()
            .all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'=' | b'\\'));
    if !valid {
        bail!("desktop cargo feature `{feature}` is not a safe Cargo feature selector");
    }
    Ok(())
}

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

    #[test]
    fn selected_variant_replaces_base_features_and_default_policy() {
        let root =
            std::env::temp_dir().join(format!("fission-desktop-features-{}", std::process::id()));
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(&root).unwrap();
        fs::write(
            root.join("fission.toml"),
            r#"
[package.macos]
cargo_features = ["base-only"]

[package.macos.variants.app-store]
cargo_features = ["macos-app-store", "shared", "shared"]
cargo_no_default_features = true

[package.windows.variants.store]
cargo_features = ["windows-store"]
"#,
        )
        .unwrap();

        assert_eq!(
            read_desktop_cargo_options(&root, Target::Macos, Some("app-store")).unwrap(),
            DesktopCargoOptions {
                features: vec!["macos-app-store".to_string(), "shared".to_string()],
                no_default_features: true,
            }
        );
        assert_eq!(
            read_desktop_cargo_options(&root, Target::Macos, None).unwrap(),
            DesktopCargoOptions {
                features: vec!["base-only".to_string()],
                no_default_features: false,
            }
        );
        assert_eq!(
            read_desktop_cargo_options(&root, Target::Windows, Some("store")).unwrap(),
            DesktopCargoOptions {
                features: vec!["windows-store".to_string()],
                no_default_features: false,
            }
        );
        assert!(
            read_desktop_cargo_options(&root, Target::Linux, Some("app-store"))
                .unwrap()
                .features
                .is_empty()
        );
        let _ = fs::remove_dir_all(&root);
    }

    #[test]
    fn unsafe_feature_selectors_are_rejected() {
        let root = std::env::temp_dir().join(format!(
            "fission-desktop-features-invalid-{}",
            std::process::id()
        ));
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(&root).unwrap();
        fs::write(
            root.join("fission.toml"),
            r#"
[package.macos.variants.store]
cargo_features = ["safe,unsafe"]
"#,
        )
        .unwrap();

        assert!(
            read_desktop_cargo_options(&root, Target::Macos, Some("store"))
                .unwrap_err()
                .to_string()
                .contains("safe Cargo feature selector")
        );
        let _ = fs::remove_dir_all(&root);
    }
}