1use std::{
2 collections::HashMap,
3 fs,
4 path::{Path, PathBuf},
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::error::Error;
10
11#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
15#[serde(tag = "mode", rename_all = "kebab-case")]
16pub enum Config {
17 #[serde(rename = "magic")]
18 Magic, #[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 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 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 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 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#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
111pub struct LaserEyesWorkspaceConfig {
112 pub manifest_path: String,
113 pub only: Vec<String>,
114}
115
116#[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
125pub 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
140pub 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); 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); assert_eq!(global_deny.len(), 0); 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 assert_eq!(workspaces[0].manifest_path, "./Cargo.toml");
408 assert_eq!(workspaces[0].deny.len(), 0);
409
410 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 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 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 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 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}