fission_command_core/
desktop_features.rs1use crate::Target;
2use anyhow::{bail, Context, Result};
3use serde::Deserialize;
4use std::collections::{BTreeMap, BTreeSet};
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Default, Deserialize)]
9struct PackageManifest {
10 package: Option<PackageRoot>,
11}
12
13#[derive(Debug, Default, Deserialize)]
14struct PackageRoot {
15 linux: Option<DesktopPackageConfig>,
16 macos: Option<DesktopPackageConfig>,
17 windows: Option<DesktopPackageConfig>,
18}
19
20#[derive(Debug, Default, Deserialize)]
21struct DesktopPackageConfig {
22 #[serde(default)]
23 cargo_features: Vec<String>,
24 #[serde(default)]
25 cargo_no_default_features: bool,
26 #[serde(default)]
27 variants: BTreeMap<String, DesktopPackageVariant>,
28}
29
30#[derive(Debug, Default, Deserialize)]
31struct DesktopPackageVariant {
32 cargo_features: Option<Vec<String>>,
33 cargo_no_default_features: Option<bool>,
34}
35
36#[derive(Clone, Debug, Default, Eq, PartialEq)]
37pub struct DesktopCargoOptions {
38 pub features: Vec<String>,
39 pub no_default_features: bool,
40}
41
42pub fn read_desktop_cargo_options(
43 project_dir: &Path,
44 target: Target,
45 variant: Option<&str>,
46) -> Result<DesktopCargoOptions> {
47 let path = project_dir.join("fission.toml");
48 let data =
49 fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
50 let manifest: PackageManifest =
51 toml::from_str(&data).with_context(|| format!("failed to parse {}", path.display()))?;
52 let config = match target {
53 Target::Linux => manifest.package.and_then(|package| package.linux),
54 Target::Macos => manifest.package.and_then(|package| package.macos),
55 Target::Windows => manifest.package.and_then(|package| package.windows),
56 _ => None,
57 };
58 let Some(config) = config else {
59 return Ok(DesktopCargoOptions::default());
60 };
61
62 let selected_variant = variant.and_then(|variant| config.variants.get(variant));
63 let configured_features = selected_variant
64 .and_then(|variant| variant.cargo_features.as_ref())
65 .unwrap_or(&config.cargo_features);
66 let mut features = BTreeSet::new();
67 for feature in configured_features {
68 validate_cargo_feature(feature)?;
69 features.insert(feature.clone());
70 }
71 Ok(DesktopCargoOptions {
72 features: features.into_iter().collect(),
73 no_default_features: selected_variant
74 .and_then(|variant| variant.cargo_no_default_features)
75 .unwrap_or(config.cargo_no_default_features),
76 })
77}
78
79fn validate_cargo_feature(feature: &str) -> Result<()> {
80 let valid = !feature.is_empty()
81 && feature.len() <= 128
82 && !feature.starts_with('-')
83 && !feature.contains(',')
84 && feature
85 .bytes()
86 .all(|byte| byte.is_ascii_graphic() && !matches!(byte, b'=' | b'\\'));
87 if !valid {
88 bail!("desktop cargo feature `{feature}` is not a safe Cargo feature selector");
89 }
90 Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn selected_variant_replaces_base_features_and_default_policy() {
99 let root =
100 std::env::temp_dir().join(format!("fission-desktop-features-{}", std::process::id()));
101 let _ = fs::remove_dir_all(&root);
102 fs::create_dir_all(&root).unwrap();
103 fs::write(
104 root.join("fission.toml"),
105 r#"
106[package.macos]
107cargo_features = ["base-only"]
108
109[package.macos.variants.app-store]
110cargo_features = ["macos-app-store", "shared", "shared"]
111cargo_no_default_features = true
112
113[package.windows.variants.store]
114cargo_features = ["windows-store"]
115"#,
116 )
117 .unwrap();
118
119 assert_eq!(
120 read_desktop_cargo_options(&root, Target::Macos, Some("app-store")).unwrap(),
121 DesktopCargoOptions {
122 features: vec!["macos-app-store".to_string(), "shared".to_string()],
123 no_default_features: true,
124 }
125 );
126 assert_eq!(
127 read_desktop_cargo_options(&root, Target::Macos, None).unwrap(),
128 DesktopCargoOptions {
129 features: vec!["base-only".to_string()],
130 no_default_features: false,
131 }
132 );
133 assert_eq!(
134 read_desktop_cargo_options(&root, Target::Windows, Some("store")).unwrap(),
135 DesktopCargoOptions {
136 features: vec!["windows-store".to_string()],
137 no_default_features: false,
138 }
139 );
140 assert!(
141 read_desktop_cargo_options(&root, Target::Linux, Some("app-store"))
142 .unwrap()
143 .features
144 .is_empty()
145 );
146 let _ = fs::remove_dir_all(&root);
147 }
148
149 #[test]
150 fn unsafe_feature_selectors_are_rejected() {
151 let root = std::env::temp_dir().join(format!(
152 "fission-desktop-features-invalid-{}",
153 std::process::id()
154 ));
155 let _ = fs::remove_dir_all(&root);
156 fs::create_dir_all(&root).unwrap();
157 fs::write(
158 root.join("fission.toml"),
159 r#"
160[package.macos.variants.store]
161cargo_features = ["safe,unsafe"]
162"#,
163 )
164 .unwrap();
165
166 assert!(
167 read_desktop_cargo_options(&root, Target::Macos, Some("store"))
168 .unwrap_err()
169 .to_string()
170 .contains("safe Cargo feature selector")
171 );
172 let _ = fs::remove_dir_all(&root);
173 }
174}