Skip to main content

ffmetadata/
lib.rs

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}