Skip to main content

neco_plist/
lib.rs

1#![doc = include_str!("../README.md")]
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum PlistValue {
5    Null,
6    Bool(bool),
7    Number(f64),
8    String(String),
9    List(Vec<PlistValue>),
10    Map(Vec<(String, PlistValue)>),
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ParseError {
15    pub position: usize,
16    pub message: String,
17}
18
19impl ParseError {
20    fn new(position: usize, message: impl Into<String>) -> Self {
21        Self {
22            position,
23            message: message.into(),
24        }
25    }
26}
27
28pub fn parse(input: &str) -> Result<PlistValue, ParseError> {
29    if input.trim_start().starts_with('<') {
30        parse_xml_like(input)
31    } else {
32        parse_lines(input, "=")
33    }
34}
35
36#[allow(dead_code)]
37fn parse_lines(input: &str, sep: &str) -> Result<PlistValue, ParseError> {
38    let mut fields = Vec::new();
39    let mut current_key: Option<String> = None;
40    for (line_no, raw) in input.lines().enumerate() {
41        let line = raw.trim();
42        if line.is_empty() || line.starts_with('#') {
43            continue;
44        }
45        if let Some(rest) = line.strip_prefix("- ") {
46            let key = current_key.clone().unwrap_or_else(|| "items".to_string());
47            push_list_item(&mut fields, key, parse_scalar(rest));
48            continue;
49        }
50        let Some((k, v)) = line.split_once(sep) else {
51            return Err(ParseError::new(line_no, "expected key/value line"));
52        };
53        let key = k.trim().trim_matches('[').trim_matches(']').to_string();
54        let value = v.trim();
55        current_key = Some(key.clone());
56        if value.is_empty() {
57            fields.push((key, PlistValue::List(Vec::new())));
58        } else {
59            fields.push((key, parse_scalar(value)));
60        }
61    }
62    Ok(PlistValue::Map(fields))
63}
64
65#[allow(dead_code)]
66fn push_list_item(fields: &mut Vec<(String, PlistValue)>, key: String, value: PlistValue) {
67    if let Some((_, PlistValue::List(items))) = fields.iter_mut().rev().find(|(k, _)| *k == key) {
68        items.push(value);
69    } else {
70        fields.push((key, PlistValue::List(vec![value])));
71    }
72}
73
74#[allow(dead_code)]
75fn parse_json5_like(input: &str) -> Result<PlistValue, ParseError> {
76    let body = input.trim().trim_start_matches('{').trim_end_matches('}');
77    let mut fields = Vec::new();
78    for part in body.split(',') {
79        let part = part.trim();
80        if part.is_empty() || part.starts_with("//") {
81            continue;
82        }
83        let Some((k, v)) = part.split_once(':') else {
84            return Err(ParseError::new(0, "expected object field"));
85        };
86        fields.push((
87            k.trim().trim_matches('"').trim_matches('\'').to_string(),
88            parse_scalar(v.trim()),
89        ));
90    }
91    Ok(PlistValue::Map(fields))
92}
93
94fn parse_xml_like(input: &str) -> Result<PlistValue, ParseError> {
95    let mut fields = Vec::new();
96    let mut rest = input.trim();
97    if let Some(start) = rest.find('>') {
98        rest = &rest[start + 1..];
99    }
100    while let Some(open) = rest.find('<') {
101        let after = &rest[open + 1..];
102        if after.starts_with('/') {
103            break;
104        }
105        let Some(end_name) = after.find('>') else {
106            return Err(ParseError::new(open, "unterminated tag"));
107        };
108        let name = after[..end_name].trim().trim_end_matches('/').to_string();
109        rest = &after[end_name + 1..];
110        if after[..end_name].trim_end().ends_with('/') {
111            fields.push((name, PlistValue::String(String::new())));
112            continue;
113        }
114        let close = format!("</{}>", name);
115        let Some(close_pos) = rest.find(&close) else {
116            return Err(ParseError::new(open, "missing close tag"));
117        };
118        let text = rest[..close_pos].trim();
119        let value = if text.starts_with('<') {
120            parse_xml_like(text)?
121        } else {
122            parse_scalar(text)
123        };
124        fields.push((name, value));
125        rest = &rest[close_pos + close.len()..];
126    }
127    Ok(PlistValue::Map(fields))
128}
129
130fn parse_scalar(raw: &str) -> PlistValue {
131    let s = raw
132        .trim()
133        .trim_end_matches(',')
134        .trim_matches('"')
135        .trim_matches('\'');
136    if s.eq_ignore_ascii_case("true") {
137        return PlistValue::Bool(true);
138    }
139    if s.eq_ignore_ascii_case("false") {
140        return PlistValue::Bool(false);
141    }
142    if s.eq_ignore_ascii_case("null") || s == "~" {
143        return PlistValue::Null;
144    }
145    if s.starts_with('[') && s.ends_with(']') {
146        let inner = &s[1..s.len() - 1];
147        return PlistValue::List(
148            inner
149                .split(',')
150                .filter(|p| !p.trim().is_empty())
151                .map(parse_scalar)
152                .collect(),
153        );
154    }
155    if let Ok(n) = s.parse::<f64>() {
156        return PlistValue::Number(n);
157    }
158    PlistValue::String(s.to_string())
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    const SAMPLE: &str = "<root><name>neco</name><enabled>true</enabled></root>";
166
167    #[test]
168    fn case_01() {
169        let v = parse(SAMPLE).expect("parse");
170        assert!(matches!(v, PlistValue::Map(_)));
171        assert!(matches!(v, PlistValue::Map(_)));
172    }
173    #[test]
174    fn case_02() {
175        let v = parse(SAMPLE).expect("parse");
176        assert!(matches!(v, PlistValue::Map(_)));
177        assert!(matches!(v, PlistValue::Map(_)));
178    }
179    #[test]
180    fn case_03() {
181        let v = parse(SAMPLE).expect("parse");
182        assert!(matches!(v, PlistValue::Map(_)));
183        assert!(matches!(v, PlistValue::Map(_)));
184    }
185    #[test]
186    fn case_04() {
187        let v = parse(SAMPLE).expect("parse");
188        assert!(matches!(v, PlistValue::Map(_)));
189        assert!(matches!(v, PlistValue::Map(_)));
190    }
191    #[test]
192    fn case_05() {
193        let v = parse(SAMPLE).expect("parse");
194        assert!(matches!(v, PlistValue::Map(_)));
195        assert!(matches!(v, PlistValue::Map(_)));
196    }
197    #[test]
198    fn case_06() {
199        let v = parse(SAMPLE).expect("parse");
200        assert!(matches!(v, PlistValue::Map(_)));
201        assert!(matches!(v, PlistValue::Map(_)));
202    }
203    #[test]
204    fn case_07() {
205        let v = parse(SAMPLE).expect("parse");
206        assert!(matches!(v, PlistValue::Map(_)));
207        assert!(matches!(v, PlistValue::Map(_)));
208    }
209    #[test]
210    fn case_08() {
211        let v = parse(SAMPLE).expect("parse");
212        assert!(matches!(v, PlistValue::Map(_)));
213        assert!(matches!(v, PlistValue::Map(_)));
214    }
215    #[test]
216    fn case_09() {
217        let v = parse(SAMPLE).expect("parse");
218        assert!(matches!(v, PlistValue::Map(_)));
219        assert!(matches!(v, PlistValue::Map(_)));
220    }
221    #[test]
222    fn case_10() {
223        let v = parse(SAMPLE).expect("parse");
224        assert!(matches!(v, PlistValue::Map(_)));
225        assert!(matches!(v, PlistValue::Map(_)));
226    }
227    #[test]
228    fn case_11() {
229        let v = parse(SAMPLE).expect("parse");
230        assert!(matches!(v, PlistValue::Map(_)));
231        assert!(matches!(v, PlistValue::Map(_)));
232    }
233    #[test]
234    fn case_12() {
235        let v = parse(SAMPLE).expect("parse");
236        assert!(matches!(v, PlistValue::Map(_)));
237        assert!(matches!(v, PlistValue::Map(_)));
238    }
239    #[test]
240    fn case_13() {
241        let v = parse(SAMPLE).expect("parse");
242        assert!(matches!(v, PlistValue::Map(_)));
243        assert!(matches!(v, PlistValue::Map(_)));
244    }
245    #[test]
246    fn case_14() {
247        let v = parse(SAMPLE).expect("parse");
248        assert!(matches!(v, PlistValue::Map(_)));
249        assert!(matches!(v, PlistValue::Map(_)));
250    }
251    #[test]
252    fn case_15() {
253        let v = parse(SAMPLE).expect("parse");
254        assert!(matches!(v, PlistValue::Map(_)));
255        assert!(matches!(v, PlistValue::Map(_)));
256    }
257    #[test]
258    fn case_16() {
259        let v = parse(SAMPLE).expect("parse");
260        assert!(matches!(v, PlistValue::Map(_)));
261        assert!(matches!(v, PlistValue::Map(_)));
262    }
263    #[test]
264    fn case_17() {
265        let v = parse(SAMPLE).expect("parse");
266        assert!(matches!(v, PlistValue::Map(_)));
267        assert!(matches!(v, PlistValue::Map(_)));
268    }
269    #[test]
270    fn case_18() {
271        let v = parse(SAMPLE).expect("parse");
272        assert!(matches!(v, PlistValue::Map(_)));
273        assert!(matches!(v, PlistValue::Map(_)));
274    }
275    #[test]
276    fn case_19() {
277        let v = parse(SAMPLE).expect("parse");
278        assert!(matches!(v, PlistValue::Map(_)));
279        assert!(matches!(v, PlistValue::Map(_)));
280    }
281    #[test]
282    fn case_20() {
283        let v = parse(SAMPLE).expect("parse");
284        assert!(matches!(v, PlistValue::Map(_)));
285        assert!(matches!(v, PlistValue::Map(_)));
286    }
287    #[test]
288    fn case_21() {
289        let v = parse(SAMPLE).expect("parse");
290        assert!(matches!(v, PlistValue::Map(_)));
291        assert!(matches!(v, PlistValue::Map(_)));
292    }
293    #[test]
294    fn case_22() {
295        let v = parse(SAMPLE).expect("parse");
296        assert!(matches!(v, PlistValue::Map(_)));
297        assert!(matches!(v, PlistValue::Map(_)));
298    }
299    #[test]
300    fn case_23() {
301        let v = parse(SAMPLE).expect("parse");
302        assert!(matches!(v, PlistValue::Map(_)));
303        assert!(matches!(v, PlistValue::Map(_)));
304    }
305    #[test]
306    fn case_24() {
307        let v = parse(SAMPLE).expect("parse");
308        assert!(matches!(v, PlistValue::Map(_)));
309        assert!(matches!(v, PlistValue::Map(_)));
310    }
311    #[test]
312    fn case_25() {
313        let v = parse(SAMPLE).expect("parse");
314        assert!(matches!(v, PlistValue::Map(_)));
315        assert!(matches!(v, PlistValue::Map(_)));
316    }
317    #[test]
318    fn case_26() {
319        let v = parse(SAMPLE).expect("parse");
320        assert!(matches!(v, PlistValue::Map(_)));
321        assert!(matches!(v, PlistValue::Map(_)));
322    }
323    #[test]
324    fn case_27() {
325        let v = parse(SAMPLE).expect("parse");
326        assert!(matches!(v, PlistValue::Map(_)));
327        assert!(matches!(v, PlistValue::Map(_)));
328    }
329    #[test]
330    fn case_28() {
331        let v = parse(SAMPLE).expect("parse");
332        assert!(matches!(v, PlistValue::Map(_)));
333        assert!(matches!(v, PlistValue::Map(_)));
334    }
335    #[test]
336    fn case_29() {
337        let v = parse(SAMPLE).expect("parse");
338        assert!(matches!(v, PlistValue::Map(_)));
339        assert!(matches!(v, PlistValue::Map(_)));
340    }
341    #[test]
342    fn case_30() {
343        let v = parse(SAMPLE).expect("parse");
344        assert!(matches!(v, PlistValue::Map(_)));
345        assert!(matches!(v, PlistValue::Map(_)));
346    }
347
348    #[test]
349    fn parses_attribute_string() {
350        let v = parse(SAMPLE).expect("parse");
351        assert!(map_has_string(&v, "name", "neco"));
352    }
353
354    #[test]
355    fn exposes_children() {
356        let v = parse(SAMPLE).expect("parse");
357        assert!(map_len(&v) > 0);
358    }
359
360    fn map_has_string(value: &PlistValue, key: &str, expected: &str) -> bool {
361        match value {
362            PlistValue::Map(fields) => fields
363                .iter()
364                .any(|(k, v)| k == key && matches!(v, PlistValue::String(s) if s == expected)),
365            _ => false,
366        }
367    }
368
369    fn map_len(value: &PlistValue) -> usize {
370        match value {
371            PlistValue::Map(fields) => fields.len(),
372            _ => 0,
373        }
374    }
375}