ffmetadata 0.1.2

A simple parser for ffmpeg's ffmetadata format
Documentation
use std::fmt;
use std::borrow::Cow;

use nom::IResult;
use nom::Parser;
use nom::bytes::complete::{tag, take_till1, take_until};
use nom::character::complete::{char, one_of, none_of};
use nom::branch::alt;
use nom::sequence::{preceded, delimited};
use nom::multi::{many0, fold_many0};
use nom::combinator::opt;

mod error;
#[cfg(test)]
mod test;

type KV = (String, String);

#[derive(Debug, Default)]
pub struct FFMetadata {
  pub global: Vec<KV>,
  pub sections: Vec<(String, Vec<KV>)>,
}

impl FFMetadata {
  pub fn parse(s: &str) -> Result<Self, error::ParseError<'_>> {
    let (remaining, r) = ffmetadata(s)?;
    if !remaining.is_empty() {
      Err(error::ParseError::Remaining(remaining))
    } else {
      Ok(r)
    }
  }
}

impl fmt::Display for FFMetadata {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    writeln!(f, ";FFMETADATA1")?;

    for (k, v) in &self.global {
      writeln!(f, "{}={}", escape(k), escape(v))?;
    }
    writeln!(f)?;

    for (header, section) in &self.sections {
      writeln!(f, "[{header}]")?;
      for (k, v) in section {
        writeln!(f, "{}={}", escape(k), escape(v))?;
      }
      writeln!(f)?;
    }

    Ok(())
  }
}

fn header(input: &str) -> IResult<&str, ()> {
  tag(";FFMETADATA1\n").map(|_| ()).parse(input)
}

fn string(input: &str) -> IResult<&str, String> {
  fold_many0(
    alt((
      preceded(char('\\'), one_of("=;#\\\n")),
      none_of("=;#\\\n"),
    )),
    String::new,
    |mut acc: String, c: char| {
        acc.push(c);
        acc
    }
  ).parse(input)
}

fn kv(input: &str) -> IResult<&str, KV> {
  let (input, key) = string(input)?;
  let (input, _) = char('=')(input)?;
  let (input, value) = string(input)?;
  let (input, _) = char('\n')(input)?;
  Ok((input, (key, value)))
}

fn section_header(input: &str) -> IResult<&str, String> {
  delimited(char('['), take_till1(|c| c == ']'), tag("]\n"))
    .map(String::from).parse(input)
}

fn comment(input: &str) -> IResult<&str, ()> {
  let (input, _) = opt(preceded(
    one_of(";#"),
    take_until("\n")
  )).parse(input)?;
  let (input, _) = char('\n')(input)?;
  Ok((input, ()))
}

fn comment_or_kv(input: &str) -> IResult<&str, Option<KV>> {
  alt((
    comment.map(|_| None),
    kv.map(Some),
  ))(input)
}

fn kvs(input: &str) -> IResult<&str, Vec<KV>> {
  fold_many0(comment_or_kv, Vec::new, |mut acc: Vec<_>, item| {
    if let Some(kv) = item {
      acc.push(kv);
    }
    acc
  })(input)
}

fn section(input: &str) -> IResult<&str, (String, Vec<KV>)> {
  let (input, header) = section_header(input)?;
  let (input, kvs) = kvs(input)?;
  Ok((input, (header, kvs)))
}

fn ffmetadata(input: &str) -> IResult<&str, FFMetadata> {
  let (input, _) = header(input)?;
  let (input, global) = kvs(input)?;
  let (input, sections) = many0(section)(input)?;
  Ok((input, FFMetadata {
    global, sections,
  }))
}

const ESCAPING_CHARS: &[char] = &['=', ';', '#', '\\', '\n'];

fn escape(s: &str) -> Cow<str> {
  if s.contains(ESCAPING_CHARS) {
    let escaped = s.chars()
      .fold(String::new(), |mut s, ch| {
        if ESCAPING_CHARS.contains(&ch) {
          s.push('\\');
        }
        s.push(ch);
        s
      });
    Cow::Owned(escaped)
  } else {
    Cow::Borrowed(s)
  }
}