elf_magic/
config.rs

1use std::{
2    collections::HashMap,
3    fs,
4    path::{Path, PathBuf},
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::error::Error;
10
11/// Configuration for elf-magic from package.metadata.elf-magic
12///
13/// Clean three-mode system: Magic (default single workspace) vs Permissive (multi-workspace with excludes) vs Laser Eyes (multi-workspace with includes)
14#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
15#[serde(tag = "mode", rename_all = "kebab-case")]
16pub enum Config {
17    #[serde(rename = "magic")]
18    Magic, // No fields! Just "run cargo metadata here"
19
20    #[serde(rename = "laser-eyes")]
21    LaserEyes {
22        workspaces: Vec<LaserEyesWorkspaceConfig>,
23        #[serde(default)]
24        constants: HashMap<String, String>,
25        #[serde(default)]
26        targets: HashMap<String, String>,
27    },
28
29    #[serde(rename = "permissive")]
30    Permissive {
31        workspaces: Vec<PermissiveWorkspaceConfig>,
32        #[serde(default)]
33        global_deny: Vec<String>,
34        #[serde(default)]
35        constants: HashMap<String, String>,
36        #[serde(default)]
37        targets: HashMap<String, String>,
38    },
39}
40
41impl Config {
42    pub fn load(manifest_dir: &Path) -> Result<Self, Error> {
43        let manifest_path = manifest_dir.join("Cargo.toml");
44        let content = fs::read_to_string(&manifest_path).map_err(|e| {
45            let message = format!("Failed to read Cargo.toml: {}", e);
46            Error::Config(message)
47        })?;
48
49        let toml_value: toml::Value = toml::from_str(&content).map_err(|e| {
50            let message = format!("Invalid TOML in Cargo.toml: {}", e);
51            Error::Config(message)
52        })?;
53
54        // Extract package.metadata.elf-magic, default to Magic mode if not present
55        let metadata = toml_value
56            .get("package")
57            .and_then(|p| p.get("metadata"))
58            .and_then(|m| m.get("elf-magic"));
59
60        match metadata {
61            Some(config_value) => {
62                let json_value = serde_json::to_value(config_value).map_err(|e| {
63                    let message = format!("Failed to convert config: {}", e);
64                    Error::Config(message)
65                })?;
66                serde_json::from_value(json_value).map_err(|e| {
67                    let message = format!("Invalid elf-magic config: {}", e);
68                    Error::Config(message)
69                })
70            }
71            None => Ok(Config::Magic),
72        }
73    }
74
75    /// Get the mode name as a string
76    pub fn mode_name(&self) -> &'static str {
77        match self {
78            Config::Magic => "magic",
79            Config::LaserEyes { .. } => "laser-eyes",
80            Config::Permissive { .. } => "permissive",
81        }
82    }
83
84    /// Get the constants map for this config
85    pub fn constants(&self) -> HashMap<String, String> {
86        match self {
87            Config::Magic => HashMap::new(),
88            Config::LaserEyes { constants, .. } => constants.clone(),
89            Config::Permissive { constants, .. } => constants.clone(),
90        }
91    }
92
93    /// Get the targets map for this config
94    pub fn targets(&self) -> HashMap<String, String> {
95        match self {
96            Config::Magic => HashMap::new(),
97            Config::LaserEyes { targets, .. } => targets.clone(),
98            Config::Permissive { targets, .. } => targets.clone(),
99        }
100    }
101}
102
103impl Default for Config {
104    fn default() -> Self {
105        Self::Magic
106    }
107}
108
109/// Configuration for a single workspace in laser-eyes mode
110#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
111pub struct LaserEyesWorkspaceConfig {
112    pub manifest_path: String,
113    pub only: Vec<String>,
114}
115
116/// Configuration for a single workspace in permissive mode
117#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
118pub struct PermissiveWorkspaceConfig {
119    pub manifest_path: String,
120    #[serde(default)]
121    #[serde(alias = "exclude")]
122    pub deny: Vec<String>,
123}
124
125/// Resolve constant override paths to absolute paths based on config file location
126pub fn resolve_constants_paths(
127    constants: &HashMap<String, String>,
128    config_file_dir: &Path,
129) -> HashMap<PathBuf, String> {
130    let mut resolved = HashMap::new();
131
132    for (relative_path, constant_name) in constants {
133        let absolute_path = config_file_dir.join(relative_path);
134        resolved.insert(absolute_path, constant_name.clone());
135    }
136
137    resolved
138}
139
140/// Resolve target override paths to absolute paths based on config file location
141pub fn resolve_targets_paths(
142    targets: &HashMap<String, String>,
143    config_file_dir: &Path,
144) -> HashMap<PathBuf, String> {
145    let mut resolved = HashMap::new();
146
147    for (relative_path, target_name) in targets {
148        let absolute_path = config_file_dir.join(relative_path);
149        resolved.insert(absolute_path, target_name.clone());
150    }
151
152    resolved
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use std::fs;
159    use tempfile::TempDir;
160
161    fn create_temp_manifest(content: &str) -> (TempDir, PathBuf) {
162        let temp_dir = TempDir::new().unwrap();
163        let manifest_path = temp_dir.path().join("Cargo.toml");
164        fs::write(&manifest_path, content).unwrap();
165        let path = temp_dir.path().to_path_buf();
166        (temp_dir, path)
167    }
168
169    #[test]
170    fn test_load_config_magic_mode_default() {
171        let manifest_content = r#"
172[package]
173name = "test-package"
174version = "0.1.0"
175edition = "2021"
176"#;
177
178        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
179        let config = Config::load(&manifest_dir).unwrap();
180
181        match config {
182            Config::Magic => {}
183            _ => panic!("Expected Magic mode"),
184        }
185    }
186
187    #[test]
188    fn test_load_config_magic_mode_explicit() {
189        let manifest_content = r#"
190[package]
191name = "test-package"
192version = "0.1.0"
193edition = "2021"
194
195[package.metadata.elf-magic]
196mode = "magic"
197"#;
198
199        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
200        let config = Config::load(&manifest_dir).unwrap();
201
202        match config {
203            Config::Magic => {}
204            _ => panic!("Expected Magic mode"),
205        }
206    }
207
208    #[test]
209    fn test_load_config_permissive_mode() {
210        let manifest_content = r#"
211[package]
212name = "test-package"
213version = "0.1.0"
214edition = "2021"
215
216[package.metadata.elf-magic]
217mode = "permissive"
218workspaces = [
219    { manifest_path = "./Cargo.toml" },
220    { manifest_path = "examples/basic/Cargo.toml", deny = ["target:test*"] }
221]
222"#;
223
224        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
225        let config = Config::load(&manifest_dir).unwrap();
226
227        match config {
228            Config::Permissive {
229                workspaces,
230                global_deny,
231                constants,
232                targets,
233            } => {
234                assert_eq!(workspaces.len(), 2);
235                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
236                assert_eq!(workspaces[0].deny.len(), 0);
237                assert_eq!(workspaces[1].manifest_path, "examples/basic/Cargo.toml");
238                assert_eq!(workspaces[1].deny, vec!["target:test*"]);
239                assert_eq!(global_deny.len(), 0); // No global excludes in this test
240                assert!(constants.is_empty());
241                assert!(targets.is_empty());
242            }
243            _ => panic!("Expected Permissive mode"),
244        }
245    }
246
247    #[test]
248    fn test_load_config_permissive_mode_with_exclude_alias() {
249        let manifest_content = r#"
250[package]
251name = "test-package"
252version = "0.1.0"
253edition = "2021"
254
255[package.metadata.elf-magic]
256mode = "permissive"
257workspaces = [
258    { manifest_path = "./Cargo.toml", exclude = ["target:test*"] }
259]
260"#;
261
262        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
263        let config = Config::load(&manifest_dir).unwrap();
264
265        match config {
266            Config::Permissive {
267                workspaces,
268                global_deny,
269                constants,
270                targets,
271            } => {
272                assert_eq!(workspaces.len(), 1);
273                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
274                assert_eq!(workspaces[0].deny, vec!["target:test*"]);
275                assert_eq!(global_deny.len(), 0);
276                assert!(constants.is_empty());
277                assert!(targets.is_empty());
278            }
279            _ => panic!("Expected Permissive mode"),
280        }
281    }
282
283    #[test]
284    fn test_load_config_missing_file() {
285        let temp_dir = TempDir::new().unwrap();
286        let non_existent_dir = temp_dir.path().join("non_existent");
287
288        let result = Config::load(&non_existent_dir);
289        assert!(result.is_err());
290        assert!(result
291            .unwrap_err()
292            .to_string()
293            .contains("Failed to read Cargo.toml"));
294    }
295
296    #[test]
297    fn test_load_config_invalid_toml() {
298        let manifest_content = r#"
299[package
300name = "test-package" -- invalid TOML syntax
301"#;
302
303        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
304        let result = Config::load(&manifest_dir);
305
306        assert!(result.is_err());
307        assert!(result.unwrap_err().to_string().contains("Invalid TOML"));
308    }
309
310    #[test]
311    fn test_load_config_invalid_elf_magic_config() {
312        let manifest_content = r#"
313[package]
314name = "test-package"
315version = "0.1.0"
316edition = "2021"
317
318[package.metadata.elf-magic]
319mode = "invalid-mode"
320"#;
321
322        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
323        let result = Config::load(&manifest_dir);
324
325        assert!(result.is_err());
326        let error_message = result.unwrap_err().to_string();
327        assert!(error_message.contains("Invalid elf-magic config"));
328    }
329
330    #[test]
331    fn test_config_default() {
332        let config = Config::default();
333        match config {
334            Config::Magic => {}
335            _ => panic!("Expected Magic mode as default"),
336        }
337    }
338
339    #[test]
340    fn test_workspace_config_defaults() {
341        let manifest_content = r#"
342[package]
343name = "test-package"
344version = "0.1.0"
345edition = "2021"
346
347[package.metadata.elf-magic]
348mode = "permissive"
349workspaces = [
350    { manifest_path = "./Cargo.toml" }
351]
352"#;
353
354        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
355        let config = Config::load(&manifest_dir).unwrap();
356
357        match config {
358            Config::Permissive {
359                workspaces,
360                global_deny,
361                constants,
362                targets,
363            } => {
364                assert_eq!(workspaces[0].deny.len(), 0); // Should default to empty
365                assert_eq!(global_deny.len(), 0); // Should default to empty
366                assert!(constants.is_empty());
367                assert!(targets.is_empty());
368            }
369            _ => panic!("Expected Permissive mode"),
370        }
371    }
372
373    #[test]
374    fn test_load_config_with_global_exclude() {
375        let manifest_content = r#"
376[package]
377name = "test-package"
378version = "0.1.0"
379edition = "2021"
380
381[package.metadata.elf-magic]
382mode = "permissive"
383global_deny = ["package:apl-token", "package:apl-associated-token-account"]
384workspaces = [
385    { manifest_path = "./Cargo.toml" },
386    { manifest_path = "examples/escrow/Cargo.toml", deny = ["target:test*"] }
387]
388"#;
389
390        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
391        let config = Config::load(&manifest_dir).unwrap();
392
393        match config {
394            Config::Permissive {
395                workspaces,
396                global_deny,
397                constants,
398                targets,
399            } => {
400                assert_eq!(workspaces.len(), 2);
401                assert_eq!(
402                    global_deny,
403                    vec!["package:apl-token", "package:apl-associated-token-account"]
404                );
405
406                // First workspace has no local excludes
407                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
408                assert_eq!(workspaces[0].deny.len(), 0);
409
410                // Second workspace has local excludes
411                assert_eq!(workspaces[1].manifest_path, "examples/escrow/Cargo.toml");
412                assert_eq!(workspaces[1].deny, vec!["target:test*"]);
413
414                assert!(constants.is_empty());
415                assert!(targets.is_empty());
416            }
417            _ => panic!("Expected Permissive mode"),
418        }
419    }
420
421    #[test]
422    fn test_load_config_laser_eyes_mode() {
423        let manifest_content = r#"
424[package]
425name = "test-package"
426version = "0.1.0"
427edition = "2021"
428
429[package.metadata.elf-magic]
430mode = "laser-eyes"
431workspaces = [
432    { manifest_path = "./Cargo.toml", only = ["target:token_manager", "target:governance"] },
433    { manifest_path = "examples/defi/Cargo.toml", only = ["target:swap*", "package:my-*-program"] }
434]
435"#;
436
437        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
438        let config = Config::load(&manifest_dir).unwrap();
439
440        match config {
441            Config::LaserEyes {
442                workspaces,
443                constants,
444                targets,
445            } => {
446                assert_eq!(workspaces.len(), 2);
447
448                // First workspace
449                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
450                assert_eq!(
451                    workspaces[0].only,
452                    vec!["target:token_manager", "target:governance"]
453                );
454
455                // Second workspace
456                assert_eq!(workspaces[1].manifest_path, "examples/defi/Cargo.toml");
457                assert_eq!(
458                    workspaces[1].only,
459                    vec!["target:swap*", "package:my-*-program"]
460                );
461
462                assert!(constants.is_empty());
463                assert!(targets.is_empty());
464            }
465            _ => panic!("Expected LaserEyes mode"),
466        }
467    }
468
469    #[test]
470    fn test_load_config_laser_eyes_mode_single_workspace() {
471        let manifest_content = r#"
472[package]
473name = "test-package"
474version = "0.1.0"
475edition = "2021"
476
477[package.metadata.elf-magic]
478mode = "laser-eyes"
479workspaces = [
480    { manifest_path = "./Cargo.toml", only = ["target:my_program"] }
481]
482"#;
483
484        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
485        let config = Config::load(&manifest_dir).unwrap();
486
487        match config {
488            Config::LaserEyes {
489                workspaces,
490                constants,
491                targets,
492            } => {
493                assert_eq!(workspaces.len(), 1);
494                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
495                assert_eq!(workspaces[0].only, vec!["target:my_program"]);
496                assert!(constants.is_empty());
497                assert!(targets.is_empty());
498            }
499            _ => panic!("Expected LaserEyes mode"),
500        }
501    }
502
503    #[test]
504    fn test_load_config_laser_eyes_mode_empty_include() {
505        let manifest_content = r#"
506[package]
507name = "test-package"
508version = "0.1.0"
509edition = "2021"
510
511[package.metadata.elf-magic]
512mode = "laser-eyes"
513workspaces = [
514    { manifest_path = "./Cargo.toml", only = [] }
515]
516"#;
517
518        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
519        let config = Config::load(&manifest_dir).unwrap();
520
521        match config {
522            Config::LaserEyes {
523                workspaces,
524                constants,
525                targets,
526            } => {
527                assert_eq!(workspaces.len(), 1);
528                assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
529                assert_eq!(workspaces[0].only.len(), 0);
530                assert!(constants.is_empty());
531                assert!(targets.is_empty());
532            }
533            _ => panic!("Expected LaserEyes mode"),
534        }
535    }
536
537    #[test]
538    fn test_load_config_laser_eyes_mode_with_constants_and_targets() {
539        let manifest_content = r#"
540[package]
541name = "test-package"
542version = "0.1.0"
543edition = "2021"
544
545[package.metadata.elf-magic]
546mode = "laser-eyes"
547workspaces = [
548    { manifest_path = "./upstream/Cargo.toml", only = ["path:*/program/*", "path:*/p-token/*"] }
549]
550constants = { "upstream/programs/token/program" = "SPL_TOKEN_PROGRAM_ELF", "upstream/programs/token/p-token" = "SPL_TOKEN_P_TOKEN_ELF" }
551targets = { "upstream/programs/token/p-token" = "potato" }
552"#;
553
554        let (_temp_dir, manifest_dir) = create_temp_manifest(manifest_content);
555        let config = Config::load(&manifest_dir).unwrap();
556
557        match config {
558            Config::LaserEyes {
559                workspaces,
560                constants,
561                targets,
562            } => {
563                assert_eq!(workspaces.len(), 1);
564                assert_eq!(workspaces[0].manifest_path, "./upstream/Cargo.toml");
565                assert_eq!(
566                    workspaces[0].only,
567                    vec!["path:*/program/*", "path:*/p-token/*"]
568                );
569
570                // Check constants
571                assert_eq!(constants.len(), 2);
572                assert_eq!(
573                    constants.get("upstream/programs/token/program"),
574                    Some(&"SPL_TOKEN_PROGRAM_ELF".to_string())
575                );
576                assert_eq!(
577                    constants.get("upstream/programs/token/p-token"),
578                    Some(&"SPL_TOKEN_P_TOKEN_ELF".to_string())
579                );
580
581                // Check targets
582                assert_eq!(targets.len(), 1);
583                assert_eq!(
584                    targets.get("upstream/programs/token/p-token"),
585                    Some(&"potato".to_string())
586                );
587            }
588            _ => panic!("Expected LaserEyes mode"),
589        }
590    }
591}