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-check",
"gstreamer-video",
"nom",
"once_cell",
"pango",
"pangocairo",
@ -2264,6 +2263,7 @@ dependencies = [
"serde",
"serde_json",
"uuid",
"winnow 0.6.2",
]
[[package]]

View file

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

View file

@ -139,7 +139,10 @@ fn parse_timecode_rate(
impl State {
#[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 {
self.replay_last_line = false;
&self.last_raw_line

View file

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

View file

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

View file

@ -123,7 +123,10 @@ fn parse_timecode(
impl State {
#[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) {
None => {
return Ok(None);

View file

@ -8,7 +8,7 @@
// SPDX-License-Identifier: MPL-2.0
use crate::parser_utils::{end_of_line, timecode, TimeCode};
use nom::IResult;
use winnow::{error::StrContext, PResult, Parser};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SccLine {
@ -30,44 +30,36 @@ pub struct SccParser {
}
/// Parser for the SCC header
fn header(s: &[u8]) -> IResult<&[u8], SccLine> {
use nom::bytes::complete::tag;
use nom::combinator::{map, opt};
use nom::error::context;
use nom::sequence::tuple;
fn header(s: &mut &[u8]) -> PResult<SccLine> {
use winnow::combinator::opt;
use winnow::token::literal;
context(
"invalid header",
map(
tuple((
opt(tag(&[0xEFu8, 0xBBu8, 0xBFu8][..])),
tag("Scenarist_SCC V1.0"),
end_of_line,
)),
|_| SccLine::Header,
),
)(s)
(
opt(literal(&[0xEFu8, 0xBBu8, 0xBFu8][..])),
literal("Scenarist_SCC V1.0"),
end_of_line,
)
.map(|_| SccLine::Header)
.context(StrContext::Label("invalid header"))
.parse_next(s)
}
/// Parser that accepts only an empty line
fn empty_line(s: &[u8]) -> IResult<&[u8], SccLine> {
use nom::combinator::map;
use nom::error::context;
context("invalid empty line", map(end_of_line, |_| SccLine::Empty))(s)
fn empty_line(s: &mut &[u8]) -> PResult<SccLine> {
end_of_line
.map(|_| SccLine::Empty)
.context(StrContext::Label("invalid empty line"))
.parse_next(s)
}
/// A single SCC payload item. This is ASCII 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)> {
use nom::bytes::complete::take_while_m_n;
use nom::character::is_hex_digit;
use nom::combinator::map;
use nom::error::context;
fn scc_payload_item(s: &mut &[u8]) -> PResult<(u8, u8)> {
use winnow::stream::AsChar;
use winnow::token::take_while;
context(
"invalid SCC payload item",
map(take_while_m_n(4, 4, is_hex_digit), |s: &[u8]| {
take_while(4..=4, AsChar::is_hex_digit)
.map(|s: &[u8]| {
let hex_to_u8 = |v: u8| match v {
v if v.is_ascii_digit() => v - b'0',
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]);
(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.
fn scc_payload(s: &[u8]) -> IResult<&[u8], Vec<u8>> {
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::combinator::map;
use nom::error::context;
use nom::multi::fold_many1;
use nom::sequence::pair;
use nom::Parser;
fn scc_payload(s: &mut &[u8]) -> PResult<Vec<u8>> {
use winnow::combinator::{alt, repeat};
use winnow::token::literal;
let parse_item = map(
pair(scc_payload_item, alt((tag(" ").map(|_| ()), end_of_line))),
|(item, _)| item,
);
let parse_item = (
scc_payload_item,
alt((literal(" ").map(|_| ()), end_of_line)),
)
.map(|(item, _)| item);
context(
"invalid SCC payload",
fold_many1(parse_item, Vec::new, |mut acc: Vec<_>, item| {
repeat(1.., parse_item)
.fold(Vec::new, |mut acc: Vec<_>, item| {
acc.push(item.0);
acc.push(item.1);
acc
}),
)(s)
})
.context(StrContext::Label("invalid SCC payload"))
.parse_next(s)
}
/// Parser for a SCC caption line in the form `timecode\tpayload`.
fn caption(s: &[u8]) -> IResult<&[u8], SccLine> {
use nom::bytes::complete::tag;
use nom::character::complete::multispace0;
use nom::combinator::map;
use nom::error::context;
use nom::sequence::tuple;
fn caption(s: &mut &[u8]) -> PResult<SccLine> {
use winnow::ascii::multispace0;
use winnow::token::literal;
context(
"invalid SCC caption line",
map(
tuple((timecode, tag("\t"), multispace0, scc_payload, end_of_line)),
|(tc, _, _, value, _)| SccLine::Caption(tc, value),
),
)(s)
(
timecode,
literal("\t"),
multispace0,
scc_payload,
end_of_line,
)
.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.
@ -143,37 +132,36 @@ impl SccParser {
self.state = State::Header;
}
pub fn parse_line<'a>(
&mut self,
line: &'a [u8],
) -> Result<SccLine, nom::error::Error<&'a [u8]>> {
use nom::branch::alt;
pub fn parse_line(&mut self, mut line: &[u8]) -> Result<SccLine, winnow::error::ContextError> {
use winnow::combinator::alt;
match self.state {
State::Header => header(line)
State::Header => header(&mut line)
.map(|v| {
self.state = State::Empty;
v.1
v
})
.map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(e) => e,
winnow::error::ErrMode::Incomplete(_) => unreachable!(),
winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(e) => e,
}),
State::Empty => empty_line(line)
State::Empty => empty_line(&mut line)
.map(|v| {
self.state = State::CaptionOrEmpty;
v.1
v
})
.map_err(|err| match err {
nom::Err::Incomplete(_) => unreachable!(),
nom::Err::Error(e) | nom::Err::Failure(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,
winnow::error::ErrMode::Incomplete(_) => unreachable!(),
winnow::error::ErrMode::Backtrack(e) | winnow::error::ErrMode::Cut(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]
fn test_header() {
assert_eq!(
header(b"Scenarist_SCC V1.0".as_ref()),
Ok((b"".as_ref(), SccLine::Header,))
);
let mut input = b"Scenarist_SCC V1.0".as_slice();
assert_eq!(header(&mut input), Ok(SccLine::Header));
assert!(input.is_empty());
assert_eq!(
header(b"Scenarist_SCC V1.0\n".as_ref()),
Ok((b"".as_ref(), SccLine::Header))
);
let mut input = b"Scenarist_SCC V1.0\n".as_slice();
assert_eq!(header(&mut input), Ok(SccLine::Header));
assert!(input.is_empty());
assert_eq!(
header(b"Scenarist_SCC V1.0\r\n".as_ref()),
Ok((b"".as_ref(), SccLine::Header))
);
let mut input = b"Scenarist_SCC V1.0\r\n".as_slice();
assert_eq!(header(&mut input), Ok(SccLine::Header));
assert!(input.is_empty());
assert_eq!(
header(b"Scenarist_SCC V1.1".as_ref()),
Err(nom::Err::Error(nom::error::Error::new(
b"Scenarist_SCC V1.1".as_ref(),
nom::error::ErrorKind::Tag
))),
);
let mut input = b"Scenarist_SCC V1.1".as_slice();
assert!(header(&mut input).is_err());
}
#[test]
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!(
empty_line(b"\n".as_ref()),
Ok((b"".as_ref(), SccLine::Empty))
);
let mut input = b"\n".as_slice();
assert_eq!(empty_line(&mut input), Ok(SccLine::Empty));
assert!(input.is_empty());
assert_eq!(
empty_line(b"\r\n".as_ref()),
Ok((b"".as_ref(), SccLine::Empty))
);
let mut input = b"\r\n".as_slice();
assert_eq!(empty_line(&mut input), Ok(SccLine::Empty));
assert!(input.is_empty());
assert_eq!(
empty_line(b" \r\n".as_ref()),
Err(nom::Err::Error(nom::error::Error::new(
b" \r\n".as_ref(),
nom::error::ErrorKind::Eof
))),
);
let mut input = b" \r\n".as_slice();
assert!(empty_line(&mut input).is_err());
}
#[test]
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!(
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()),
Ok((
b"".as_ref(),
SccLine::Caption(
TimeCode {
hours: 1,
minutes: 2,
seconds: 53,
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, 0x6b, 0xe9,
0x6e, 0x67, 0x20, 0x29, 0x94, 0x2c, 0x94, 0x2c, 0x80, 0x80, 0x80, 0x80, 0x94, 0x2f,
0x94, 0x2f,
]
),
caption(&mut input),
Ok(SccLine::Caption(
TimeCode {
hours: 1,
minutes: 2,
seconds: 53,
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,
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!(
caption(b"01:02:55;14 942c 942c".as_ref()),
Ok((
b"".as_ref(),
SccLine::Caption(
TimeCode {
hours: 1,
minutes: 2,
seconds: 55,
frames: 14,
drop_frame: true
},
vec![0x94, 0x2c, 0x94, 0x2c]
),
caption(&mut input),
Ok(SccLine::Caption(
TimeCode {
hours: 1,
minutes: 2,
seconds: 55,
frames: 14,
drop_frame: true
},
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!(
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()),
Ok((
b"".as_ref(),
SccLine::Caption(
TimeCode {
hours: 1,
minutes: 3,
seconds: 27,
frames: 29,
drop_frame: false
},
vec![
0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0xf2, 0x94, 0xf2, 0xc8, 0x45,
0xd9, 0x2c, 0x20, 0x54, 0xc8, 0x45, 0x52, 0x45, 0xae, 0x80, 0x94, 0x2c, 0x94, 0x2c,
0x80, 0x80, 0x80, 0x80, 0x94, 0x2f, 0x94, 0x2f,
]
),
caption(&mut input),
Ok(SccLine::Caption(
TimeCode {
hours: 1,
minutes: 3,
seconds: 27,
frames: 29,
drop_frame: false
},
vec![
0x94, 0xae, 0x94, 0xae, 0x94, 0x20, 0x94, 0x20, 0x94, 0xf2, 0x94, 0xf2, 0xc8,
0x45, 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!(
caption(b"00:00:00;00\t942c 942c".as_ref()),
Ok((
b"".as_ref(),
SccLine::Caption(
TimeCode {
hours: 0,
minutes: 0,
seconds: 00,
frames: 00,
drop_frame: true,
},
vec![0x94, 0x2c, 0x94, 0x2c],
),
caption(&mut input),
Ok(SccLine::Caption(
TimeCode {
hours: 0,
minutes: 0,
seconds: 00,
frames: 00,
drop_frame: true,
},
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!(
caption(b"00:00:00;00\t942c 942c\r\n".as_ref()),
Ok((
b"".as_ref(),
SccLine::Caption(
TimeCode {
hours: 0,
minutes: 0,
seconds: 00,
frames: 00,
drop_frame: true,
},
vec![0x94, 0x2c, 0x94, 0x2c],
),
caption(&mut input),
Ok(SccLine::Caption(
TimeCode {
hours: 0,
minutes: 0,
seconds: 00,
frames: 00,
drop_frame: true,
},
vec![0x94, 0x2c, 0x94, 0x2c],
))
);
assert!(input.is_empty());
}
#[test]