normalize_manifest/
zig.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
8
9pub struct ZigZonParser;
11
12impl ManifestParser for ZigZonParser {
13 fn filename(&self) -> &'static str {
14 "build.zig.zon"
15 }
16
17 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
18 let mut name: Option<String> = None;
19 let mut version: Option<String> = None;
20 let mut deps: Vec<DeclaredDep> = Vec::new();
21
22 #[derive(PartialEq)]
28 enum State {
29 TopLevel,
30 InDeps,
31 InDepEntry,
32 }
33
34 let mut state = State::TopLevel;
35 let mut depth = 0usize;
36 let mut deps_depth = 0usize;
37 let mut dep_entry_depth = 0usize;
38 let mut current_dep_name: Option<String> = None;
39
40 for line in content.lines() {
41 let trimmed = line.trim();
42
43 if trimmed.starts_with("//") || trimmed.is_empty() {
45 continue;
46 }
47
48 let opens = trimmed.chars().filter(|&c| c == '{').count();
50 let closes = trimmed.chars().filter(|&c| c == '}').count();
51
52 if state == State::TopLevel && depth <= 1 {
54 if let Some(v) = extract_field_string(trimmed, ".name")
55 && name.is_none()
56 {
57 name = Some(v);
58 }
59 if let Some(v) = extract_field_string(trimmed, ".version")
60 && version.is_none()
61 {
62 version = Some(v);
63 }
64 }
65
66 if state == State::TopLevel
68 && trimmed.contains(".dependencies")
69 && trimmed.contains('=')
70 && opens > 0
71 {
72 state = State::InDeps;
73 deps_depth = depth + opens - closes;
74 depth = depth + opens - closes;
75 continue;
76 }
77
78 if state == State::InDeps {
80 let new_depth = depth + opens - closes;
82 if new_depth < deps_depth {
83 state = State::TopLevel;
84 depth = new_depth;
85 continue;
86 }
87
88 if opens > 0
90 && trimmed.starts_with('.')
91 && trimmed.contains('=')
92 && let Some(dep_name) = extract_zon_key(trimmed)
93 {
94 state = State::InDepEntry;
95 current_dep_name = Some(dep_name);
96 dep_entry_depth = new_depth;
97 depth = new_depth;
98 continue;
99 }
100
101 depth = new_depth;
102 continue;
103 }
104
105 if state == State::InDepEntry {
107 let new_depth = depth + opens - closes;
108 if new_depth < dep_entry_depth {
109 if let Some(dep_name) = current_dep_name.take() {
111 deps.push(DeclaredDep {
112 name: dep_name,
113 version_req: None,
114 kind: DepKind::Normal,
115 });
116 }
117 if new_depth < deps_depth {
119 state = State::TopLevel;
120 } else {
121 state = State::InDeps;
122 }
123 depth = new_depth;
124 continue;
125 }
126 depth = new_depth;
127 continue;
128 }
129
130 depth = (depth + opens).saturating_sub(closes);
131 }
132
133 if state == State::InDepEntry
135 && let Some(dep_name) = current_dep_name.take()
136 {
137 deps.push(DeclaredDep {
138 name: dep_name,
139 version_req: None,
140 kind: DepKind::Normal,
141 });
142 }
143
144 Ok(ParsedManifest {
145 ecosystem: "zig",
146 name,
147 version,
148 dependencies: deps,
149 })
150 }
151}
152
153fn extract_field_string(line: &str, field: &str) -> Option<String> {
155 let rest = line.strip_prefix(field)?.trim();
156 let rest = rest.strip_prefix('=')?.trim();
157 let rest = rest.strip_prefix('"')?;
158 let end = rest.find('"')?;
159 Some(rest[..end].to_string())
160}
161
162fn extract_zon_key(line: &str) -> Option<String> {
164 let rest = line.strip_prefix('.')?;
165 let end = rest.find(|c: char| c.is_whitespace() || c == '=')?;
167 let key = rest[..end].trim();
168 if key.is_empty() {
169 None
170 } else {
171 Some(key.to_string())
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::ManifestParser;
179
180 const SAMPLE: &str = r#".{
181 .name = "my-project",
182 .version = "0.12.0",
183 .minimum_zig_version = "0.12.0",
184 .dependencies = .{
185 .zap = .{
186 .url = "https://github.com/zigzap/zap/archive/refs/tags/v0.2.0.tar.gz",
187 .hash = "122059d35a68afb4f5e59b52fdc63be4c09ee07f72bf7c7abaab46c5ebe8c39e8f",
188 },
189 .known_folders = .{
190 .url = "https://github.com/ziglibs/known-folders/archive/fa75e1bc672952efa0cf06160bbd942b47f6d59b.tar.gz",
191 .hash = "122048992d",
192 },
193 },
194}
195"#;
196
197 #[test]
198 fn test_parse_zig_zon() {
199 let m = ZigZonParser.parse(SAMPLE).unwrap();
200 assert_eq!(m.ecosystem, "zig");
201 assert_eq!(m.name.as_deref(), Some("my-project"));
202 assert_eq!(m.version.as_deref(), Some("0.12.0"));
203
204 let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
205 assert!(names.contains(&"zap"), "{names:?}");
206 assert!(names.contains(&"known_folders"), "{names:?}");
207 assert_eq!(m.dependencies.len(), 2);
208 assert!(m.dependencies.iter().all(|d| d.version_req.is_none()));
209 assert!(m.dependencies.iter().all(|d| d.kind == DepKind::Normal));
210 }
211
212 #[test]
213 fn test_no_deps() {
214 let content = r#".{
215 .name = "simple",
216 .version = "0.1.0",
217}
218"#;
219 let m = ZigZonParser.parse(content).unwrap();
220 assert_eq!(m.name.as_deref(), Some("simple"));
221 assert_eq!(m.version.as_deref(), Some("0.1.0"));
222 assert!(m.dependencies.is_empty());
223 }
224}