closedcaption: Port from nom to winnow

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1475>
This commit is contained in:
Sebastian Dröge 2024-02-26 12:00:31 +02:00
parent f7ffa13543
commit 5df7c01cb5
7 changed files with 598 additions and 736 deletions

2
Cargo.lock generated
View file

@ -2255,7 +2255,6 @@ dependencies = [
"gstreamer-base", "gstreamer-base",
"gstreamer-check", "gstreamer-check",
"gstreamer-video", "gstreamer-video",
"nom",
"once_cell", "once_cell",
"pango", "pango",
"pangocairo", "pangocairo",
@ -2264,6 +2263,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"uuid", "uuid",
"winnow 0.6.2",
] ]
[[package]] [[package]]

View file

@ -10,7 +10,6 @@ repository.workspace = true
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
nom = "7.0"
either = "1" either = "1"
uuid = { version = "1.0", features = ["v4"] } uuid = { version = "1.0", features = ["v4"] }
chrono = "0.4.23" chrono = "0.4.23"
@ -26,6 +25,7 @@ once_cell.workspace = true
gst = { workspace = true, features = ["v1_16"]} gst = { workspace = true, features = ["v1_16"]}
gst-base = { workspace = true, features = ["v1_16"]} gst-base = { workspace = true, features = ["v1_16"]}
gst-video = { workspace = true, features = ["v1_16"]} gst-video = { workspace = true, features = ["v1_16"]}
winnow = "0.6"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1" pretty_assertions = "1"

View file

@ -139,7 +139,10 @@ fn parse_timecode_rate(
impl State { impl State {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn line(&mut self, drain: bool) -> Result<Option<MccLine>, (&[u8], nom::error::Error<&[u8]>)> { fn line(
&mut self,
drain: bool,
) -> Result<Option<MccLine>, (&[u8], winnow::error::ContextError)> {
let line = if self.replay_last_line { let line = if self.replay_last_line {
self.replay_last_line = false; self.replay_last_line = false;
&self.last_raw_line &self.last_raw_line

View file

@ -9,7 +9,7 @@
use either::Either; use either::Either;
use crate::parser_utils::{digits, digits_range, end_of_line, timecode, TimeCode}; use crate::parser_utils::{digits, digits_range, end_of_line, timecode, TimeCode};
use nom::IResult; use winnow::{error::StrContext, PResult, Parser};
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum MccLine<'a> { pub enum MccLine<'a> {
@ -37,110 +37,84 @@ pub struct MccParser {
} }
/// Parser for the MCC header /// Parser for the MCC header
fn header(s: &[u8]) -> IResult<&[u8], MccLine> { fn header<'a>(s: &mut &'a [u8]) -> PResult<MccLine<'a>> {
use nom::branch::alt; use winnow::combinator::{alt, opt};
use nom::bytes::complete::tag; use winnow::token::literal;
use nom::combinator::{map, opt};
use nom::error::context;
use nom::sequence::tuple;
context( (
"invalid header", opt(literal(&[0xEFu8, 0xBBu8, 0xBFu8][..])),
map( literal("File Format=MacCaption_MCC V"),
tuple(( alt((literal("1.0"), literal("2.0"))),
opt(tag(&[0xEFu8, 0xBBu8, 0xBFu8][..])), end_of_line,
tag("File Format=MacCaption_MCC V"), )
alt((tag("1.0"), tag("2.0"))), .map(|_| MccLine::Header)
end_of_line, .context(StrContext::Label("invalid header"))
)), .parse_next(s)
|_| MccLine::Header,
),
)(s)
} }
/// Parser for an MCC comment, i.e. a line starting with `//`. We don't return the actual comment /// Parser for an MCC comment, i.e. a line starting with `//`. We don't return the actual comment
/// text as it's irrelevant for us. /// text as it's irrelevant for us.
fn comment(s: &[u8]) -> IResult<&[u8], MccLine> { fn comment<'a>(s: &mut &'a [u8]) -> PResult<MccLine<'a>> {
use nom::bytes::complete::tag; use winnow::combinator::rest;
use nom::combinator::{map, rest}; use winnow::token::literal;
use nom::error::context;
use nom::sequence::tuple;
context( (literal("//"), rest)
"invalid comment", .map(|_| MccLine::Comment)
map(tuple((tag("//"), rest)), |_| MccLine::Comment), .context(StrContext::Label("invalid comment"))
)(s) .parse_next(s)
} }
/// Parser for the MCC UUID line. /// Parser for the MCC UUID line.
fn uuid(s: &[u8]) -> IResult<&[u8], MccLine> { fn uuid<'a>(s: &mut &'a [u8]) -> PResult<MccLine<'a>> {
use nom::bytes::complete::{tag, take_while1}; use winnow::token::{literal, take_while};
use nom::combinator::map;
use nom::error::context;
use nom::sequence::tuple;
context( (
"invalid uuid", literal("UUID="),
map( take_while(1.., |b| b != b'\n' && b != b'\r'),
tuple(( end_of_line,
tag("UUID="), )
take_while1(|b| b != b'\n' && b != b'\r'), .map(|(_, uuid, _)| MccLine::Uuid(uuid))
end_of_line, .context(StrContext::Label("invalid uuid"))
)), .parse_next(s)
|(_, uuid, _)| MccLine::Uuid(uuid),
),
)(s)
} }
/// Parser for the MCC Time Code Rate line. /// Parser for the MCC Time Code Rate line.
fn time_code_rate(s: &[u8]) -> IResult<&[u8], MccLine> { fn time_code_rate<'a>(s: &mut &'a [u8]) -> PResult<MccLine<'a>> {
use nom::bytes::complete::tag; use winnow::combinator::opt;
use nom::combinator::{map, opt}; use winnow::token::literal;
use nom::error::context;
use nom::sequence::tuple;
context( (
"invalid timecode rate", literal("Time Code Rate="),
map( digits_range(1..256),
tuple(( opt(literal("DF")),
tag("Time Code Rate="), end_of_line,
digits_range(1..256), )
opt(tag("DF")), .map(|(_, v, df, _)| MccLine::TimeCodeRate(v as u8, df.is_some()))
end_of_line, .context(StrContext::Label("invalid timecode rate"))
)), .parse_next(s)
|(_, v, df, _)| MccLine::TimeCodeRate(v as u8, df.is_some()),
),
)(s)
} }
/// Parser for generic MCC metadata lines in the form `key=value`. /// Parser for generic MCC metadata lines in the form `key=value`.
fn metadata(s: &[u8]) -> IResult<&[u8], MccLine> { fn metadata<'a>(s: &mut &'a [u8]) -> PResult<MccLine<'a>> {
use nom::bytes::complete::take_while1; use winnow::token::{one_of, take_while};
use nom::character::complete::char;
use nom::combinator::map;
use nom::error::context;
use nom::sequence::tuple;
context( (
"invalid metadata", take_while(1.., |b| b != b'='),
map( one_of('='),
tuple(( take_while(1.., |b| b != b'\n' && b != b'\r'),
take_while1(|b| b != b'='), end_of_line,
char('='), )
take_while1(|b| b != b'\n' && b != b'\r'), .map(|(name, _, value, _)| MccLine::Metadata(name, value))
end_of_line, .context(StrContext::Label("invalid metadata"))
)), .parse_next(s)
|(name, _, value, _)| MccLine::Metadata(name, value),
),
)(s)
} }
/// Parser that accepts only an empty line /// Parser that accepts only an empty line
fn empty_line(s: &[u8]) -> IResult<&[u8], MccLine> { fn empty_line<'a>(s: &mut &'a [u8]) -> PResult<MccLine<'a>> {
use nom::combinator::map; end_of_line
use nom::error::context; .map(|_| MccLine::Empty)
.context(StrContext::Label("invalid empty line"))
context("invalid empty line", map(end_of_line, |_| MccLine::Empty))(s) .parse_next(s)
} }
/// A single MCC payload item. This is ASCII hex encoded bytes plus some single-character /// A single MCC payload item. This is ASCII hex encoded bytes plus some single-character
@ -148,152 +122,145 @@ fn empty_line(s: &[u8]) -> IResult<&[u8], MccLine> {
/// ///
/// It returns an `Either` of the single hex encoded byte or the short-cut byte sequence as a /// It returns an `Either` of the single hex encoded byte or the short-cut byte sequence as a
/// static byte slice. /// static byte slice.
fn mcc_payload_item(s: &[u8]) -> IResult<&[u8], Either<u8, &'static [u8]>> { fn mcc_payload_item(s: &mut &[u8]) -> PResult<Either<u8, &'static [u8]>> {
use nom::branch::alt; use winnow::combinator::alt;
use nom::bytes::complete::tag; use winnow::stream::AsChar;
use nom::bytes::complete::take_while_m_n; use winnow::token::{literal, take_while};
use nom::character::is_hex_digit;
use nom::combinator::map;
use nom::error::context;
context( alt((
"invalid payload item", literal("G").map(|_| Either::Right([0xfa, 0x00, 0x00].as_ref())),
alt(( literal("H").map(|_| Either::Right([0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref())),
map(tag("G"), |_| Either::Right([0xfa, 0x00, 0x00].as_ref())), literal("I").map(|_| {
map(tag("H"), |_| { Either::Right([0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref())
Either::Right([0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref()) }),
}), literal("J").map(|_| {
map(tag("I"), |_| { Either::Right(
Either::Right([0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref()) [
}), 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00,
map(tag("J"), |_| { ]
Either::Right( .as_ref(),
[ )
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, }),
] literal("K").map(|_| {
.as_ref(), Either::Right(
) [
}), 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
map(tag("K"), |_| { 0x00, 0x00,
Either::Right( ]
[ .as_ref(),
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, )
0xfa, 0x00, 0x00, }),
] literal("L").map(|_| {
.as_ref(), Either::Right(
) [
}), 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
map(tag("L"), |_| { 0x00, 0x00, 0xfa, 0x00, 0x00,
Either::Right( ]
[ .as_ref(),
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, )
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, }),
] literal("M").map(|_| {
.as_ref(), Either::Right(
) [
}), 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
map(tag("M"), |_| { 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00,
Either::Right( ]
[ .as_ref(),
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, )
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, }),
] literal("N").map(|_| {
.as_ref(), Either::Right(
) [
}), 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
map(tag("N"), |_| { 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00,
Either::Right( ]
[ .as_ref(),
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, )
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, }),
] literal("O").map(|_| {
.as_ref(), Either::Right(
) [
}), 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
map(tag("O"), |_| { 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00,
Either::Right( 0x00,
[ ]
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, .as_ref(),
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, )
0xfa, 0x00, 0x00, }),
] literal("P").map(|_| Either::Right([0xfb, 0x80, 0x80].as_ref())),
.as_ref(), literal("Q").map(|_| Either::Right([0xfc, 0x80, 0x80].as_ref())),
) literal("R").map(|_| Either::Right([0xfd, 0x80, 0x80].as_ref())),
}), literal("S").map(|_| Either::Right([0x96, 0x69].as_ref())),
map(tag("P"), |_| Either::Right([0xfb, 0x80, 0x80].as_ref())), literal("T").map(|_| Either::Right([0x61, 0x01].as_ref())),
map(tag("Q"), |_| Either::Right([0xfc, 0x80, 0x80].as_ref())), literal("U").map(|_| Either::Right([0xe1, 0x00, 0x00].as_ref())),
map(tag("R"), |_| Either::Right([0xfd, 0x80, 0x80].as_ref())), literal("Z").map(|_| Either::Right([0x00].as_ref())),
map(tag("S"), |_| Either::Right([0x96, 0x69].as_ref())), take_while(2..=2, AsChar::is_hex_digit).map(|s: &[u8]| {
map(tag("T"), |_| Either::Right([0x61, 0x01].as_ref())), let hex_to_u8 = |v: u8| match v {
map(tag("U"), |_| Either::Right([0xe1, 0x00, 0x00].as_ref())), v if v.is_ascii_digit() => v - b'0',
map(tag("Z"), |_| Either::Right([0x00].as_ref())), v if (b'A'..=b'F').contains(&v) => 10 + v - b'A',
map(take_while_m_n(2, 2, is_hex_digit), |s: &[u8]| { v if (b'a'..=b'f').contains(&v) => 10 + v - b'a',
let hex_to_u8 = |v: u8| match v { _ => unreachable!(),
v if v.is_ascii_digit() => v - b'0', };
v if (b'A'..=b'F').contains(&v) => 10 + v - b'A', let val = (hex_to_u8(s[0]) << 4) | hex_to_u8(s[1]);
v if (b'a'..=b'f').contains(&v) => 10 + v - b'a', Either::Left(val)
_ => unreachable!(), }),
}; ))
let val = (hex_to_u8(s[0]) << 4) | hex_to_u8(s[1]); .context(StrContext::Label("invalid payload item"))
Either::Left(val) .parse_next(s)
}),
)),
)(s)
} }
/// Parser for the whole MCC payload with conversion to the underlying byte values. /// Parser for the whole MCC payload with conversion to the underlying byte values.
fn mcc_payload(s: &[u8]) -> IResult<&[u8], Vec<u8>> { fn mcc_payload(s: &mut &[u8]) -> PResult<Vec<u8>> {
use nom::error::context; use winnow::combinator::repeat;
use nom::multi::fold_many1;
context( repeat(1.., mcc_payload_item)
"invalid MCC payload", .fold(Vec::new, |mut acc: Vec<_>, item| {
fold_many1(mcc_payload_item, Vec::new, |mut acc: Vec<_>, item| {
match item { match item {
Either::Left(val) => acc.push(val), Either::Left(val) => acc.push(val),
Either::Right(vals) => acc.extend_from_slice(vals), Either::Right(vals) => acc.extend_from_slice(vals),
} }
acc acc
}), })
)(s) .context(StrContext::Label("invalid MCC payload"))
.parse_next(s)
} }
/// Parser for a MCC caption line in the form `timecode\tpayload`. /// Parser for a MCC caption line in the form `timecode\tpayload`.
fn caption(parse_payload: bool) -> impl FnMut(&[u8]) -> IResult<&[u8], MccLine> { fn caption<'a>(
use nom::bytes::complete::take_while; parse_payload: bool,
use nom::character::complete::{char, one_of}; ) -> impl Parser<&'a [u8], MccLine<'a>, winnow::error::ContextError> {
use nom::combinator::{map, opt}; use winnow::combinator::opt;
use nom::error::context; use winnow::token::{one_of, take_while};
use nom::sequence::tuple;
fn parse(parse_payload: bool) -> impl FnMut(&[u8]) -> IResult<&[u8], Option<Vec<u8>>> { fn parse<'a>(
move |s| { parse_payload: bool,
) -> impl Parser<&'a [u8], Option<Vec<u8>>, winnow::error::ContextError> {
move |s: &mut &'a [u8]| {
if parse_payload { if parse_payload {
map(mcc_payload, Some)(s) mcc_payload.map(Some).parse_next(s)
} else { } else {
map(take_while(|b| b != b'\n' && b != b'\r'), |_| None)(s) take_while(0.., |b| b != b'\n' && b != b'\r')
.map(|_| None)
.parse_next(s)
} }
} }
} }
move |s: &[u8]| { move |s: &mut &'a [u8]| {
context( (
"invalid MCC caption", timecode,
map( opt((
tuple(( one_of('.'),
timecode, one_of(|c| b"01".contains(&c)),
opt(tuple(( opt((one_of(','), digits)),
char('.'), )),
one_of("01"), one_of('\t'),
opt(tuple((char(','), digits))), parse(parse_payload),
))), end_of_line,
char('\t'), )
parse(parse_payload), .map(|(tc, _, _, value, _)| MccLine::Caption(tc, value))
end_of_line, .context(StrContext::Label("invalid MCC caption"))
)), .parse_next(s)
|(tc, _, _, value, _)| MccLine::Caption(tc, value),
),
)(s)
} }
} }
@ -317,60 +284,64 @@ impl MccParser {
pub fn parse_line<'a>( pub fn parse_line<'a>(
&mut self, &mut self,
line: &'a [u8], mut line: &'a [u8],
parse_payload: bool, parse_payload: bool,
) -> Result<MccLine<'a>, nom::error::Error<&'a [u8]>> { ) -> Result<MccLine<'a>, winnow::error::ContextError> {
use nom::branch::alt; use winnow::combinator::alt;
match self.state { match self.state {
State::Header => header(line) State::Header => header(&mut line)
.map(|v| { .map(|v| {
self.state = State::EmptyAfterHeader; self.state = State::EmptyAfterHeader;
v.1 v
}) })
.map_err(|err| match err { .map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(), winnow::error::ErrMode::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e, winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
}), }),
State::EmptyAfterHeader => empty_line(line) State::EmptyAfterHeader => empty_line(&mut line)
.map(|v| { .map(|v| {
self.state = State::Comments; self.state = State::Comments;
v.1 v
}) })
.map_err(|err| match err { .map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(), winnow::error::ErrMode::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e, winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
}), }),
State::Comments => alt((empty_line, comment))(line) State::Comments => alt((empty_line, comment))
.parse_next(&mut line)
.map(|v| { .map(|v| {
if v.1 == MccLine::Empty { if v == MccLine::Empty {
self.state = State::Metadata; self.state = State::Metadata;
} }
v.1 v
}) })
.map_err(|err| match err { .map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(), winnow::error::ErrMode::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e, winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
}), }),
State::Metadata => alt((empty_line, uuid, time_code_rate, metadata))(line) State::Metadata => alt((empty_line, uuid, time_code_rate, metadata))
.parse_next(&mut line)
.map(|v| { .map(|v| {
if v.1 == MccLine::Empty { if v == MccLine::Empty {
self.state = State::Captions; self.state = State::Captions;
} }
v.1 v
}) })
.map_err(|err| match err { .map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(), winnow::error::ErrMode::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e, winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
}),
State::Captions => caption(parse_payload)(line)
.map(|v| v.1)
.map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e,
}), }),
State::Captions => {
caption(parse_payload)
.parse_next(&mut line)
.map_err(|err| match err {
winnow::error::ErrMode::Incomplete(_) => unreachable!(),
winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
})
}
} }
} }
} }
@ -381,293 +352,221 @@ mod tests {
#[test] #[test]
fn test_header() { fn test_header() {
assert_eq!( let mut input = b"File Format=MacCaption_MCC V1.0".as_slice();
header(b"File Format=MacCaption_MCC V1.0".as_ref()), assert_eq!(header(&mut input), Ok(MccLine::Header));
Ok((b"".as_ref(), MccLine::Header)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"File Format=MacCaption_MCC V1.0\n".as_slice();
header(b"File Format=MacCaption_MCC V1.0\n".as_ref()), assert_eq!(header(&mut input), Ok(MccLine::Header));
Ok((b"".as_ref(), MccLine::Header)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"File Format=MacCaption_MCC V1.0\r\n".as_slice();
header(b"File Format=MacCaption_MCC V1.0\r\n".as_ref()), assert_eq!(header(&mut input), Ok(MccLine::Header));
Ok((b"".as_ref(), MccLine::Header)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"File Format=MacCaption_MCC V2.0\r\n".as_slice();
header(b"File Format=MacCaption_MCC V2.0\r\n".as_ref()), assert_eq!(header(&mut input), Ok(MccLine::Header));
Ok((b"".as_ref(), MccLine::Header)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"File Format=MacCaption_MCC V1.1".as_slice();
header(b"File Format=MacCaption_MCC V1.1".as_ref()), assert!(header(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"1.1".as_ref(),
nom::error::ErrorKind::Tag
))),
);
} }
#[test] #[test]
fn test_empty_line() { fn test_empty_line() {
assert_eq!(empty_line(b"".as_ref()), Ok((b"".as_ref(), MccLine::Empty))); let mut input = b"".as_slice();
assert_eq!(empty_line(&mut input), Ok(MccLine::Empty));
assert!(input.is_empty());
assert_eq!( let mut input = b"\n".as_slice();
empty_line(b"\n".as_ref()), assert_eq!(empty_line(&mut input), Ok(MccLine::Empty));
Ok((b"".as_ref(), MccLine::Empty)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"\r\n".as_slice();
empty_line(b"\r\n".as_ref()), assert_eq!(empty_line(&mut input), Ok(MccLine::Empty));
Ok((b"".as_ref(), MccLine::Empty)) assert!(input.is_empty());
);
assert_eq!( let mut input = b" \r\n".as_slice();
empty_line(b" \r\n".as_ref()), assert!(empty_line(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b" \r\n".as_ref(),
nom::error::ErrorKind::Eof
))),
);
} }
#[test] #[test]
fn test_comment() { fn test_comment() {
assert_eq!( let mut input = b"// blabla".as_slice();
comment(b"// blabla".as_ref()), assert_eq!(comment(&mut input), Ok(MccLine::Comment));
Ok((b"".as_ref(), MccLine::Comment)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"//\n".as_slice();
comment(b"//\n".as_ref()), assert_eq!(comment(&mut input), Ok(MccLine::Comment));
Ok((b"".as_ref(), MccLine::Comment)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"//".as_slice();
comment(b"//".as_ref()), assert_eq!(comment(&mut input), Ok(MccLine::Comment));
Ok((b"".as_ref(), MccLine::Comment)) assert!(input.is_empty());
);
assert_eq!( let mut input = b" //".as_slice();
comment(b" //".as_ref()), assert!(comment(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b" //".as_ref(),
nom::error::ErrorKind::Tag
))),
);
} }
#[test] #[test]
fn test_uuid() { fn test_uuid() {
assert_eq!( let mut input = b"UUID=1234".as_slice();
uuid(b"UUID=1234".as_ref()), assert_eq!(uuid(&mut input), Ok(MccLine::Uuid(b"1234".as_ref())));
Ok((b"".as_ref(), MccLine::Uuid(b"1234".as_ref()))) assert!(input.is_empty());
);
assert_eq!( let mut input = b"UUID=1234\n".as_slice();
uuid(b"UUID=1234\n".as_ref()), assert_eq!(uuid(&mut input), Ok(MccLine::Uuid(b"1234".as_ref())));
Ok((b"".as_ref(), MccLine::Uuid(b"1234".as_ref()))) assert!(input.is_empty());
);
assert_eq!( let mut input = b"UUID=1234\r\n".as_slice();
uuid(b"UUID=1234\r\n".as_ref()), assert_eq!(uuid(&mut input), Ok(MccLine::Uuid(b"1234".as_ref())));
Ok((b"".as_ref(), MccLine::Uuid(b"1234".as_ref()))) assert!(input.is_empty());
);
assert_eq!( let mut input = b"UUID=".as_slice();
uuid(b"UUID=".as_ref()), assert!(uuid(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"".as_ref(),
nom::error::ErrorKind::TakeWhile1
))),
);
assert_eq!( let mut input = b"uUID=1234".as_slice();
uuid(b"uUID=1234".as_ref()), assert!(uuid(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"uUID=1234".as_ref(),
nom::error::ErrorKind::Tag,
))),
);
} }
#[test] #[test]
fn test_time_code_rate() { fn test_time_code_rate() {
let mut input = b"Time Code Rate=30".as_slice();
assert_eq!( assert_eq!(
time_code_rate(b"Time Code Rate=30".as_ref()), time_code_rate(&mut input),
Ok((b"".as_ref(), MccLine::TimeCodeRate(30, false))) Ok(MccLine::TimeCodeRate(30, false))
); );
assert!(input.is_empty());
let mut input = b"Time Code Rate=30DF".as_slice();
assert_eq!( assert_eq!(
time_code_rate(b"Time Code Rate=30DF".as_ref()), time_code_rate(&mut input),
Ok((b"".as_ref(), MccLine::TimeCodeRate(30, true))) Ok(MccLine::TimeCodeRate(30, true))
); );
assert!(input.is_empty());
let mut input = b"Time Code Rate=60".as_slice();
assert_eq!( assert_eq!(
time_code_rate(b"Time Code Rate=60".as_ref()), time_code_rate(&mut input),
Ok((b"".as_ref(), MccLine::TimeCodeRate(60, false))) Ok(MccLine::TimeCodeRate(60, false))
); );
assert!(input.is_empty());
assert_eq!( let mut input = b"Time Code Rate=17F".as_slice();
time_code_rate(b"Time Code Rate=17F".as_ref()), assert!(time_code_rate(&mut input).is_err(),);
Err(nom::Err::Error(nom::error::Error::new(
b"F".as_ref(),
nom::error::ErrorKind::Eof
))),
);
assert_eq!( let mut input = b"Time Code Rate=256".as_slice();
time_code_rate(b"Time Code Rate=256".as_ref()), assert!(time_code_rate(&mut input).is_err(),);
Err(nom::Err::Error(nom::error::Error::new(
b"256".as_ref(),
nom::error::ErrorKind::Verify
))),
);
} }
#[test] #[test]
fn test_metadata() { fn test_metadata() {
let mut input = b"Creation Date=Thursday, June 04, 2015".as_slice();
assert_eq!( assert_eq!(
metadata(b"Creation Date=Thursday, June 04, 2015".as_ref()), metadata(&mut input),
Ok(( Ok(MccLine::Metadata(
b"".as_ref(), b"Creation Date".as_ref(),
MccLine::Metadata( b"Thursday, June 04, 2015".as_ref()
b"Creation Date".as_ref(),
b"Thursday, June 04, 2015".as_ref()
),
)) ))
); );
assert!(input.is_empty());
let mut input = b"Creation Date= ".as_slice();
assert_eq!( assert_eq!(
metadata(b"Creation Date= ".as_ref()), metadata(&mut input),
Ok(( Ok(MccLine::Metadata(b"Creation Date".as_ref(), b" ".as_ref()),)
b"".as_ref(),
MccLine::Metadata(b"Creation Date".as_ref(), b" ".as_ref()),
))
); );
assert!(input.is_empty());
assert_eq!( let mut input = b"Creation Date".as_slice();
metadata(b"Creation Date".as_ref()), assert!(metadata(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"".as_ref(),
nom::error::ErrorKind::Char
))),
);
assert_eq!( let mut input = b"Creation Date\n".as_slice();
metadata(b"Creation Date\n".as_ref()), assert!(metadata(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"".as_ref(),
nom::error::ErrorKind::Char,
))),
);
assert_eq!( let mut input = b"Creation Date=".as_slice();
metadata(b"Creation Date=".as_ref()), assert!(metadata(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"".as_ref(),
nom::error::ErrorKind::TakeWhile1
))),
);
assert_eq!( let mut input = b"Creation Date=\n".as_slice();
metadata(b"Creation Date=\n".as_ref()), assert!(metadata(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"\n".as_ref(),
nom::error::ErrorKind::TakeWhile1
))),
);
} }
#[test] #[test]
fn test_caption() { fn test_caption() {
let mut input = b"00:00:00:00\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_slice();
assert_eq!( assert_eq!(
caption(true)(b"00:00:00:00\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_ref()), caption(true).parse_next(&mut input),
Ok(( Ok(MccLine::Caption(
b"".as_ref(), TimeCode {
MccLine::Caption( hours: 0,
TimeCode { minutes: 0,
hours: 0, seconds: 0,
minutes: 0, frames: 0,
seconds: 0, drop_frame: false
frames: 0, },
drop_frame: false Some(vec![
}, 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0xFC,
Some(vec![ 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00,
0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00,
0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0xB4
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, ])
0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE,
0xB4
])
),
)) ))
); );
assert!(input.is_empty());
let mut input = b"00:00:00:00.0\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_slice();
assert_eq!( assert_eq!(
caption(true)(b"00:00:00:00.0\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_ref()), caption(true).parse_next(&mut input),
Ok(( Ok(MccLine::Caption(
b"".as_ref(), TimeCode {
MccLine::Caption( hours: 0,
TimeCode { minutes: 0,
hours: 0, seconds: 0,
minutes: 0, frames: 0,
seconds: 0, drop_frame: false
frames: 0, },
drop_frame: false Some(vec![
}, 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0xFC,
Some(vec![ 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00,
0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00,
0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0xB4
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, ])
0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE,
0xB4
])
),
)) ))
); );
assert!(input.is_empty());
let mut input = b"00:00:00:00.0,9\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_slice();
assert_eq!( assert_eq!(
caption(true)(b"00:00:00:00.0,9\tT52S524F67ZZ72F4QROO7391UC13FFF74ZZAEB4".as_ref()), caption(true).parse_next(&mut input),
Ok(( Ok(MccLine::Caption(
b"".as_ref(), TimeCode {
MccLine::Caption( hours: 0,
TimeCode { minutes: 0,
hours: 0, seconds: 0,
minutes: 0, frames: 0,
seconds: 0, drop_frame: false
frames: 0, },
drop_frame: false Some(vec![
}, 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0xFC,
Some(vec![ 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00,
0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00,
0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0xB4
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, ])
0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE,
0xB4
])
),
)) ))
); );
assert!(input.is_empty());
assert_eq!( let mut input = b"Creation Date=\n".as_slice();
caption(true)(b"Creation Date=\n".as_ref()), assert!(caption(true).parse_next(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"Creation Date=\n".as_ref(),
nom::error::ErrorKind::MapRes
))),
);
} }
#[test] #[test]

View file

@ -6,7 +6,7 @@
// //
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use nom::IResult; use winnow::{error::StrContext, PResult, Parser};
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct TimeCode { pub struct TimeCode {
@ -18,66 +18,62 @@ pub struct TimeCode {
} }
/// Parser for parsing a run of ASCII, decimal digits and converting them into a `u32` /// Parser for parsing a run of ASCII, decimal digits and converting them into a `u32`
pub fn digits(s: &[u8]) -> IResult<&[u8], u32> { pub fn digits(s: &mut &[u8]) -> PResult<u32> {
use nom::bytes::complete::take_while; use winnow::stream::AsChar;
use nom::character::is_digit; use winnow::token::take_while;
use nom::combinator::map_res;
map_res( take_while(0.., AsChar::is_dec_digit)
map_res(take_while(is_digit), std::str::from_utf8), .try_map(std::str::from_utf8)
|s: &str| s.parse::<u32>(), .try_map(|s: &str| s.parse::<u32>())
)(s) .parse_next(s)
} }
/// Parser for a run of decimal digits, that converts them into a `u32` and checks if the result is /// Parser for a run of decimal digits, that converts them into a `u32` and checks if the result is
/// in the allowed range. /// in the allowed range.
pub fn digits_range<R: std::ops::RangeBounds<u32>>( pub fn digits_range<'a, R: std::ops::RangeBounds<u32>>(
range: R, range: R,
) -> impl FnMut(&[u8]) -> IResult<&[u8], u32> { ) -> impl Parser<&'a [u8], u32, winnow::error::ContextError> {
use nom::combinator::verify; move |s: &mut &'a [u8]| {
use nom::error::context; digits
.verify(|v| range.contains(v))
move |s: &[u8]| context("digits out of range", verify(digits, |v| range.contains(v)))(s) .context(StrContext::Label("digits out of range"))
.parse_next(s)
}
} }
/// Parser for a timecode in the form `hh:mm:ss:fs` /// Parser for a timecode in the form `hh:mm:ss:fs`
pub fn timecode(s: &[u8]) -> IResult<&[u8], TimeCode> { pub fn timecode(s: &mut &[u8]) -> PResult<TimeCode> {
use nom::character::complete::{char, one_of}; use winnow::token::one_of;
use nom::combinator::map;
use nom::error::context;
use nom::sequence::tuple;
context( (
"invalid timecode", digits,
map( one_of(':'),
tuple(( digits_range(0..60),
digits, one_of(':'),
char(':'), digits_range(0..60),
digits_range(0..60), one_of(|c| b":.;,".contains(&c)),
char(':'), digits,
digits_range(0..60), )
one_of(":.;,"), .map(|(hours, _, minutes, _, seconds, sep, frames)| TimeCode {
digits, hours,
)), minutes,
|(hours, _, minutes, _, seconds, sep, frames)| TimeCode { seconds,
hours, frames,
minutes, drop_frame: sep == b';' || sep == b',',
seconds, })
frames, .context(StrContext::Label("invalid timecode"))
drop_frame: sep == ';' || sep == ',', .parse_next(s)
},
),
)(s)
} }
/// Parser that checks for EOF and optionally `\n` or `\r\n` before EOF /// Parser that checks for EOF and optionally `\n` or `\r\n` before EOF
pub fn end_of_line(s: &[u8]) -> IResult<&[u8], ()> { pub fn end_of_line(s: &mut &[u8]) -> PResult<()> {
use nom::branch::alt; use winnow::combinator::alt;
use nom::bytes::complete::tag; use winnow::combinator::{eof, opt};
use nom::combinator::{eof, map, opt}; use winnow::token::literal;
use nom::sequence::pair;
map(pair(opt(alt((tag("\r\n"), tag("\n")))), eof), |_| ())(s) (opt(alt((literal("\r\n"), literal("\n")))), eof)
.map(|_| ())
.parse_next(s)
} }
#[cfg(test)] #[cfg(test)]
@ -86,54 +82,46 @@ mod tests {
#[test] #[test]
fn test_timecode() { fn test_timecode() {
let mut input = b"11:12:13;14".as_slice();
assert_eq!( assert_eq!(
timecode(b"11:12:13;14".as_ref()), timecode(&mut input),
Ok(( Ok(TimeCode {
b"".as_ref(), hours: 11,
TimeCode { minutes: 12,
hours: 11, seconds: 13,
minutes: 12, frames: 14,
seconds: 13, drop_frame: true
frames: 14, })
drop_frame: true
},
))
); );
assert!(input.is_empty());
let mut input = b"11:12:13:14".as_slice();
assert_eq!( assert_eq!(
timecode(b"11:12:13:14".as_ref()), timecode(&mut input),
Ok(( Ok(TimeCode {
b"".as_ref(), hours: 11,
TimeCode { minutes: 12,
hours: 11, seconds: 13,
minutes: 12, frames: 14,
seconds: 13, drop_frame: false
frames: 14, })
drop_frame: false
},
))
); );
assert!(input.is_empty());
let mut input = b"11:12:13:14abcd".as_slice();
assert_eq!( assert_eq!(
timecode(b"11:12:13:14abcd".as_ref()), timecode(&mut input),
Ok(( Ok(TimeCode {
b"abcd".as_ref(), hours: 11,
TimeCode { minutes: 12,
hours: 11, seconds: 13,
minutes: 12, frames: 14,
seconds: 13, drop_frame: false
frames: 14, })
drop_frame: false
},
))
); );
assert_eq!(input, b"abcd");
assert_eq!( let mut input = b"abcd11:12:13:14".as_slice();
timecode(b"abcd11:12:13:14".as_ref()), assert!(timecode(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"abcd11:12:13:14".as_ref(),
nom::error::ErrorKind::MapRes
))),
);
} }
} }

View file

@ -123,7 +123,10 @@ fn parse_timecode(
impl State { impl State {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn line(&mut self, drain: bool) -> Result<Option<SccLine>, (&[u8], nom::error::Error<&[u8]>)> { fn line(
&mut self,
drain: bool,
) -> Result<Option<SccLine>, (&[u8], winnow::error::ContextError)> {
let line = match self.reader.line_with_drain(drain) { let line = match self.reader.line_with_drain(drain) {
None => { None => {
return Ok(None); return Ok(None);

View file

@ -8,7 +8,7 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::parser_utils::{end_of_line, timecode, TimeCode}; use crate::parser_utils::{end_of_line, timecode, TimeCode};
use nom::IResult; use winnow::{error::StrContext, PResult, Parser};
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum SccLine { pub enum SccLine {
@ -30,44 +30,36 @@ pub struct SccParser {
} }
/// Parser for the SCC header /// Parser for the SCC header
fn header(s: &[u8]) -> IResult<&[u8], SccLine> { fn header(s: &mut &[u8]) -> PResult<SccLine> {
use nom::bytes::complete::tag; use winnow::combinator::opt;
use nom::combinator::{map, opt}; use winnow::token::literal;
use nom::error::context;
use nom::sequence::tuple;
context( (
"invalid header", opt(literal(&[0xEFu8, 0xBBu8, 0xBFu8][..])),
map( literal("Scenarist_SCC V1.0"),
tuple(( end_of_line,
opt(tag(&[0xEFu8, 0xBBu8, 0xBFu8][..])), )
tag("Scenarist_SCC V1.0"), .map(|_| SccLine::Header)
end_of_line, .context(StrContext::Label("invalid header"))
)), .parse_next(s)
|_| SccLine::Header,
),
)(s)
} }
/// Parser that accepts only an empty line /// Parser that accepts only an empty line
fn empty_line(s: &[u8]) -> IResult<&[u8], SccLine> { fn empty_line(s: &mut &[u8]) -> PResult<SccLine> {
use nom::combinator::map; end_of_line
use nom::error::context; .map(|_| SccLine::Empty)
.context(StrContext::Label("invalid empty line"))
context("invalid empty line", map(end_of_line, |_| SccLine::Empty))(s) .parse_next(s)
} }
/// A single SCC payload item. This is ASCII hex encoded bytes. /// A single SCC payload item. This is ASCII hex encoded bytes.
/// It returns an tuple of `(u8, u8)` of the hex encoded bytes. /// It returns an tuple of `(u8, u8)` of the hex encoded bytes.
fn scc_payload_item(s: &[u8]) -> IResult<&[u8], (u8, u8)> { fn scc_payload_item(s: &mut &[u8]) -> PResult<(u8, u8)> {
use nom::bytes::complete::take_while_m_n; use winnow::stream::AsChar;
use nom::character::is_hex_digit; use winnow::token::take_while;
use nom::combinator::map;
use nom::error::context;
context( take_while(4..=4, AsChar::is_hex_digit)
"invalid SCC payload item", .map(|s: &[u8]| {
map(take_while_m_n(4, 4, is_hex_digit), |s: &[u8]| {
let hex_to_u8 = |v: u8| match v { let hex_to_u8 = |v: u8| match v {
v if v.is_ascii_digit() => v - b'0', v if v.is_ascii_digit() => v - b'0',
v if (b'A'..=b'F').contains(&v) => 10 + v - b'A', v if (b'A'..=b'F').contains(&v) => 10 + v - b'A',
@ -79,50 +71,47 @@ fn scc_payload_item(s: &[u8]) -> IResult<&[u8], (u8, u8)> {
let val2 = (hex_to_u8(s[2]) << 4) | hex_to_u8(s[3]); let val2 = (hex_to_u8(s[2]) << 4) | hex_to_u8(s[3]);
(val1, val2) (val1, val2)
}), })
)(s) .context(StrContext::Label("invalid SCC payload item"))
.parse_next(s)
} }
/// Parser for the whole SCC payload with conversion to the underlying byte values. /// Parser for the whole SCC payload with conversion to the underlying byte values.
fn scc_payload(s: &[u8]) -> IResult<&[u8], Vec<u8>> { fn scc_payload(s: &mut &[u8]) -> PResult<Vec<u8>> {
use nom::branch::alt; use winnow::combinator::{alt, repeat};
use nom::bytes::complete::tag; use winnow::token::literal;
use nom::combinator::map;
use nom::error::context;
use nom::multi::fold_many1;
use nom::sequence::pair;
use nom::Parser;
let parse_item = map( let parse_item = (
pair(scc_payload_item, alt((tag(" ").map(|_| ()), end_of_line))), scc_payload_item,
|(item, _)| item, alt((literal(" ").map(|_| ()), end_of_line)),
); )
.map(|(item, _)| item);
context( repeat(1.., parse_item)
"invalid SCC payload", .fold(Vec::new, |mut acc: Vec<_>, item| {
fold_many1(parse_item, Vec::new, |mut acc: Vec<_>, item| {
acc.push(item.0); acc.push(item.0);
acc.push(item.1); acc.push(item.1);
acc acc
}), })
)(s) .context(StrContext::Label("invalid SCC payload"))
.parse_next(s)
} }
/// Parser for a SCC caption line in the form `timecode\tpayload`. /// Parser for a SCC caption line in the form `timecode\tpayload`.
fn caption(s: &[u8]) -> IResult<&[u8], SccLine> { fn caption(s: &mut &[u8]) -> PResult<SccLine> {
use nom::bytes::complete::tag; use winnow::ascii::multispace0;
use nom::character::complete::multispace0; use winnow::token::literal;
use nom::combinator::map;
use nom::error::context;
use nom::sequence::tuple;
context( (
"invalid SCC caption line", timecode,
map( literal("\t"),
tuple((timecode, tag("\t"), multispace0, scc_payload, end_of_line)), multispace0,
|(tc, _, _, value, _)| SccLine::Caption(tc, value), scc_payload,
), end_of_line,
)(s) )
.map(|(tc, _, _, value, _)| SccLine::Caption(tc, value))
.context(StrContext::Label("invalid SCC caption line"))
.parse_next(s)
} }
/// SCC parser the parses line-by-line and keeps track of the current state in the file. /// SCC parser the parses line-by-line and keeps track of the current state in the file.
@ -143,37 +132,36 @@ impl SccParser {
self.state = State::Header; self.state = State::Header;
} }
pub fn parse_line<'a>( pub fn parse_line(&mut self, mut line: &[u8]) -> Result<SccLine, winnow::error::ContextError> {
&mut self, use winnow::combinator::alt;
line: &'a [u8],
) -> Result<SccLine, nom::error::Error<&'a [u8]>> {
use nom::branch::alt;
match self.state { match self.state {
State::Header => header(line) State::Header => header(&mut line)
.map(|v| { .map(|v| {
self.state = State::Empty; self.state = State::Empty;
v.1 v
}) })
.map_err(|err| match err { .map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(), winnow::error::ErrMode::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e, winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
}), }),
State::Empty => empty_line(line) State::Empty => empty_line(&mut line)
.map(|v| { .map(|v| {
self.state = State::CaptionOrEmpty; self.state = State::CaptionOrEmpty;
v.1 v
}) })
.map_err(|err| match err { .map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(), winnow::error::ErrMode::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e, winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
}),
State::CaptionOrEmpty => alt((caption, empty_line))(line)
.map(|v| v.1)
.map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e,
}), }),
State::CaptionOrEmpty => {
alt((caption, empty_line))
.parse_next(&mut line)
.map_err(|err| match err {
winnow::error::ErrMode::Incomplete(_) => unreachable!(),
winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
})
}
} }
} }
} }
@ -184,149 +172,130 @@ mod tests {
#[test] #[test]
fn test_header() { fn test_header() {
assert_eq!( let mut input = b"Scenarist_SCC V1.0".as_slice();
header(b"Scenarist_SCC V1.0".as_ref()), assert_eq!(header(&mut input), Ok(SccLine::Header));
Ok((b"".as_ref(), SccLine::Header,)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"Scenarist_SCC V1.0\n".as_slice();
header(b"Scenarist_SCC V1.0\n".as_ref()), assert_eq!(header(&mut input), Ok(SccLine::Header));
Ok((b"".as_ref(), SccLine::Header)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"Scenarist_SCC V1.0\r\n".as_slice();
header(b"Scenarist_SCC V1.0\r\n".as_ref()), assert_eq!(header(&mut input), Ok(SccLine::Header));
Ok((b"".as_ref(), SccLine::Header)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"Scenarist_SCC V1.1".as_slice();
header(b"Scenarist_SCC V1.1".as_ref()), assert!(header(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b"Scenarist_SCC V1.1".as_ref(),
nom::error::ErrorKind::Tag
))),
);
} }
#[test] #[test]
fn test_empty_line() { fn test_empty_line() {
assert_eq!(empty_line(b"".as_ref()), Ok((b"".as_ref(), SccLine::Empty))); let mut input = b"".as_slice();
assert_eq!(empty_line(&mut input), Ok(SccLine::Empty));
assert!(input.is_empty());
assert_eq!( let mut input = b"\n".as_slice();
empty_line(b"\n".as_ref()), assert_eq!(empty_line(&mut input), Ok(SccLine::Empty));
Ok((b"".as_ref(), SccLine::Empty)) assert!(input.is_empty());
);
assert_eq!( let mut input = b"\r\n".as_slice();
empty_line(b"\r\n".as_ref()), assert_eq!(empty_line(&mut input), Ok(SccLine::Empty));
Ok((b"".as_ref(), SccLine::Empty)) assert!(input.is_empty());
);
assert_eq!( let mut input = b" \r\n".as_slice();
empty_line(b" \r\n".as_ref()), assert!(empty_line(&mut input).is_err());
Err(nom::Err::Error(nom::error::Error::new(
b" \r\n".as_ref(),
nom::error::ErrorKind::Eof
))),
);
} }
#[test] #[test]
fn test_caption() { fn test_caption() {
let mut input = b"01:02:53:14\t94ae 94ae 9420 9420 947a 947a 97a2 97a2 a820 68ef f26e 2068 ef6e 6be9 6e67 2029 942c 942c 8080 8080 942f 942f".as_slice();
assert_eq!( assert_eq!(
caption(b"01:02:53:14\t94ae 94ae 9420 9420 947a 947a 97a2 97a2 a820 68ef f26e 2068 ef6e 6be9 6e67 2029 942c 942c 8080 8080 942f 942f".as_ref()), caption(&mut input),
Ok(( Ok(SccLine::Caption(
b"".as_ref(), TimeCode {
SccLine::Caption( hours: 1,
TimeCode { minutes: 2,
hours: 1, seconds: 53,
minutes: 2, frames: 14,
seconds: 53, drop_frame: false
frames: 14, },
drop_frame: false vec![
}, 0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0x7a, 0x94, 0x7a, 0x97,
0xa2, 0x97, 0xa2, 0xa8, 0x20, 0x68, 0xef, 0xf2, 0x6e, 0x20, 0x68, 0xef, 0x6e,
vec![ 0x6b, 0xe9, 0x6e, 0x67, 0x20, 0x29, 0x94, 0x2c, 0x94, 0x2c, 0x80, 0x80, 0x80,
0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0x7a, 0x94, 0x7a, 0x97, 0xa2, 0x80, 0x94, 0x2f, 0x94, 0x2f,
0x97, 0xa2, 0xa8, 0x20, 0x68, 0xef, 0xf2, 0x6e, 0x20, 0x68, 0xef, 0x6e, 0x6b, 0xe9, ]
0x6e, 0x67, 0x20, 0x29, 0x94, 0x2c, 0x94, 0x2c, 0x80, 0x80, 0x80, 0x80, 0x94, 0x2f,
0x94, 0x2f,
]
),
)) ))
); );
assert!(input.is_empty());
let mut input = b"01:02:55;14 942c 942c".as_slice();
assert_eq!( assert_eq!(
caption(b"01:02:55;14 942c 942c".as_ref()), caption(&mut input),
Ok(( Ok(SccLine::Caption(
b"".as_ref(), TimeCode {
SccLine::Caption( hours: 1,
TimeCode { minutes: 2,
hours: 1, seconds: 55,
minutes: 2, frames: 14,
seconds: 55, drop_frame: true
frames: 14, },
drop_frame: true vec![0x94, 0x2c, 0x94, 0x2c]
},
vec![0x94, 0x2c, 0x94, 0x2c]
),
)) ))
); );
assert!(input.is_empty());
let mut input = b"01:03:27:29 94ae 94ae 9420 9420 94f2 94f2 c845 d92c 2054 c845 5245 ae80 942c 942c 8080 8080 942f 942f".as_slice();
assert_eq!( assert_eq!(
caption(b"01:03:27:29 94ae 94ae 9420 9420 94f2 94f2 c845 d92c 2054 c845 5245 ae80 942c 942c 8080 8080 942f 942f".as_ref()), caption(&mut input),
Ok(( Ok(SccLine::Caption(
b"".as_ref(), TimeCode {
SccLine::Caption( hours: 1,
TimeCode { minutes: 3,
hours: 1, seconds: 27,
minutes: 3, frames: 29,
seconds: 27, drop_frame: false
frames: 29, },
drop_frame: false vec![
}, 0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0xf2, 0x94, 0xf2, 0xc8,
vec![ 0x45, 0xd9, 0x2c, 0x20, 0x54, 0xc8, 0x45, 0x52, 0x45, 0xae, 0x80, 0x94, 0x2c,
0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0xf2, 0x94, 0xf2, 0xc8, 0x45, 0x94, 0x2c, 0x80, 0x80, 0x80, 0x80, 0x94, 0x2f, 0x94, 0x2f,
0xd9, 0x2c, 0x20, 0x54, 0xc8, 0x45, 0x52, 0x45, 0xae, 0x80, 0x94, 0x2c, 0x94, 0x2c, ]
0x80, 0x80, 0x80, 0x80, 0x94, 0x2f, 0x94, 0x2f,
]
),
)) ))
); );
assert!(input.is_empty());
let mut input = b"00:00:00;00\t942c 942c".as_slice();
assert_eq!( assert_eq!(
caption(b"00:00:00;00\t942c 942c".as_ref()), caption(&mut input),
Ok(( Ok(SccLine::Caption(
b"".as_ref(), TimeCode {
SccLine::Caption( hours: 0,
TimeCode { minutes: 0,
hours: 0, seconds: 00,
minutes: 0, frames: 00,
seconds: 00, drop_frame: true,
frames: 00, },
drop_frame: true, vec![0x94, 0x2c, 0x94, 0x2c],
},
vec![0x94, 0x2c, 0x94, 0x2c],
),
)) ))
); );
assert!(input.is_empty());
let mut input = b"00:00:00;00\t942c 942c\r\n".as_slice();
assert_eq!( assert_eq!(
caption(b"00:00:00;00\t942c 942c\r\n".as_ref()), caption(&mut input),
Ok(( Ok(SccLine::Caption(
b"".as_ref(), TimeCode {
SccLine::Caption( hours: 0,
TimeCode { minutes: 0,
hours: 0, seconds: 00,
minutes: 0, frames: 00,
seconds: 00, drop_frame: true,
frames: 00, },
drop_frame: true, vec![0x94, 0x2c, 0x94, 0x2c],
},
vec![0x94, 0x2c, 0x94, 0x2c],
),
)) ))
); );
assert!(input.is_empty());
} }
#[test] #[test]