use crate::{
config::ParsingOptions,
error::{ParseLineBytesError, ParseLineStrError, SyntaxError},
tag::{CustomTag, CustomTagAccess, KnownTag, NoCustomTag, UnknownTag, hls},
tag_internal::unknown::parse_assuming_ext_taken,
utils::{split_on_new_line, str_from},
};
use std::{borrow::Cow, cmp::PartialEq, fmt::Debug};
#[derive(Debug, PartialEq, Clone)]
#[allow(clippy::large_enum_variant)] pub enum HlsLine<'a, Custom = NoCustomTag>
where
Custom: CustomTag<'a>,
{
KnownTag(KnownTag<'a, Custom>),
UnknownTag(UnknownTag<'a>),
Comment(Cow<'a, str>),
Uri(Cow<'a, str>),
Blank,
}
impl<'a, Custom> From<hls::Tag<'a>> for HlsLine<'a, Custom>
where
Custom: CustomTag<'a>,
{
fn from(tag: hls::Tag<'a>) -> Self {
Self::KnownTag(KnownTag::Hls(tag))
}
}
impl<'a, Custom> From<CustomTagAccess<'a, Custom>> for HlsLine<'a, Custom>
where
Custom: CustomTag<'a>,
{
fn from(tag: CustomTagAccess<'a, Custom>) -> Self {
Self::KnownTag(KnownTag::Custom(tag))
}
}
impl<'a, Custom> From<UnknownTag<'a>> for HlsLine<'a, Custom>
where
Custom: CustomTag<'a>,
{
fn from(tag: UnknownTag<'a>) -> Self {
Self::UnknownTag(tag)
}
}
impl<'a> HlsLine<'a> {
pub fn comment(comment: impl Into<Cow<'a, str>>) -> Self {
Self::Comment(comment.into())
}
pub fn uri(uri: impl Into<Cow<'a, str>>) -> Self {
Self::Uri(uri.into())
}
pub fn blank() -> Self {
Self::Blank
}
}
macro_rules! impl_line_from_tag {
($tag_mod_path:path, $tag_name:ident) => {
impl<'a, Custom> From<$tag_mod_path> for HlsLine<'a, Custom>
where
Custom: CustomTag<'a>,
{
fn from(tag: $tag_mod_path) -> Self {
Self::KnownTag($crate::tag::KnownTag::Hls(
$crate::tag::hls::Tag::$tag_name(tag),
))
}
}
};
}
impl_line_from_tag!(hls::M3u, M3u);
impl_line_from_tag!(hls::Version<'a>, Version);
impl_line_from_tag!(hls::IndependentSegments, IndependentSegments);
impl_line_from_tag!(hls::Start<'a>, Start);
impl_line_from_tag!(hls::Define<'a>, Define);
impl_line_from_tag!(hls::Targetduration<'a>, Targetduration);
impl_line_from_tag!(hls::MediaSequence<'a>, MediaSequence);
impl_line_from_tag!(hls::DiscontinuitySequence<'a>, DiscontinuitySequence);
impl_line_from_tag!(hls::Endlist, Endlist);
impl_line_from_tag!(hls::PlaylistType, PlaylistType);
impl_line_from_tag!(hls::IFramesOnly, IFramesOnly);
impl_line_from_tag!(hls::PartInf<'a>, PartInf);
impl_line_from_tag!(hls::ServerControl<'a>, ServerControl);
impl_line_from_tag!(hls::Inf<'a>, Inf);
impl_line_from_tag!(hls::Byterange<'a>, Byterange);
impl_line_from_tag!(hls::Discontinuity, Discontinuity);
impl_line_from_tag!(hls::Key<'a>, Key);
impl_line_from_tag!(hls::Map<'a>, Map);
impl_line_from_tag!(hls::ProgramDateTime<'a>, ProgramDateTime);
impl_line_from_tag!(hls::Gap, Gap);
impl_line_from_tag!(hls::Bitrate<'a>, Bitrate);
impl_line_from_tag!(hls::Part<'a>, Part);
impl_line_from_tag!(hls::Daterange<'a>, Daterange);
impl_line_from_tag!(hls::Skip<'a>, Skip);
impl_line_from_tag!(hls::PreloadHint<'a>, PreloadHint);
impl_line_from_tag!(hls::RenditionReport<'a>, RenditionReport);
impl_line_from_tag!(hls::Media<'a>, Media);
impl_line_from_tag!(hls::StreamInf<'a>, StreamInf);
impl_line_from_tag!(hls::IFrameStreamInf<'a>, IFrameStreamInf);
impl_line_from_tag!(hls::SessionData<'a>, SessionData);
impl_line_from_tag!(hls::SessionKey<'a>, SessionKey);
impl_line_from_tag!(hls::ContentSteering<'a>, ContentSteering);
#[derive(Debug, PartialEq, Clone)]
pub struct ParsedLineSlice<'a, T>
where
T: Debug + PartialEq,
{
pub parsed: T,
pub remaining: Option<&'a str>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct ParsedByteSlice<'a, T>
where
T: Debug + PartialEq,
{
pub parsed: T,
pub remaining: Option<&'a [u8]>,
}
pub fn parse<'a>(
input: &'a str,
options: &ParsingOptions,
) -> Result<ParsedLineSlice<'a, HlsLine<'a>>, ParseLineStrError<'a>> {
parse_with_custom::<NoCustomTag>(input, options)
}
pub fn parse_with_custom<'a, 'b, Custom>(
input: &'a str,
options: &'b ParsingOptions,
) -> Result<ParsedLineSlice<'a, HlsLine<'a, Custom>>, ParseLineStrError<'a>>
where
Custom: CustomTag<'a>,
{
parse_bytes_with_custom(input.as_bytes(), options)
.map(|r| ParsedLineSlice {
parsed: r.parsed,
remaining: r.remaining.map(str_from),
})
.map_err(|error| ParseLineStrError {
errored_line_slice: ParsedLineSlice {
parsed: str_from(error.errored_line_slice.parsed),
remaining: error.errored_line_slice.remaining.map(str_from),
},
error: error.error,
})
}
pub fn parse_bytes<'a>(
input: &'a [u8],
options: &ParsingOptions,
) -> Result<ParsedByteSlice<'a, HlsLine<'a>>, ParseLineBytesError<'a>> {
parse_bytes_with_custom::<NoCustomTag>(input, options)
}
pub fn parse_bytes_with_custom<'a, 'b, Custom>(
input: &'a [u8],
options: &'b ParsingOptions,
) -> Result<ParsedByteSlice<'a, HlsLine<'a, Custom>>, ParseLineBytesError<'a>>
where
Custom: CustomTag<'a>,
{
if input.is_empty() {
Ok(ParsedByteSlice {
parsed: HlsLine::Blank,
remaining: None,
})
} else if input[0] == b'#' {
if input.get(3) == Some(&b'T') && &input[..3] == b"#EX" {
let tag_rest = &input[4..];
let mut tag = parse_assuming_ext_taken(tag_rest, input)
.map_err(|error| map_err_bytes(error, input))?;
if options.is_known_name(tag.parsed.name) || Custom::is_known_name(tag.parsed.name) {
match KnownTag::try_from(tag.parsed) {
Ok(known_tag) => Ok(ParsedByteSlice {
parsed: HlsLine::KnownTag(known_tag),
remaining: tag.remaining,
}),
Err(e) => {
tag.parsed.validation_error = Some(e);
Ok(ParsedByteSlice {
parsed: HlsLine::UnknownTag(tag.parsed),
remaining: tag.remaining,
})
}
}
} else {
Ok(ParsedByteSlice {
parsed: HlsLine::UnknownTag(tag.parsed),
remaining: tag.remaining,
})
}
} else {
let ParsedByteSlice { parsed, remaining } = split_on_new_line(&input[1..]);
let comment =
std::str::from_utf8(parsed).map_err(|error| map_err_bytes(error, input))?;
Ok(ParsedByteSlice {
parsed: HlsLine::Comment(Cow::Borrowed(comment)),
remaining,
})
}
} else {
let ParsedByteSlice { parsed, remaining } = split_on_new_line(input);
let uri = std::str::from_utf8(parsed).map_err(|error| map_err_bytes(error, input))?;
if uri.is_empty() {
Ok(ParsedByteSlice {
parsed: HlsLine::Blank,
remaining,
})
} else {
Ok(ParsedByteSlice {
parsed: HlsLine::Uri(Cow::Borrowed(uri)),
remaining,
})
}
}
}
fn map_err_bytes<E: Into<SyntaxError>>(error: E, input: &[u8]) -> ParseLineBytesError<'_> {
let errored_line_slice = split_on_new_line(input);
ParseLineBytesError {
errored_line_slice,
error: error.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
config::ParsingOptionsBuilder,
error::{ParseTagValueError, ValidationError},
tag::{
AttributeValue, TagValue,
hls::{self, M3u, Start},
},
};
use pretty_assertions::assert_eq;
#[test]
fn uri_line() {
assert_eq!(
Ok(HlsLine::Uri("hello/world.m3u8".into())),
parse("hello/world.m3u8", &ParsingOptions::default()).map(|p| p.parsed)
)
}
#[test]
fn blank_line() {
assert_eq!(
Ok(HlsLine::Blank),
parse("", &ParsingOptions::default()).map(|p| p.parsed)
);
}
#[test]
fn comment() {
assert_eq!(
Ok(HlsLine::Comment("Comment".into())),
parse("#Comment", &ParsingOptions::default()).map(|p| p.parsed)
);
}
#[test]
fn basic_tag() {
assert_eq!(
Ok(HlsLine::from(hls::Tag::M3u(M3u))),
parse("#EXTM3U", &ParsingOptions::default()).map(|p| p.parsed)
);
}
#[test]
fn custom_tag() {
#[derive(Debug, PartialEq, Clone)]
struct TestTag<'a> {
greeting_type: &'a str,
message: &'a str,
times: u64,
score: Option<f64>,
}
impl<'a> TryFrom<UnknownTag<'a>> for TestTag<'a> {
type Error = ValidationError;
fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
let value = tag.value().ok_or(ParseTagValueError::UnexpectedEmpty)?;
let list = value.try_as_attribute_list()?;
let Some(greeting_type) = list
.get("TYPE")
.and_then(AttributeValue::unquoted)
.and_then(|v| v.try_as_utf_8().ok())
else {
return Err(ValidationError::MissingRequiredAttribute("TYPE"));
};
let Some(message) = list.get("MESSAGE").and_then(AttributeValue::quoted) else {
return Err(ValidationError::MissingRequiredAttribute("MESSAGE"));
};
let Some(times) = list
.get("TIMES")
.and_then(AttributeValue::unquoted)
.and_then(|v| v.try_as_decimal_integer().ok())
else {
return Err(ValidationError::MissingRequiredAttribute("TIMES"));
};
let score = list
.get("SCORE")
.and_then(AttributeValue::unquoted)
.and_then(|v| v.try_as_decimal_floating_point().ok());
Ok(Self {
greeting_type,
message,
times,
score,
})
}
}
impl CustomTag<'static> for TestTag<'static> {
fn is_known_name(name: &str) -> bool {
name == "-X-TEST-TAG"
}
}
assert_eq!(
Ok(HlsLine::from(CustomTagAccess {
custom_tag: TestTag {
greeting_type: "GREETING".into(),
message: "Hello, World!".into(),
times: 42,
score: None,
},
is_dirty: false,
original_input: b"#EXT-X-TEST-TAG:TYPE=GREETING,MESSAGE=\"Hello, World!\",TIMES=42"
})),
parse_with_custom::<TestTag>(
"#EXT-X-TEST-TAG:TYPE=GREETING,MESSAGE=\"Hello, World!\",TIMES=42",
&ParsingOptions::default()
)
.map(|p| p.parsed)
);
}
#[test]
fn avoiding_parsing_known_tag_when_configured_to_avoid_via_parsing_options() {
assert_eq!(
Ok(HlsLine::from(hls::Tag::Start(
Start::builder().with_time_offset(-18.0).finish()
))),
parse("#EXT-X-START:TIME-OFFSET=-18", &ParsingOptions::default()).map(|p| p.parsed)
);
assert_eq!(
Ok(HlsLine::UnknownTag(UnknownTag {
name: "-X-START",
value: Some(TagValue(b"TIME-OFFSET=-18")),
original_input: b"#EXT-X-START:TIME-OFFSET=-18",
validation_error: None,
})),
parse(
"#EXT-X-START:TIME-OFFSET=-18",
&ParsingOptionsBuilder::new()
.with_parsing_for_all_tags()
.without_parsing_for_start()
.build()
)
.map(|p| p.parsed)
);
}
#[test]
fn empty_line_before_new_line_break_should_be_parsed_as_blank() {
let input = "\n#something else";
assert_eq!(
ParsedLineSlice {
parsed: HlsLine::Blank,
remaining: Some("#something else")
},
parse(input, &ParsingOptions::default()).unwrap()
);
}
}