elf_magic/
config.rs

1use std::{fs, path::Path};
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Error;
6
7/// Configuration for elf-magic from package.metadata.elf-magic
8///
9/// Clean three-mode system: Magic (default single workspace) vs Permissive (multi-workspace with excludes) vs Laser Eyes (multi-workspace with includes)
10#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
11#[serde(tag = "mode", rename_all = "kebab-case")]
12pub enum Config {
13    #[serde(rename = "magic")]
14    Magic, // No fields! Just "run cargo metadata here"
15
16    #[serde(rename = "laser-eyes")]
17    LaserEyes {
18        workspaces: Vec<LaserEyesWorkspaceConfig>,
19    },
20
21    #[serde(rename = "permissive")]
22    Permissive {
23        workspaces: Vec<PermissiveWorkspaceConfig>,
24        #[serde(default)]
25        global_deny: Vec<String>,
26    },
27}
28
29impl Config {
30    pub fn load(manifest_dir: &Path) -> Result<Self, Error> {
31        let manifest_path = manifest_dir.join("Cargo.toml");
32        let content = fs::read_to_string(&manifest_path).map_err(|e| {
33            let message = format!("Failed to read Cargo.toml: {}", e);
34            Error::Config(message)
35        })?;
36
37        let toml_value: toml::Value = toml::from_str(&content).map_err(|e| {
38            let message = format!("Invalid TOML in Cargo.toml: {}", e);
39            Error::Config(message)
40        })?;
41
42        // Extract package.metadata.elf-magic, default to Magic mode if not present
43        let metadata = toml_value
44            .get("package")
45            .and_then(|p| p.get("metadata"))
46            .and_then(|m| m.get("elf-magic"));
47
48        match metadata {
49            Some(config_value) => {
50                let json_value = serde_json::to_value(config_value).map_err(|e| {
51                    let message = format!("Failed to convert config: {}", e);
52                    Error::Config(message)
53                })?;
54                serde_json::from_value(json_value).map_err(|e| {
55                    let message = format!("Invalid elf-magic config: {}", e);
56                    Error::Config(message)
57                })
58            }
59            None => Ok(Config::Magic),
60        }
61    }
62}
63
64impl Default for Config {
65    fn default() -> Self {
66        Self::Magic
67    }
68}
69
70/// Configuration for a single workspace in laser-eyes mode
71#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
72pub struct LaserEyesWorkspaceConfig {
73    pub manifest_path: String,
74    pub only: Vec<String>,
75}
76
77/// Configuration for a single workspace in permissive mode
78#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
79pub struct PermissiveWorkspaceConfig {
80    pub manifest_path: String,
81    #[serde(default)]
82    #[serde(alias = "exclude")]
83    pub deny: Vec<String>,
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use std::fs;
90    use tempfile::TempDir;
91
92    fn create_temp_manifest(content: &str) -> (TempDir, std::path::PathBuf) {
93        let temp_dir = TempDir::new().unwrap();
94        let manifest_path = temp_dir.path().join("Cargo.toml");
95        fs::write(&manifest_path, content).unwrap();
96        let path = temp_dir.path().to_path_buf();
97        (temp_dir, path)
98    }
99
100    #[test]
101    fn test_load_config_magic_mode_default() {
102        let manifest_content = r#"
103[package]
104name = "test-package"
105version = "0.1.0"
106edition = "2021"
107"#;
108
109        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
110        let config = Config::load(&manifest_dir).unwrap();
111
112        match config {
113            Config::Magic => {}
114            _ => panic!("Expected Magic mode"),
115        }
116    }
117
118    #[test]
119    fn test_load_config_magic_mode_explicit() {
120        let manifest_content = r#"
121[package]
122name = "test-package"
123version = "0.1.0"
124edition = "2021"
125
126[package.metadata.elf-magic]
127mode = "magic"
128"#;
129
130        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
131        let config = Config::load(&manifest_dir).unwrap();
132
133        match config {
134            Config::Magic => {}
135            _ => panic!("Expected Magic mode"),
136        }
137    }
138
139    #[test]
140    fn test_load_config_permissive_mode() {
141        let manifest_content = r#"
142[package]
143name = "test-package"
144version = "0.1.0"
145edition = "2021"
146
147[package.metadata.elf-magic]
148mode = "permissive"
149workspaces = [
150    { manifest_path = "./Cargo.toml" },
151    { manifest_path = "examples/basic/Cargo.toml", deny = ["target:test*"] }
152]
153"#;
154
155        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
156        let config = Config::load(&manifest_dir).unwrap();
157
158        match config {
159            Config::Permissive {
160                workspaces,
161                global_deny,
162            } => {
163                assert_eq!(workspaces.len(), 2);
164                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
165                assert_eq!(workspaces[0].deny.len(), 0);
166                assert_eq!(workspaces[1].manifest_path, "examples/basic/Cargo.toml");
167                assert_eq!(workspaces[1].deny, vec!["target:test*"]);
168                assert_eq!(global_deny.len(), 0); // No global excludes in this test
169            }
170            _ => panic!("Expected Permissive mode"),
171        }
172    }
173
174    #[test]
175    fn test_load_config_permissive_mode_with_exclude_alias() {
176        let manifest_content = r#"
177[package]
178name = "test-package"
179version = "0.1.0"
180edition = "2021"
181
182[package.metadata.elf-magic]
183mode = "permissive"
184workspaces = [
185    { manifest_path = "./Cargo.toml", exclude = ["target:test*", "package:dev*"] }
186]
187"#;
188
189        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
190        let config = Config::load(&manifest_dir).unwrap();
191
192        match config {
193            Config::Permissive {
194                workspaces,
195                global_deny,
196            } => {
197                assert_eq!(workspaces.len(), 1);
198                assert_eq!(workspaces[0].deny, vec!["target:test*", "package:dev*"]);
199                assert_eq!(global_deny.len(), 0); // No global excludes in this test
200            }
201            _ => panic!("Expected Permissive mode"),
202        }
203    }
204
205    #[test]
206    fn test_load_config_missing_file() {
207        let temp_dir = TempDir::new().unwrap();
208        let non_existent = temp_dir.path().join("missing");
209
210        let result = Config::load(&non_existent);
211        assert!(result.is_err());
212        assert!(result
213            .unwrap_err()
214            .to_string()
215            .contains("Failed to read Cargo.toml"));
216    }
217
218    #[test]
219    fn test_load_config_invalid_toml() {
220        let invalid_toml = r#"
221[package
222name = "invalid"
223"#;
224
225        let (_temp_dir, manifest_dir) = create_temp_manifest(invalid_toml);
226        let result = Config::load(&manifest_dir);
227
228        assert!(result.is_err());
229        assert!(result.unwrap_err().to_string().contains("Invalid TOML"));
230    }
231
232    #[test]
233    fn test_load_config_invalid_elf_magic_config() {
234        let manifest_content = r#"
235[package]
236name = "test-package"
237version = "0.1.0"
238edition = "2021"
239
240[package.metadata.elf-magic]
241mode = "invalid-mode"
242"#;
243
244        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
245        let result = Config::load(&manifest_dir);
246
247        assert!(result.is_err());
248        assert!(result
249            .unwrap_err()
250            .to_string()
251            .contains("Invalid elf-magic config"));
252    }
253
254    #[test]
255    fn test_config_default() {
256        let config = Config::default();
257        match config {
258            Config::Magic => {}
259            _ => panic!("Default should be Magic mode"),
260        }
261    }
262
263    #[test]
264    fn test_workspace_config_defaults() {
265        let manifest_content = r#"
266[package]
267name = "test-package"
268version = "0.1.0"
269edition = "2021"
270
271[package.metadata.elf-magic]
272mode = "permissive"
273workspaces = [
274    { manifest_path = "./Cargo.toml" }
275]
276"#;
277
278        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
279        let config = Config::load(&manifest_dir).unwrap();
280
281        match config {
282            Config::Permissive {
283                workspaces,
284                global_deny,
285            } => {
286                assert_eq!(workspaces[0].deny.len(), 0); // Should default to empty
287                assert_eq!(global_deny.len(), 0); // Should default to empty
288            }
289            _ => panic!("Expected Permissive mode"),
290        }
291    }
292
293    #[test]
294    fn test_load_config_with_global_exclude() {
295        let manifest_content = r#"
296[package]
297name = "test-package"
298version = "0.1.0"
299edition = "2021"
300
301[package.metadata.elf-magic]
302mode = "permissive"
303global_deny = ["package:apl-token", "package:apl-associated-token-account"]
304workspaces = [
305    { manifest_path = "./Cargo.toml" },
306    { manifest_path = "examples/escrow/Cargo.toml", deny = ["target:test*"] }
307]
308"#;
309
310        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
311        let config = Config::load(&manifest_dir).unwrap();
312
313        match config {
314            Config::Permissive {
315                workspaces,
316                global_deny,
317            } => {
318                assert_eq!(workspaces.len(), 2);
319                assert_eq!(
320                    global_deny,
321                    vec!["package:apl-token", "package:apl-associated-token-account"]
322                );
323
324                // First workspace has no local excludes
325                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
326                assert_eq!(workspaces[0].deny.len(), 0);
327
328                // Second workspace has local excludes
329                assert_eq!(workspaces[1].manifest_path, "examples/escrow/Cargo.toml");
330                assert_eq!(workspaces[1].deny, vec!["target:test*"]);
331            }
332            _ => panic!("Expected Permissive mode"),
333        }
334    }
335
336    #[test]
337    fn test_load_config_laser_eyes_mode() {
338        let manifest_content = r#"
339[package]
340name = "test-package"
341version = "0.1.0"
342edition = "2021"
343
344[package.metadata.elf-magic]
345mode = "laser-eyes"
346workspaces = [
347    { manifest_path = "./Cargo.toml", only = ["target:token_manager", "target:governance"] },
348    { manifest_path = "examples/defi/Cargo.toml", only = ["target:swap*", "package:my-*-program"] }
349]
350"#;
351
352        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
353        let config = Config::load(&manifest_dir).unwrap();
354
355        match config {
356            Config::LaserEyes { workspaces } => {
357                assert_eq!(workspaces.len(), 2);
358
359                // First workspace
360                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
361                assert_eq!(
362                    workspaces[0].only,
363                    vec!["target:token_manager", "target:governance"]
364                );
365
366                // Second workspace
367                assert_eq!(workspaces[1].manifest_path, "examples/defi/Cargo.toml");
368                assert_eq!(
369                    workspaces[1].only,
370                    vec!["target:swap*", "package:my-*-program"]
371                );
372            }
373            _ => panic!("Expected LaserEyes mode"),
374        }
375    }
376
377    #[test]
378    fn test_load_config_laser_eyes_mode_single_workspace() {
379        let manifest_content = r#"
380[package]
381name = "test-package"
382version = "0.1.0"
383edition = "2021"
384
385[package.metadata.elf-magic]
386mode = "laser-eyes"
387workspaces = [
388    { manifest_path = "./Cargo.toml", only = ["target:my_program"] }
389]
390"#;
391
392        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
393        let config = Config::load(&manifest_dir).unwrap();
394
395        match config {
396            Config::LaserEyes { workspaces } => {
397                assert_eq!(workspaces.len(), 1);
398                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
399                assert_eq!(workspaces[0].only, vec!["target:my_program"]);
400            }
401            _ => panic!("Expected LaserEyes mode"),
402        }
403    }
404
405    #[test]
406    fn test_load_config_laser_eyes_mode_empty_include() {
407        let manifest_content = r#"
408[package]
409name = "test-package"
410version = "0.1.0"
411edition = "2021"
412
413[package.metadata.elf-magic]
414mode = "laser-eyes"
415workspaces = [
416    { manifest_path = "./Cargo.toml", only = [] }
417]
418"#;
419
420        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
421        let config = Config::load(&manifest_dir).unwrap();
422
423        match config {
424            Config::LaserEyes { workspaces } => {
425                assert_eq!(workspaces.len(), 1);
426                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
427                assert_eq!(workspaces[0].only.len(), 0);
428            }
429            _ => panic!("Expected LaserEyes mode"),
430        }
431    }
432}