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][..])),
tag("File Format=MacCaption_MCC V"),
alt((tag("1.0"), tag("2.0"))),
end_of_line, end_of_line,
)), )
|_| MccLine::Header, .map(|_| MccLine::Header)
), .context(StrContext::Label("invalid header"))
)(s) .parse_next(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((
tag("UUID="),
take_while1(|b| b != b'\n' && b != b'\r'),
end_of_line, end_of_line,
)), )
|(_, uuid, _)| MccLine::Uuid(uuid), .map(|(_, uuid, _)| MccLine::Uuid(uuid))
), .context(StrContext::Label("invalid uuid"))
)(s) .parse_next(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(
tuple((
tag("Time Code Rate="),
digits_range(1..256), digits_range(1..256),
opt(tag("DF")), opt(literal("DF")),
end_of_line, end_of_line,
)), )
|(_, v, df, _)| MccLine::TimeCodeRate(v as u8, df.is_some()), .map(|(_, v, df, _)| MccLine::TimeCodeRate(v as u8, df.is_some()))
), .context(StrContext::Label("invalid timecode rate"))
)(s) .parse_next(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'='),
char('='),
take_while1(|b| b != b'\n' && b != b'\r'),
end_of_line, end_of_line,
)), )
|(name, _, value, _)| MccLine::Metadata(name, value), .map(|(name, _, value, _)| MccLine::Metadata(name, value))
), .context(StrContext::Label("invalid metadata"))
)(s) .parse_next(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,25 +122,18 @@ 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(
"invalid payload item",
alt(( alt((
map(tag("G"), |_| Either::Right([0xfa, 0x00, 0x00].as_ref())), literal("G").map(|_| Either::Right([0xfa, 0x00, 0x00].as_ref())),
map(tag("H"), |_| { literal("H").map(|_| Either::Right([0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref())),
Either::Right([0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref()) literal("I").map(|_| {
}),
map(tag("I"), |_| {
Either::Right([0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref()) Either::Right([0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00].as_ref())
}), }),
map(tag("J"), |_| { literal("J").map(|_| {
Either::Right( Either::Right(
[ [
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00,
@ -174,60 +141,60 @@ fn mcc_payload_item(s: &[u8]) -> IResult<&[u8], Either<u8, &'static [u8]>> {
.as_ref(), .as_ref(),
) )
}), }),
map(tag("K"), |_| { literal("K").map(|_| {
Either::Right( Either::Right(
[ [
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa,
0xfa, 0x00, 0x00, 0x00, 0x00,
] ]
.as_ref(), .as_ref(),
) )
}), }),
map(tag("L"), |_| { literal("L").map(|_| {
Either::Right( Either::Right(
[ [
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 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, 0x00, 0x00, 0xfa, 0x00, 0x00,
] ]
.as_ref(), .as_ref(),
) )
}), }),
map(tag("M"), |_| { literal("M").map(|_| {
Either::Right( Either::Right(
[ [
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 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, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00,
] ]
.as_ref(), .as_ref(),
) )
}), }),
map(tag("N"), |_| { literal("N").map(|_| {
Either::Right( Either::Right(
[ [
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 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,
] ]
.as_ref(), .as_ref(),
) )
}), }),
map(tag("O"), |_| { literal("O").map(|_| {
Either::Right( Either::Right(
[ [
0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 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, 0x00,
] ]
.as_ref(), .as_ref(),
) )
}), }),
map(tag("P"), |_| Either::Right([0xfb, 0x80, 0x80].as_ref())), literal("P").map(|_| Either::Right([0xfb, 0x80, 0x80].as_ref())),
map(tag("Q"), |_| Either::Right([0xfc, 0x80, 0x80].as_ref())), literal("Q").map(|_| Either::Right([0xfc, 0x80, 0x80].as_ref())),
map(tag("R"), |_| Either::Right([0xfd, 0x80, 0x80].as_ref())), literal("R").map(|_| Either::Right([0xfd, 0x80, 0x80].as_ref())),
map(tag("S"), |_| Either::Right([0x96, 0x69].as_ref())), literal("S").map(|_| Either::Right([0x96, 0x69].as_ref())),
map(tag("T"), |_| Either::Right([0x61, 0x01].as_ref())), literal("T").map(|_| Either::Right([0x61, 0x01].as_ref())),
map(tag("U"), |_| Either::Right([0xe1, 0x00, 0x00].as_ref())), literal("U").map(|_| Either::Right([0xe1, 0x00, 0x00].as_ref())),
map(tag("Z"), |_| Either::Right([0x00].as_ref())), literal("Z").map(|_| Either::Right([0x00].as_ref())),
map(take_while_m_n(2, 2, is_hex_digit), |s: &[u8]| { take_while(2..=2, AsChar::is_hex_digit).map(|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',
@ -237,63 +204,63 @@ fn mcc_payload_item(s: &[u8]) -> IResult<&[u8], Either<u8, &'static [u8]>> {
let val = (hex_to_u8(s[0]) << 4) | hex_to_u8(s[1]); let val = (hex_to_u8(s[0]) << 4) | hex_to_u8(s[1]);
Either::Left(val) Either::Left(val)
}), }),
)), ))
)(s) .context(StrContext::Label("invalid payload item"))
.parse_next(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",
map(
tuple((
timecode, timecode,
opt(tuple(( opt((
char('.'), one_of('.'),
one_of("01"), one_of(|c| b"01".contains(&c)),
opt(tuple((char(','), digits))), opt((one_of(','), digits)),
))), )),
char('\t'), one_of('\t'),
parse(parse_payload), parse(parse_payload),
end_of_line, end_of_line,
)), )
|(tc, _, _, value, _)| MccLine::Caption(tc, value), .map(|(tc, _, _, value, _)| MccLine::Caption(tc, value))
), .context(StrContext::Label("invalid MCC caption"))
)(s) .parse_next(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) State::Captions => {
.map(|v| v.1) caption(parse_payload)
.parse_next(&mut line)
.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,
}), })
}
} }
} }
} }
@ -381,213 +352,151 @@ 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(),
MccLine::Metadata(
b"Creation Date".as_ref(), b"Creation Date".as_ref(),
b"Thursday, June 04, 2015".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(),
MccLine::Caption(
TimeCode { TimeCode {
hours: 0, hours: 0,
minutes: 0, minutes: 0,
@ -596,24 +505,22 @@ mod tests {
drop_frame: false drop_frame: false
}, },
Some(vec![ Some(vec![
0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0xFC,
0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x80, 0x80, 0xFD, 0x80, 0x80, 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, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 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,
0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0xB4
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(),
MccLine::Caption(
TimeCode { TimeCode {
hours: 0, hours: 0,
minutes: 0, minutes: 0,
@ -622,24 +529,22 @@ mod tests {
drop_frame: false drop_frame: false
}, },
Some(vec![ Some(vec![
0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0xFC,
0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x80, 0x80, 0xFD, 0x80, 0x80, 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, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 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,
0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0xB4
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(),
MccLine::Caption(
TimeCode { TimeCode {
hours: 0, hours: 0,
minutes: 0, minutes: 0,
@ -648,26 +553,20 @@ mod tests {
drop_frame: false drop_frame: false
}, },
Some(vec![ Some(vec![
0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0x61, 0x01, 0x52, 0x96, 0x69, 0x52, 0x4F, 0x67, 0x00, 0x00, 0x72, 0xF4, 0xFC,
0xFC, 0x80, 0x80, 0xFD, 0x80, 0x80, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x80, 0x80, 0xFD, 0x80, 0x80, 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, 0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00,
0xFA, 0x00, 0x00, 0xFA, 0x00, 0x00, 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,
0x73, 0x91, 0xE1, 0x00, 0x00, 0xC1, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0x3F, 0xFF, 0x74, 0x00, 0x00, 0xAE, 0xB4
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",
map(
tuple((
digits, digits,
char(':'), one_of(':'),
digits_range(0..60), digits_range(0..60),
char(':'), one_of(':'),
digits_range(0..60), digits_range(0..60),
one_of(":.;,"), one_of(|c| b":.;,".contains(&c)),
digits, digits,
)), )
|(hours, _, minutes, _, seconds, sep, frames)| TimeCode { .map(|(hours, _, minutes, _, seconds, sep, frames)| TimeCode {
hours, hours,
minutes, minutes,
seconds, seconds,
frames, frames,
drop_frame: sep == ';' || sep == ',', drop_frame: sep == b';' || sep == b',',
}, })
), .context(StrContext::Label("invalid timecode"))
)(s) .parse_next(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(),
TimeCode {
hours: 11, hours: 11,
minutes: 12, minutes: 12,
seconds: 13, seconds: 13,
frames: 14, frames: 14,
drop_frame: true 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(),
TimeCode {
hours: 11, hours: 11,
minutes: 12, minutes: 12,
seconds: 13, seconds: 13,
frames: 14, frames: 14,
drop_frame: false 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(),
TimeCode {
hours: 11, hours: 11,
minutes: 12, minutes: 12,
seconds: 13, seconds: 13,
frames: 14, frames: 14,
drop_frame: false 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((
opt(tag(&[0xEFu8, 0xBBu8, 0xBFu8][..])),
tag("Scenarist_SCC V1.0"),
end_of_line, end_of_line,
)), )
|_| SccLine::Header, .map(|_| SccLine::Header)
), .context(StrContext::Label("invalid header"))
)(s) .parse_next(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) State::CaptionOrEmpty => {
.map(|v| v.1) alt((caption, empty_line))
.parse_next(&mut line)
.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,
}), })
}
} }
} }
} }
@ -184,60 +172,46 @@ 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(),
SccLine::Caption(
TimeCode { TimeCode {
hours: 1, hours: 1,
minutes: 2, minutes: 2,
@ -245,22 +219,20 @@ mod tests {
frames: 14, frames: 14,
drop_frame: false drop_frame: false
}, },
vec![ vec![
0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0x7a, 0x94, 0x7a, 0x97, 0xa2, 0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0x7a, 0x94, 0x7a, 0x97,
0x97, 0xa2, 0xa8, 0x20, 0x68, 0xef, 0xf2, 0x6e, 0x20, 0x68, 0xef, 0x6e, 0x6b, 0xe9, 0xa2, 0x97, 0xa2, 0xa8, 0x20, 0x68, 0xef, 0xf2, 0x6e, 0x20, 0x68, 0xef, 0x6e,
0x6e, 0x67, 0x20, 0x29, 0x94, 0x2c, 0x94, 0x2c, 0x80, 0x80, 0x80, 0x80, 0x94, 0x2f, 0x6b, 0xe9, 0x6e, 0x67, 0x20, 0x29, 0x94, 0x2c, 0x94, 0x2c, 0x80, 0x80, 0x80,
0x94, 0x2f, 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(),
SccLine::Caption(
TimeCode { TimeCode {
hours: 1, hours: 1,
minutes: 2, minutes: 2,
@ -269,15 +241,14 @@ mod tests {
drop_frame: true 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(),
SccLine::Caption(
TimeCode { TimeCode {
hours: 1, hours: 1,
minutes: 3, minutes: 3,
@ -286,19 +257,18 @@ mod tests {
drop_frame: false drop_frame: false
}, },
vec![ vec![
0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0xf2, 0x94, 0xf2, 0xc8, 0x45, 0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0xf2, 0x94, 0xf2, 0xc8,
0xd9, 0x2c, 0x20, 0x54, 0xc8, 0x45, 0x52, 0x45, 0xae, 0x80, 0x94, 0x2c, 0x94, 0x2c, 0x45, 0xd9, 0x2c, 0x20, 0x54, 0xc8, 0x45, 0x52, 0x45, 0xae, 0x80, 0x94, 0x2c,
0x80, 0x80, 0x80, 0x80, 0x94, 0x2f, 0x94, 0x2f, 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(),
SccLine::Caption(
TimeCode { TimeCode {
hours: 0, hours: 0,
minutes: 0, minutes: 0,
@ -307,15 +277,14 @@ mod tests {
drop_frame: true, 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(),
SccLine::Caption(
TimeCode { TimeCode {
hours: 0, hours: 0,
minutes: 0, minutes: 0,
@ -324,9 +293,9 @@ mod tests {
drop_frame: true, drop_frame: true,
}, },
vec![0x94, 0x2c, 0x94, 0x2c], vec![0x94, 0x2c, 0x94, 0x2c],
),
)) ))
); );
assert!(input.is_empty());
} }
#[test] #[test]