quick_m3u8/line.rs
1//! Types and operations for working with lines of a HLS playlist.
2//!
3//! This module includes various types and functions for working with lines of a HLS playlist. The
4//! main informational type of the library, [`HlsLine`], exists in this module (and re-exported at
5//! the top level), along with parsing functions to extract `HlsLine` from input data.
6
7use crate::{
8 config::ParsingOptions,
9 error::{ParseLineBytesError, ParseLineStrError, SyntaxError},
10 tag::{CustomTag, CustomTagAccess, KnownTag, NoCustomTag, UnknownTag, hls},
11 tag_internal::unknown::parse_assuming_ext_taken,
12 utils::{split_on_new_line, str_from},
13};
14use std::{borrow::Cow, cmp::PartialEq, fmt::Debug};
15
16/// A parsed line from a HLS playlist.
17///
18/// The HLS specification, in [Section 4.1. Definition of a Playlist], defines lines in a playlist
19/// as such:
20/// > Each line is a URI, is blank, or starts with the character '#'. Lines that start with the
21/// > character '#' are either comments or tags. Tags begin with #EXT.
22///
23/// This data structure follows that guidance but also adds [`HlsLine::UnknownTag`] and
24/// [`KnownTag::Custom`]. These cases are described in more detail within their own documentation,
25/// but in short, the first allows us to capture tags that are not yet known to the library
26/// (providing at least a split between name and value), while the second allows a user of the
27/// library to define their own custom tag specification that can be then parsed into a strongly
28/// typed structure within a `HlsLine::KnownTag` by the library.
29///
30/// [Section 4.1. Definition of a Playlist]: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-18#section-4.1
31#[derive(Debug, PartialEq, Clone)]
32#[allow(clippy::large_enum_variant)] // See comment on crate::tag::known::Tag.
33pub enum HlsLine<'a, Custom = NoCustomTag>
34where
35 Custom: CustomTag<'a>,
36{
37 /// A tag known to the library, either via the included definitions of HLS tags as specified in
38 /// the `draft-pantos-hls` Internet-Draft, or via a custom tag registration provided by the user
39 /// of the library.
40 ///
41 /// See [`KnownTag`] for more information.
42 KnownTag(KnownTag<'a, Custom>),
43 /// A tag, as defined by the `#EXT` prefix, but not one that is known to the library, or that is
44 /// deliberately ignored via [`ParsingOptions`].
45 ///
46 /// See [`UnknownTag`] for more information.
47 UnknownTag(UnknownTag<'a>),
48 /// A comment line. These are lines that begin with `#` and are followed by a string of UTF-8
49 /// characters (though not BOM or UTF-8 control characters). The line is terminated by either a
50 /// line feed (`\n`) or a carriage return followed by a line feed (`\r\n`).
51 ///
52 /// The associated value is a [`std::borrow::Cow`] to allow for both a user constructed value
53 /// and also a copy-free reference to the original parsed data. It includes all characters after
54 /// the `#` (including any whitespace) and does not include the line break characters. Below
55 /// demonstrates this:
56 /// ```
57 /// # use quick_m3u8::{config::ParsingOptions, HlsLine, custom_parsing::line::parse,
58 /// # error::ParseLineStrError};
59 /// # use std::borrow::Cow;
60 /// # let options = ParsingOptions::default();
61 /// let original = "# Comment line. Note the leading space.\r\n";
62 /// let line = parse(original, &options)?.parsed;
63 /// assert_eq!(
64 /// HlsLine::Comment(Cow::Borrowed(" Comment line. Note the leading space.")),
65 /// line,
66 /// );
67 /// # Ok::<(), ParseLineStrError>(())
68 /// ```
69 Comment(Cow<'a, str>),
70 /// A URI line. These are lines that do not begin with `#` and are not empty. It is important to
71 /// note that the library does not do any validation on the line being a valid URI. The only
72 /// validation that happens is that line can be represented as a UTF-8 string (internally we use
73 /// [`std::str::from_utf8`]). This means that the line may contain characters that are invalid
74 /// in a URI, or may otherwise not make sense in the context of the parsed playlist. It is up to
75 /// the user of the library to validate the URI, perhaps using a URL parsing library (such as
76 /// [url]).
77 ///
78 /// The associated value is a [`std::borrow::Cow`] to allow for both a user constructed value
79 /// and also a copy-free reference to the original parsed data. It includes all characters up
80 /// until, but not including, the line break characters. The following demonstrates this:
81 /// ```
82 /// # use quick_m3u8::{config::ParsingOptions, HlsLine, error::ParseLineStrError};
83 /// # use quick_m3u8::custom_parsing::line::parse;
84 /// # use std::borrow::Cow;
85 /// # let options = ParsingOptions::default();
86 /// let expected = "hi.m3u8";
87 /// // Demonstrating that new line characters are not included:
88 /// assert_eq!(
89 /// HlsLine::Uri(Cow::Borrowed(expected)),
90 /// parse("hi.m3u8\n", &options)?.parsed,
91 /// );
92 /// assert_eq!(
93 /// HlsLine::Uri(Cow::Borrowed(expected)),
94 /// parse("hi.m3u8\r\n", &options)?.parsed,
95 /// );
96 /// assert_eq!(
97 /// HlsLine::Uri(Cow::Borrowed(expected)),
98 /// parse("hi.m3u8", &options)?.parsed,
99 /// );
100 /// # Ok::<(), ParseLineStrError>(())
101 /// ```
102 ///
103 /// [url]: https://crates.io/crates/url
104 Uri(Cow<'a, str>),
105 /// A blank line. This line contained no characters other than a new line. Note that since the
106 /// library does not validate characters in a URI line, a line comprised entirely of whitespace
107 /// will still be parsed as a URI line, rather than a blank line. As mentioned, it is up to the
108 /// user of the library to properly validate URI lines.
109 /// ```
110 /// # use quick_m3u8::{config::ParsingOptions, HlsLine, error::ParseLineStrError};
111 /// # use quick_m3u8::custom_parsing::line::parse;
112 /// # use std::borrow::Cow;
113 /// # let options = ParsingOptions::default();
114 /// // Demonstrating what is considered a blank line:
115 /// assert_eq!(
116 /// HlsLine::Blank,
117 /// parse("", &options)?.parsed,
118 /// );
119 /// assert_eq!(
120 /// HlsLine::Blank,
121 /// parse("\n", &options)?.parsed,
122 /// );
123 /// assert_eq!(
124 /// HlsLine::Blank,
125 /// parse("\r\n", &options)?.parsed,
126 /// );
127 /// // Demonstrating that a whitespace only line is still parsed as a URI:
128 /// assert_eq!(
129 /// HlsLine::Uri(Cow::Borrowed(" ")),
130 /// parse(" \n", &options)?.parsed,
131 /// );
132 /// # Ok::<(), ParseLineStrError>(())
133 /// ```
134 Blank,
135}
136
137impl<'a, Custom> From<hls::Tag<'a>> for HlsLine<'a, Custom>
138where
139 Custom: CustomTag<'a>,
140{
141 fn from(tag: hls::Tag<'a>) -> Self {
142 Self::KnownTag(KnownTag::Hls(tag))
143 }
144}
145
146impl<'a, Custom> From<CustomTagAccess<'a, Custom>> for HlsLine<'a, Custom>
147where
148 Custom: CustomTag<'a>,
149{
150 fn from(tag: CustomTagAccess<'a, Custom>) -> Self {
151 Self::KnownTag(KnownTag::Custom(tag))
152 }
153}
154
155impl<'a, Custom> From<UnknownTag<'a>> for HlsLine<'a, Custom>
156where
157 Custom: CustomTag<'a>,
158{
159 fn from(tag: UnknownTag<'a>) -> Self {
160 Self::UnknownTag(tag)
161 }
162}
163
164impl<'a> HlsLine<'a> {
165 /// Convenience constructor for [`HlsLine::Comment`]. This will construct the line with the
166 /// generic `Custom` in [`HlsLine::KnownTag`] being [`NoCustomTag`].
167 pub fn comment(comment: impl Into<Cow<'a, str>>) -> Self {
168 Self::Comment(comment.into())
169 }
170
171 /// Convenience constructor for [`HlsLine::Uri`]. This will construct the line with the generic
172 /// `Custom` in [`HlsLine::KnownTag`] being [`NoCustomTag`].
173 pub fn uri(uri: impl Into<Cow<'a, str>>) -> Self {
174 Self::Uri(uri.into())
175 }
176
177 /// Convenience constructor for [`HlsLine::Blank`]. This will construct the line with the
178 /// generic `Custom` in [`HlsLine::KnownTag`] being [`NoCustomTag`].
179 pub fn blank() -> Self {
180 Self::Blank
181 }
182}
183
184macro_rules! impl_line_from_tag {
185 ($tag_mod_path:path, $tag_name:ident) => {
186 impl<'a, Custom> From<$tag_mod_path> for HlsLine<'a, Custom>
187 where
188 Custom: CustomTag<'a>,
189 {
190 fn from(tag: $tag_mod_path) -> Self {
191 Self::KnownTag($crate::tag::KnownTag::Hls(
192 $crate::tag::hls::Tag::$tag_name(tag),
193 ))
194 }
195 }
196 };
197}
198
199impl_line_from_tag!(hls::M3u, M3u);
200impl_line_from_tag!(hls::Version<'a>, Version);
201impl_line_from_tag!(hls::IndependentSegments, IndependentSegments);
202impl_line_from_tag!(hls::Start<'a>, Start);
203impl_line_from_tag!(hls::Define<'a>, Define);
204impl_line_from_tag!(hls::Targetduration<'a>, Targetduration);
205impl_line_from_tag!(hls::MediaSequence<'a>, MediaSequence);
206impl_line_from_tag!(hls::DiscontinuitySequence<'a>, DiscontinuitySequence);
207impl_line_from_tag!(hls::Endlist, Endlist);
208impl_line_from_tag!(hls::PlaylistType, PlaylistType);
209impl_line_from_tag!(hls::IFramesOnly, IFramesOnly);
210impl_line_from_tag!(hls::PartInf<'a>, PartInf);
211impl_line_from_tag!(hls::ServerControl<'a>, ServerControl);
212impl_line_from_tag!(hls::Inf<'a>, Inf);
213impl_line_from_tag!(hls::Byterange<'a>, Byterange);
214impl_line_from_tag!(hls::Discontinuity, Discontinuity);
215impl_line_from_tag!(hls::Key<'a>, Key);
216impl_line_from_tag!(hls::Map<'a>, Map);
217impl_line_from_tag!(hls::ProgramDateTime<'a>, ProgramDateTime);
218impl_line_from_tag!(hls::Gap, Gap);
219impl_line_from_tag!(hls::Bitrate<'a>, Bitrate);
220impl_line_from_tag!(hls::Part<'a>, Part);
221impl_line_from_tag!(hls::Daterange<'a>, Daterange);
222impl_line_from_tag!(hls::Skip<'a>, Skip);
223impl_line_from_tag!(hls::PreloadHint<'a>, PreloadHint);
224impl_line_from_tag!(hls::RenditionReport<'a>, RenditionReport);
225impl_line_from_tag!(hls::Media<'a>, Media);
226impl_line_from_tag!(hls::StreamInf<'a>, StreamInf);
227impl_line_from_tag!(hls::IFrameStreamInf<'a>, IFrameStreamInf);
228impl_line_from_tag!(hls::SessionData<'a>, SessionData);
229impl_line_from_tag!(hls::SessionKey<'a>, SessionKey);
230impl_line_from_tag!(hls::ContentSteering<'a>, ContentSteering);
231
232/// A slice of parsed line data from a HLS playlist.
233///
234/// This struct allows us to parse some way into a playlist, breaking on the new line, and providing
235/// the remaining characters after the new line in the [`Self::remaining`] field. This is a building
236/// block type that is used by the [`crate::Reader`] to work through an input playlist with each
237/// call to [`crate::Reader::read_line`].
238#[derive(Debug, PartialEq, Clone)]
239pub struct ParsedLineSlice<'a, T>
240where
241 T: Debug + PartialEq,
242{
243 /// The parsed data from the slice of line data from the playlist.
244 pub parsed: T,
245 /// The remaining string slice (after new line characters) from the playlist after parsing. If
246 /// the parsed line was the last in the input data then the `remaining` is `None`.
247 pub remaining: Option<&'a str>,
248}
249/// A slice of parsed line data from a HLS playlist.
250///
251/// This struct allows us to parse some way into a playlist, breaking on the new line, and providing
252/// the remaining characters after the new line in the [`Self::remaining`] field. This is a building
253/// block type that is used by the [`crate::Reader`] to work through an input playlist with each
254/// call to [`crate::Reader::read_line`].
255#[derive(Debug, PartialEq, Clone)]
256pub struct ParsedByteSlice<'a, T>
257where
258 T: Debug + PartialEq,
259{
260 /// The parsed data from the slice of line data from the playlist.
261 pub parsed: T,
262 /// The remaining byte slice (after new line characters) from the playlist after parsing. If
263 /// the parsed line was the last in the input data then the `remaining` is `None`.
264 pub remaining: Option<&'a [u8]>,
265}
266
267/// Parse an input string slice with the provided options.
268///
269/// This method is a lower level method than using [`crate::Reader`] directly. The `Reader` uses
270/// this method internally. It allows the user to parse a single line of HLS data and provides the
271/// remaining data after the new line. Custom reader implementations can be built on top of this
272/// method.
273///
274/// ## Example
275/// ```
276/// # use quick_m3u8::{
277/// # config::ParsingOptions,
278/// # HlsLine, custom_parsing::{ParsedLineSlice, line::parse},
279/// # error::ParseLineStrError,
280/// # tag::hls::{M3u, Targetduration, Version},
281/// # };
282/// const PLAYLIST: &str = r#"#EXTM3U
283/// #EXT-X-TARGETDURATION:10
284/// #EXT-X-VERSION:3
285/// "#;
286/// let options = ParsingOptions::default();
287///
288/// let ParsedLineSlice { parsed, remaining } = parse(PLAYLIST, &options)?;
289/// assert_eq!(parsed, HlsLine::from(M3u));
290///
291/// let Some(remaining) = remaining else { return Ok(()) };
292/// let ParsedLineSlice { parsed, remaining } = parse(remaining, &options)?;
293/// assert_eq!(parsed, HlsLine::from(Targetduration::new(10)));
294///
295/// let Some(remaining) = remaining else { return Ok(()) };
296/// let ParsedLineSlice { parsed, remaining } = parse(remaining, &options)?;
297/// assert_eq!(parsed, HlsLine::from(Version::new(3)));
298///
299/// let Some(remaining) = remaining else { return Ok(()) };
300/// let ParsedLineSlice { parsed, remaining } = parse(remaining, &options)?;
301/// assert_eq!(parsed, HlsLine::Blank);
302/// assert_eq!(remaining, None);
303/// # Ok::<(), ParseLineStrError>(())
304/// ```
305pub fn parse<'a>(
306 input: &'a str,
307 options: &ParsingOptions,
308) -> Result<ParsedLineSlice<'a, HlsLine<'a>>, ParseLineStrError<'a>> {
309 parse_with_custom::<NoCustomTag>(input, options)
310}
311
312/// Parse an input string slice with the provided options with support for the provided custom tag.
313///
314/// This method is a lower level method than using [`crate::Reader`] directly. The `Reader` uses
315/// this method internally. It allows the user to parse a single line of HLS data and provides the
316/// remaining data after the new line. Custom reader implementations can be built on top of this
317/// method. This method differs from [`parse`] as it allows the user to provide their own custom tag
318/// implementation for parsing.
319///
320/// ## Example
321/// ```
322/// # use quick_m3u8::{
323/// # HlsLine,
324/// # config::ParsingOptions,
325/// # custom_parsing::{ParsedLineSlice, line::parse_with_custom},
326/// # error::{ParseLineStrError, ValidationError, ParseTagValueError},
327/// # tag::{KnownTag, CustomTag, UnknownTag},
328/// # tag::hls::{M3u, Targetduration, Version},
329/// # };
330/// #[derive(Debug, Clone, PartialEq)]
331/// struct UserDefinedTag<'a> {
332/// message: &'a str,
333/// }
334/// impl<'a> TryFrom<UnknownTag<'a>> for UserDefinedTag<'a> { // --snip--
335/// # type Error = ValidationError;
336/// # fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
337/// # let mut list = tag
338/// # .value()
339/// # .ok_or(ParseTagValueError::UnexpectedEmpty)?
340/// # .try_as_attribute_list()?;
341/// # let Some(message) = list.remove("MESSAGE").and_then(|v| v.quoted()) else {
342/// # return Err(ValidationError::MissingRequiredAttribute("MESSAGE"));
343/// # };
344/// # Ok(Self { message })
345/// # }
346/// }
347/// impl<'a> CustomTag<'a> for UserDefinedTag<'a> { // --snip--
348/// # fn is_known_name(name: &str) -> bool {
349/// # name == "-X-USER-DEFINED-TAG"
350/// # }
351/// }
352///
353/// const PLAYLIST: &str = r#"#EXTM3U
354/// #EXT-X-USER-DEFINED-TAG:MESSAGE="Hello, World!"
355/// "#;
356/// let options = ParsingOptions::default();
357///
358/// let ParsedLineSlice {
359/// parsed,
360/// remaining
361/// } = parse_with_custom::<UserDefinedTag>(PLAYLIST, &options)?;
362/// assert_eq!(parsed, HlsLine::from(M3u));
363///
364/// let Some(remaining) = remaining else { return Ok(()) };
365/// let ParsedLineSlice {
366/// parsed,
367/// remaining
368/// } = parse_with_custom::<UserDefinedTag>(remaining, &options)?;
369/// let HlsLine::KnownTag(KnownTag::Custom(tag)) = parsed else { return Ok(()) };
370/// assert_eq!(tag.as_ref(), &UserDefinedTag { message: "Hello, World!" });
371///
372/// let Some(remaining) = remaining else { return Ok(()) };
373/// let ParsedLineSlice {
374/// parsed,
375/// remaining
376/// } = parse_with_custom::<UserDefinedTag>(remaining, &options)?;
377/// assert_eq!(parsed, HlsLine::Blank);
378/// assert_eq!(remaining, None);
379/// # Ok::<(), ParseLineStrError>(())
380/// ```
381pub fn parse_with_custom<'a, 'b, Custom>(
382 input: &'a str,
383 options: &'b ParsingOptions,
384) -> Result<ParsedLineSlice<'a, HlsLine<'a, Custom>>, ParseLineStrError<'a>>
385where
386 Custom: CustomTag<'a>,
387{
388 parse_bytes_with_custom(input.as_bytes(), options)
389 // These conversions from ParsedByteSlice to ParsedLineSlice are only safe here because we
390 // know that these must represent valid UTF-8.
391 .map(|r| ParsedLineSlice {
392 parsed: r.parsed,
393 remaining: r.remaining.map(str_from),
394 })
395 .map_err(|error| ParseLineStrError {
396 errored_line_slice: ParsedLineSlice {
397 parsed: str_from(error.errored_line_slice.parsed),
398 remaining: error.errored_line_slice.remaining.map(str_from),
399 },
400 error: error.error,
401 })
402}
403
404/// Parse an input byte slice with the provided options.
405///
406/// This method is equivalent to [`parse`] but using `&[u8]` instead of `&str`. Refer to
407/// documentation of [`parse`] for more information.
408pub fn parse_bytes<'a>(
409 input: &'a [u8],
410 options: &ParsingOptions,
411) -> Result<ParsedByteSlice<'a, HlsLine<'a>>, ParseLineBytesError<'a>> {
412 parse_bytes_with_custom::<NoCustomTag>(input, options)
413}
414
415/// Parse an input byte slice with the provided options with support for the provided custom tag.
416///
417/// This method is equivalent to [`parse_with_custom`] but using `&[u8]` instead of `&str`. Refer to
418/// documentation of [`parse_with_custom`] for more information.
419pub fn parse_bytes_with_custom<'a, 'b, Custom>(
420 input: &'a [u8],
421 options: &'b ParsingOptions,
422) -> Result<ParsedByteSlice<'a, HlsLine<'a, Custom>>, ParseLineBytesError<'a>>
423where
424 Custom: CustomTag<'a>,
425{
426 if input.is_empty() {
427 Ok(ParsedByteSlice {
428 parsed: HlsLine::Blank,
429 remaining: None,
430 })
431 } else if input[0] == b'#' {
432 if input.get(3) == Some(&b'T') && &input[..3] == b"#EX" {
433 let tag_rest = &input[4..];
434 let mut tag = parse_assuming_ext_taken(tag_rest, input)
435 .map_err(|error| map_err_bytes(error, input))?;
436 if options.is_known_name(tag.parsed.name) || Custom::is_known_name(tag.parsed.name) {
437 match KnownTag::try_from(tag.parsed) {
438 Ok(known_tag) => Ok(ParsedByteSlice {
439 parsed: HlsLine::KnownTag(known_tag),
440 remaining: tag.remaining,
441 }),
442 Err(e) => {
443 tag.parsed.validation_error = Some(e);
444 Ok(ParsedByteSlice {
445 parsed: HlsLine::UnknownTag(tag.parsed),
446 remaining: tag.remaining,
447 })
448 }
449 }
450 } else {
451 Ok(ParsedByteSlice {
452 parsed: HlsLine::UnknownTag(tag.parsed),
453 remaining: tag.remaining,
454 })
455 }
456 } else {
457 let ParsedByteSlice { parsed, remaining } = split_on_new_line(&input[1..]);
458 let comment =
459 std::str::from_utf8(parsed).map_err(|error| map_err_bytes(error, input))?;
460 Ok(ParsedByteSlice {
461 parsed: HlsLine::Comment(Cow::Borrowed(comment)),
462 remaining,
463 })
464 }
465 } else {
466 let ParsedByteSlice { parsed, remaining } = split_on_new_line(input);
467 let uri = std::str::from_utf8(parsed).map_err(|error| map_err_bytes(error, input))?;
468 if uri.is_empty() {
469 Ok(ParsedByteSlice {
470 parsed: HlsLine::Blank,
471 remaining,
472 })
473 } else {
474 Ok(ParsedByteSlice {
475 parsed: HlsLine::Uri(Cow::Borrowed(uri)),
476 remaining,
477 })
478 }
479 }
480}
481
482fn map_err_bytes<E: Into<SyntaxError>>(error: E, input: &[u8]) -> ParseLineBytesError<'_> {
483 let errored_line_slice = split_on_new_line(input);
484 ParseLineBytesError {
485 errored_line_slice,
486 error: error.into(),
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use crate::{
494 config::ParsingOptionsBuilder,
495 error::{ParseTagValueError, ValidationError},
496 tag::{
497 AttributeValue, TagValue,
498 hls::{self, M3u, Start},
499 },
500 };
501 use pretty_assertions::assert_eq;
502
503 #[test]
504 fn uri_line() {
505 assert_eq!(
506 Ok(HlsLine::Uri("hello/world.m3u8".into())),
507 parse("hello/world.m3u8", &ParsingOptions::default()).map(|p| p.parsed)
508 )
509 }
510
511 #[test]
512 fn blank_line() {
513 assert_eq!(
514 Ok(HlsLine::Blank),
515 parse("", &ParsingOptions::default()).map(|p| p.parsed)
516 );
517 }
518
519 #[test]
520 fn comment() {
521 assert_eq!(
522 Ok(HlsLine::Comment("Comment".into())),
523 parse("#Comment", &ParsingOptions::default()).map(|p| p.parsed)
524 );
525 }
526
527 #[test]
528 fn basic_tag() {
529 assert_eq!(
530 Ok(HlsLine::from(hls::Tag::M3u(M3u))),
531 parse("#EXTM3U", &ParsingOptions::default()).map(|p| p.parsed)
532 );
533 }
534
535 #[test]
536 fn custom_tag() {
537 // Set up custom tag
538 #[derive(Debug, PartialEq, Clone)]
539 struct TestTag<'a> {
540 greeting_type: &'a str,
541 message: &'a str,
542 times: u64,
543 score: Option<f64>,
544 }
545 impl<'a> TryFrom<UnknownTag<'a>> for TestTag<'a> {
546 type Error = ValidationError;
547
548 fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
549 let value = tag.value().ok_or(ParseTagValueError::UnexpectedEmpty)?;
550 let list = value.try_as_attribute_list()?;
551 let Some(greeting_type) = list
552 .get("TYPE")
553 .and_then(AttributeValue::unquoted)
554 .and_then(|v| v.try_as_utf_8().ok())
555 else {
556 return Err(ValidationError::MissingRequiredAttribute("TYPE"));
557 };
558 let Some(message) = list.get("MESSAGE").and_then(AttributeValue::quoted) else {
559 return Err(ValidationError::MissingRequiredAttribute("MESSAGE"));
560 };
561 let Some(times) = list
562 .get("TIMES")
563 .and_then(AttributeValue::unquoted)
564 .and_then(|v| v.try_as_decimal_integer().ok())
565 else {
566 return Err(ValidationError::MissingRequiredAttribute("TIMES"));
567 };
568 let score = list
569 .get("SCORE")
570 .and_then(AttributeValue::unquoted)
571 .and_then(|v| v.try_as_decimal_floating_point().ok());
572 Ok(Self {
573 greeting_type,
574 message,
575 times,
576 score,
577 })
578 }
579 }
580 impl CustomTag<'static> for TestTag<'static> {
581 fn is_known_name(name: &str) -> bool {
582 name == "-X-TEST-TAG"
583 }
584 }
585 // Test
586 assert_eq!(
587 Ok(HlsLine::from(CustomTagAccess {
588 custom_tag: TestTag {
589 greeting_type: "GREETING".into(),
590 message: "Hello, World!".into(),
591 times: 42,
592 score: None,
593 },
594 is_dirty: false,
595 original_input: b"#EXT-X-TEST-TAG:TYPE=GREETING,MESSAGE=\"Hello, World!\",TIMES=42"
596 })),
597 parse_with_custom::<TestTag>(
598 "#EXT-X-TEST-TAG:TYPE=GREETING,MESSAGE=\"Hello, World!\",TIMES=42",
599 &ParsingOptions::default()
600 )
601 .map(|p| p.parsed)
602 );
603 }
604
605 #[test]
606 fn avoiding_parsing_known_tag_when_configured_to_avoid_via_parsing_options() {
607 assert_eq!(
608 Ok(HlsLine::from(hls::Tag::Start(
609 Start::builder().with_time_offset(-18.0).finish()
610 ))),
611 parse("#EXT-X-START:TIME-OFFSET=-18", &ParsingOptions::default()).map(|p| p.parsed)
612 );
613 assert_eq!(
614 Ok(HlsLine::UnknownTag(UnknownTag {
615 name: "-X-START",
616 value: Some(TagValue(b"TIME-OFFSET=-18")),
617 original_input: b"#EXT-X-START:TIME-OFFSET=-18",
618 validation_error: None,
619 })),
620 parse(
621 "#EXT-X-START:TIME-OFFSET=-18",
622 &ParsingOptionsBuilder::new()
623 .with_parsing_for_all_tags()
624 .without_parsing_for_start()
625 .build()
626 )
627 .map(|p| p.parsed)
628 );
629 }
630
631 #[test]
632 fn empty_line_before_new_line_break_should_be_parsed_as_blank() {
633 let input = "\n#something else";
634 assert_eq!(
635 ParsedLineSlice {
636 parsed: HlsLine::Blank,
637 remaining: Some("#something else")
638 },
639 parse(input, &ParsingOptions::default()).unwrap()
640 );
641 }
642}