1#![forbid(unsafe_code)]
7
8pub mod model;
9
10pub use model::Manifest;
11
12use std::fs;
13use std::path::Path;
14use vanta_core::{Area, VtaError, VtaResult};
15
16pub fn parse_str(src: &str, origin: &str) -> VtaResult<Manifest> {
19 toml::from_str::<Manifest>(src).map_err(|e| cfg_error(src, origin, &e))
20}
21
22pub fn load_file(path: &Path) -> VtaResult<Manifest> {
24 let src = fs::read_to_string(path).map_err(|e| {
25 VtaError::new(
26 Area::Cfg,
27 1,
28 format!("cannot read manifest {}: {e}", path.display()),
29 )
30 })?;
31 parse_str(&src, &path.display().to_string())
32}
33
34pub fn merge(global: &Manifest, project: &Manifest) -> Manifest {
37 let mut out = global.clone();
38 out.tools.extend(project.tools.clone());
39 out.env.extend(project.env.clone());
40 out.tasks.extend(project.tasks.clone());
41 out.registries.extend(project.registries.clone());
42 out.settings = merge_settings(&global.settings, &project.settings);
43 if project.workspace.is_some() {
44 out.workspace = project.workspace.clone();
45 }
46 if project.version.is_some() {
47 out.version = project.version;
48 }
49 out
50}
51
52fn merge_settings(g: &model::Settings, p: &model::Settings) -> model::Settings {
53 macro_rules! pick {
54 ($field:ident) => {
55 p.$field.clone().or_else(|| g.$field.clone())
56 };
57 }
58 model::Settings {
59 auto_install: pick!(auto_install),
60 verify: pick!(verify),
61 jobs: pick!(jobs),
62 link_strategy: pick!(link_strategy),
63 shims: pick!(shims),
64 offline: pick!(offline),
65 mirror: pick!(mirror),
66 targets: pick!(targets),
67 retain_generations: pick!(retain_generations),
68 gc_keep_days: pick!(gc_keep_days),
69 color: pick!(color),
70 telemetry: pick!(telemetry),
71 read_foreign_versions: pick!(read_foreign_versions),
72 }
73}
74
75fn cfg_error(src: &str, origin: &str, e: &toml::de::Error) -> VtaError {
76 let (line, col) = e.span().map(|s| line_col(src, s.start)).unwrap_or((0, 0));
77 VtaError::new(Area::Cfg, 1, format!("{e} ({origin}:{line}:{col})"))
78}
79
80fn line_col(src: &str, byte: usize) -> (usize, usize) {
82 let mut line = 1usize;
83 let mut col = 1usize;
84 for (i, ch) in src.char_indices() {
85 if i >= byte {
86 break;
87 }
88 if ch == '\n' {
89 line += 1;
90 col = 1;
91 } else {
92 col += 1;
93 }
94 }
95 (line, col)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::model::ToolSpec;
101 use super::*;
102
103 #[test]
104 fn parses_minimal() {
105 let m = parse_str("[tools]\nnode = \"24\"\npython = \"3.13\"\n", "<test>").unwrap();
106 assert_eq!(m.tools["node"].version(), "24");
107 assert_eq!(m.tools["python"].version(), "3.13");
108 }
109
110 #[test]
111 fn parses_detailed_tool() {
112 let m = parse_str(
113 "[tools]\nripgrep = { version = \"14\", os = [\"macos\", \"linux\"] }\n",
114 "<test>",
115 )
116 .unwrap();
117 match &m.tools["ripgrep"] {
118 ToolSpec::Detailed(d) => {
119 assert_eq!(d.version, "14");
120 assert_eq!(
121 d.os.as_deref(),
122 Some(&["macos".to_string(), "linux".to_string()][..])
123 );
124 }
125 _ => panic!("expected detailed"),
126 }
127 }
128
129 #[test]
130 fn unknown_top_level_key_is_error() {
131 let err = parse_str("[nonsense]\nx = 1\n", "<test>").unwrap_err();
132 assert_eq!(err.area, Area::Cfg);
133 }
134
135 #[test]
136 fn project_overrides_global() {
137 let g = parse_str("[tools]\nnode = \"24\"\nripgrep = \"14\"\n", "<g>").unwrap();
138 let p = parse_str("[tools]\nnode = \"20\"\n", "<p>").unwrap();
139 let merged = merge(&g, &p);
140 assert_eq!(merged.tools["node"].version(), "20"); assert_eq!(merged.tools["ripgrep"].version(), "14"); }
143
144 #[test]
145 fn settings_merge_is_field_wise() {
146 let g = parse_str("[settings]\njobs = 8\nverify = \"require\"\n", "<g>").unwrap();
147 let p = parse_str("[settings]\njobs = 12\n", "<p>").unwrap();
148 let merged = merge(&g, &p);
149 assert_eq!(merged.settings.jobs, Some(12)); assert_eq!(merged.settings.verify.as_deref(), Some("require")); }
152}
153
154#[cfg(test)]
155mod fuzz {
156 use super::*;
157 proptest::proptest! {
158 #[test]
159 fn manifest_parse_never_panics(s in ".*") { let _ = parse_str(&s, "<fuzz>"); }
160 }
161}