1extern crate alloc;
12
13use alloc::collections::BTreeMap;
14use alloc::string::{String, ToString};
15use alloc::vec::Vec;
16
17use crate::{ConfigError, Node, Origin};
18
19#[must_use]
24pub fn expand_env_vars(input: &str) -> String {
25 let mut out = String::with_capacity(input.len());
26 let chars: Vec<char> = input.chars().collect();
27 let mut i = 0;
28 while i < chars.len() {
29 if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' {
30 if let Some(end) = chars[i + 2..].iter().position(|&c| c == '}') {
31 let inner: String = chars[i + 2..i + 2 + end].iter().collect();
32 let (name, default) = match inner.split_once(":-") {
33 Some((n, d)) => (n.to_string(), Some(d.to_string())),
34 None => (inner, None),
35 };
36 let value = std::env::var(&name).ok().or(default).unwrap_or_default();
37 out.push_str(&value);
38 i += 2 + end + 1;
39 continue;
40 }
41 }
42 out.push(chars[i]);
43 i += 1;
44 }
45 out
46}
47
48#[derive(Clone)]
50struct Line {
51 indent: usize,
52 text: String,
53 number: usize,
54}
55
56pub fn parse_str(text: &str, path: &str) -> Result<Node, ConfigError> {
64 let lines = significant_lines(text, path)?;
65 if lines.is_empty() {
66 return Ok(Node::Map(BTreeMap::new()));
67 }
68 let mut at = 0usize;
69 let node = parse_block(&lines, &mut at, lines[0].indent, path, 0)?;
70 Ok(node)
71}
72
73fn significant_lines(text: &str, path: &str) -> Result<Vec<Line>, ConfigError> {
76 let mut out = Vec::new();
77 for (idx, raw) in text.lines().enumerate() {
78 let number = idx + 1;
79 if raw.contains('\t') {
80 return Err(ConfigError::Syntax {
81 path: path.to_string(),
82 line: number,
83 message: "tab in indentation; this reader takes spaces".to_string(),
84 });
85 }
86 let without_comment = strip_comment(raw);
87 let trimmed = without_comment.trim_end();
88 if trimmed.trim().is_empty() {
89 continue;
90 }
91 let body = trimmed.trim_start();
92 if let Some(unsupported) = unsupported_construct(body) {
93 return Err(ConfigError::Syntax {
94 path: path.to_string(),
95 line: number,
96 message: unsupported,
97 });
98 }
99 out.push(Line {
100 indent: trimmed.len() - body.len(),
101 text: body.to_string(),
102 number,
103 });
104 }
105 Ok(out)
106}
107
108fn strip_comment(raw: &str) -> &str {
110 let bytes = raw.as_bytes();
111 let mut in_single = false;
112 let mut in_double = false;
113 for (i, b) in bytes.iter().enumerate() {
114 match b {
115 b'\'' if !in_double => in_single = !in_single,
116 b'"' if !in_single => in_double = !in_double,
117 b'#' if !in_single && !in_double => return &raw[..i],
118 _ => {}
119 }
120 }
121 raw
122}
123
124fn unsupported_construct(body: &str) -> Option<String> {
130 let value = body.split_once(": ").map(|(_, v)| v.trim()).unwrap_or("");
131 if body.starts_with("---") || body.starts_with("...") {
132 return Some("multi-document streams are not read here".to_string());
133 }
134 if body.starts_with('&') || value.starts_with('&') {
135 return Some("anchors (`&name`) are not read here".to_string());
136 }
137 if body.starts_with('*') || value.starts_with('*') {
138 return Some("aliases (`*name`) are not read here".to_string());
139 }
140 if value.starts_with('{') && flow_map(value).is_none() {
141 return Some(
142 "only a flat map of scalars (`{k: v, k2: \"w\"}`) with distinct keys is read in \
143 flow form; use block form"
144 .to_string(),
145 );
146 }
147 if value.starts_with('[') && flow_list(value).is_none() {
148 return Some(
149 "only a flat list of scalars (`[a, \"b\"]`) is read in flow form; use block form"
150 .to_string(),
151 );
152 }
153 if value == "|" || value == ">" || value.starts_with("|-") || value.starts_with(">-") {
154 return Some("block scalars (`|`, `>`) are not read here".to_string());
155 }
156 None
157}
158
159fn parse_block(
167 lines: &[Line],
168 at: &mut usize,
169 indent: usize,
170 path: &str,
171 depth: usize,
172) -> Result<Node, ConfigError> {
173 if depth > crate::MAX_NESTING_DEPTH {
174 return Err(ConfigError::TooDeep(crate::MAX_NESTING_DEPTH));
175 }
176 if lines.get(*at).is_some_and(|l| l.text.starts_with("- ")) {
177 return parse_seq(lines, at, indent, path, depth);
178 }
179 let mut map = BTreeMap::new();
180 while let Some(line) = lines.get(*at) {
181 if line.indent < indent {
182 break;
183 }
184 if line.indent > indent {
185 return Err(ConfigError::Syntax {
186 path: path.to_string(),
187 line: line.number,
188 message: "indented further than the key it belongs to".to_string(),
189 });
190 }
191 let (key, rest) = split_key(&line.text).ok_or_else(|| ConfigError::Syntax {
192 path: path.to_string(),
193 line: line.number,
194 message: "expected `key: value` or `key:`".to_string(),
195 })?;
196 let number = line.number;
197 *at += 1;
198 if rest.is_empty() {
199 let deeper = lines.get(*at).map_or(0, |l| l.indent);
201 if deeper > indent {
202 map.insert(key, parse_block(lines, at, deeper, path, depth + 1)?);
203 } else {
204 map.insert(
205 key,
206 Node::Scalar(
207 String::new(),
208 Origin::File {
209 path: path.to_string(),
210 line: number,
211 },
212 ),
213 );
214 }
215 } else {
216 map.insert(key, value_node(rest, path, number));
217 }
218 }
219 Ok(Node::Map(map))
220}
221
222fn value_node(rest: &str, path: &str, line: usize) -> Node {
226 let origin = Origin::File {
227 path: path.to_string(),
228 line,
229 };
230 if let Some(entries) = flow_map(rest) {
231 return Node::Map(
232 entries
233 .into_iter()
234 .map(|(k, v)| {
235 (
236 k,
237 Node::Scalar(unquote(&expand_env_vars(&v)), origin.clone()),
238 )
239 })
240 .collect(),
241 );
242 }
243 match flow_list(rest) {
244 Some(items) => Node::Seq(
245 items
246 .into_iter()
247 .map(|item| Node::Scalar(unquote(&expand_env_vars(&item)), origin.clone()))
248 .collect(),
249 ),
250 None => Node::Scalar(unquote(&expand_env_vars(rest)), origin),
251 }
252}
253
254fn flow_map(value: &str) -> Option<Vec<(String, String)>> {
258 let inner = value.trim().strip_prefix('{')?.strip_suffix('}')?;
259 if inner.trim().is_empty() {
260 return Some(Vec::new());
261 }
262 let items = split_flow_items(inner)?;
263 let mut out: Vec<(String, String)> = Vec::new();
264 for item in items {
265 let (k, v) = item.split_once(':')?;
266 let key = unquote(k.trim());
267 let v = v.trim();
268 if key.is_empty() || v.is_empty() || out.iter().any(|(existing, _)| *existing == key) {
269 return None;
270 }
271 out.push((key, v.to_string()));
272 }
273 Some(out)
274}
275
276fn split_flow_items(inner: &str) -> Option<Vec<String>> {
279 let mut items = Vec::new();
280 let mut current = String::new();
281 let mut quote: Option<char> = None;
282 for c in inner.chars() {
283 match (quote, c) {
284 (Some(q), c) if c == q => {
285 quote = None;
286 current.push(c);
287 }
288 (Some(_), c) => current.push(c),
289 (None, '"' | '\'') => {
290 quote = Some(c);
291 current.push(c);
292 }
293 (None, '[' | ']' | '{' | '}') => return None,
294 (None, ',') => items.push(core::mem::take(&mut current).trim().to_string()),
295 (None, c) => current.push(c),
296 }
297 }
298 if quote.is_some() {
299 return None;
300 }
301 let last = current.trim().to_string();
302 if !last.is_empty() || !items.is_empty() {
303 items.push(last);
304 }
305 if items.iter().any(String::is_empty) {
306 return None;
307 }
308 Some(items)
309}
310
311fn flow_list(value: &str) -> Option<Vec<String>> {
314 let inner = value.trim().strip_prefix('[')?.strip_suffix(']')?;
315 if inner.trim().is_empty() {
316 return Some(Vec::new());
317 }
318 split_flow_items(inner)
319}
320
321fn parse_seq(
331 lines: &[Line],
332 at: &mut usize,
333 indent: usize,
334 path: &str,
335 depth: usize,
336) -> Result<Node, ConfigError> {
337 let mut items = Vec::new();
338 while let Some(line) = lines.get(*at) {
339 if line.indent != indent || !line.text.starts_with("- ") {
340 break;
341 }
342 let item = line.text[2..].trim();
343 let number = line.number;
344 *at += 1;
345 let mut block: Vec<Line> = Vec::new();
346 while let Some(next) = lines.get(*at) {
347 if next.indent <= indent {
348 break;
349 }
350 block.push(next.clone());
351 *at += 1;
352 }
353 if item.starts_with("- ") {
354 return Err(ConfigError::Syntax {
355 path: path.to_string(),
356 line: number,
357 message: "a sequence directly inside a sequence item is not read here".to_string(),
358 });
359 }
360 if split_key(item).is_some() {
361 let mut item_lines = Vec::with_capacity(block.len() + 1);
362 item_lines.push(Line {
363 indent: indent + 2,
364 text: item.to_string(),
365 number,
366 });
367 item_lines.extend(block);
368 let mut i = 0;
369 let node = parse_block(&item_lines, &mut i, indent + 2, path, depth + 1)?;
370 if let Some(rest) = item_lines.get(i) {
371 return Err(ConfigError::Syntax {
372 path: path.to_string(),
373 line: rest.number,
374 message: "not aligned with the keys of its sequence item".to_string(),
375 });
376 }
377 items.push(node);
378 } else {
379 if let Some(next) = block.first() {
380 return Err(ConfigError::Syntax {
381 path: path.to_string(),
382 line: next.number,
383 message: "a scalar sequence item does not continue on deeper lines".to_string(),
384 });
385 }
386 items.push(value_node(item, path, number));
387 }
388 }
389 Ok(Node::Seq(items))
390}
391
392fn split_key(text: &str) -> Option<(String, &str)> {
394 if let Some((k, v)) = text.split_once(": ") {
395 return Some((k.trim().to_string(), v.trim()));
396 }
397 let stripped = text.strip_suffix(':')?;
398 Some((stripped.trim().to_string(), ""))
399}
400
401fn unquote(value: &str) -> String {
403 let bytes = value.as_bytes();
404 if bytes.len() >= 2 {
405 let first = bytes[0];
406 let last = bytes[bytes.len() - 1];
407 if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
408 return value[1..value.len() - 1].to_string();
409 }
410 }
411 value.to_string()
412}