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}