1use std::fmt;
2use std::borrow::Cow;
3
4use nom::IResult;
5use nom::Parser;
6use nom::bytes::complete::{tag, take_till1, take_until};
7use nom::character::complete::{char, one_of, none_of};
8use nom::branch::alt;
9use nom::sequence::{preceded, delimited};
10use nom::multi::{many0, fold_many0};
11use nom::combinator::opt;
12
13mod error;
14#[cfg(test)]
15mod test;
16
17type KV = (String, String);
18
19#[derive(Debug, Default)]
20pub struct FFMetadata {
21 pub global: Vec<KV>,
22 pub sections: Vec<(String, Vec<KV>)>,
23}
24
25impl FFMetadata {
26 pub fn parse(s: &str) -> Result<Self, error::ParseError<'_>> {
27 let (remaining, r) = ffmetadata(s)?;
28 if !remaining.is_empty() {
29 Err(error::ParseError::Remaining(remaining))
30 } else {
31 Ok(r)
32 }
33 }
34}
35
36impl fmt::Display for FFMetadata {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 writeln!(f, ";FFMETADATA1")?;
39
40 for (k, v) in &self.global {
41 writeln!(f, "{}={}", escape(k), escape(v))?;
42 }
43 writeln!(f)?;
44
45 for (header, section) in &self.sections {
46 writeln!(f, "[{header}]")?;
47 for (k, v) in section {
48 writeln!(f, "{}={}", escape(k), escape(v))?;
49 }
50 writeln!(f)?;
51 }
52
53 Ok(())
54 }
55}
56
57fn header(input: &str) -> IResult<&str, ()> {
58 tag(";FFMETADATA1\n").map(|_| ()).parse(input)
59}
60
61fn string(input: &str) -> IResult<&str, String> {
62 fold_many0(
63 alt((
64 preceded(char('\\'), one_of("=;#\\\n")),
65 none_of("=;#\\\n"),
66 )),
67 String::new,
68 |mut acc: String, c: char| {
69 acc.push(c);
70 acc
71 }
72 ).parse(input)
73}
74
75fn kv(input: &str) -> IResult<&str, KV> {
76 let (input, key) = string(input)?;
77 let (input, _) = char('=')(input)?;
78 let (input, value) = string(input)?;
79 let (input, _) = char('\n')(input)?;
80 Ok((input, (key, value)))
81}
82
83fn section_header(input: &str) -> IResult<&str, String> {
84 delimited(char('['), take_till1(|c| c == ']'), tag("]\n"))
85 .map(String::from).parse(input)
86}
87
88fn comment(input: &str) -> IResult<&str, ()> {
89 let (input, _) = opt(preceded(
90 one_of(";#"),
91 take_until("\n")
92 )).parse(input)?;
93 let (input, _) = char('\n')(input)?;
94 Ok((input, ()))
95}
96
97fn comment_or_kv(input: &str) -> IResult<&str, Option<KV>> {
98 alt((
99 comment.map(|_| None),
100 kv.map(Some),
101 ))(input)
102}
103
104fn kvs(input: &str) -> IResult<&str, Vec<KV>> {
105 fold_many0(comment_or_kv, Vec::new, |mut acc: Vec<_>, item| {
106 if let Some(kv) = item {
107 acc.push(kv);
108 }
109 acc
110 })(input)
111}
112
113fn section(input: &str) -> IResult<&str, (String, Vec<KV>)> {
114 let (input, header) = section_header(input)?;
115 let (input, kvs) = kvs(input)?;
116 Ok((input, (header, kvs)))
117}
118
119fn ffmetadata(input: &str) -> IResult<&str, FFMetadata> {
120 let (input, _) = header(input)?;
121 let (input, global) = kvs(input)?;
122 let (input, sections) = many0(section)(input)?;
123 Ok((input, FFMetadata {
124 global, sections,
125 }))
126}
127
128const ESCAPING_CHARS: &[char] = &['=', ';', '#', '\\', '\n'];
129
130fn escape(s: &str) -> Cow<str> {
131 if s.contains(ESCAPING_CHARS) {
132 let escaped = s.chars()
133 .fold(String::new(), |mut s, ch| {
134 if ESCAPING_CHARS.contains(&ch) {
135 s.push('\\');
136 }
137 s.push(ch);
138 s
139 });
140 Cow::Owned(escaped)
141 } else {
142 Cow::Borrowed(s)
143 }
144}