quick_m3u8/tag_internal/known.rs
1//! Types and methods related to associated values of [`Tag`].
2//!
3//! The definitions in this module provide the constructs necessary for both parsing to strongly
4//! typed tags as well as writing when using [`crate::Writer`].
5
6use crate::{
7 error::ValidationError,
8 tag::{UnknownTag, WritableAttributeValue, WritableTagValue, hls},
9 utils::split_on_new_line,
10};
11use std::{borrow::Cow, cmp::PartialEq, fmt::Debug};
12
13/// Represents a HLS tag that is known to the library.
14///
15/// Known tags are split into two cases, those which are defined by the library, and those that are
16/// custom defined by the user. The library makes an effort to reflect in types what is specified
17/// via the latest `draft-pantos-hls-rfc8216` specification. The HLS specification also allows for
18/// unknown tags which are intended to be ignored by clients; however, using that, special custom
19/// implementations can be built up. Some notable examples are the `#EXT-X-SCTE35` tag defined in
20/// [SCTE 35 standard] (which has been superceded by the SCTE35 attributes on `#EXT-X-DATERANGE`),
21/// the `#EXT-X-IMAGE-STREAM-INF` tag (and associated tags) defined via [Roku Developers], the
22/// `#EXT-X-PREFETCH` tag defined by [LHLS], and there are many more. We find that this flexibility
23/// is a useful trait of HLS and so aim to support it here. For use cases where there is no need for
24/// any custom tag parsing, the [`NoCustomTag`] implementation of [`CustomTag`] exists, and is the
25/// default implementation of the generic `Custom` parameter in this enum.
26///
27/// [SCTE 35 standard]: https://account.scte.org/standards/library/catalog/scte-35-digital-program-insertion-cueing-message/
28/// [Roku Developers]: https://developer.roku.com/docs/developer-program/media-playback/trick-mode/hls-and-dash.md#image-media-playlists-for-hls
29/// [LHLS]: https://video-dev.github.io/hlsjs-rfcs/docs/0001-lhls
30#[derive(Debug, PartialEq, Clone)]
31#[allow(clippy::large_enum_variant)]
32pub enum KnownTag<'a, Custom = NoCustomTag>
33where
34 Custom: CustomTag<'a>,
35{
36 // =============================================================================================
37 //
38 // Clippy suggests that the `Tag` within the `Hls` case should be put in a Box, based on
39 // https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
40 // > The largest variant contains at least 272 bytes; Boxing the large field
41 // > (hls::Tag) reduces the total size of the enum.
42 //
43 // However, the description also indicates:
44 // > This lint obviously cannot take the distribution of variants in your running program into
45 // > account. It is possible that the smaller variants make up less than 1% of all instances,
46 // > in which case the overhead is negligible and the boxing is counter-productive. Always
47 // > measure the change this lint suggests.
48 //
49 // In other words, the box only really makes sense, if there is a somewhat even distribution of
50 // instances of each variant. If most instances are going to be the `Hls` case then we aren't
51 // really saving on memory. Furthermore, putting the `Tag` in a `Box` incurrs a performance
52 // penalty (validated with a Criterion bench), because we are now allocating and retrieving from
53 // the heap.
54 //
55 // I believe that the vast majority of cases where the parser is being used we will be using
56 // instances of the `Hls` variant, and therefore, I am not putting the `Tag` in a `Box` and so
57 // ignoring the Clippy warning.
58 //
59 // =============================================================================================
60 /// Indicates that the tag found was one of the 32 known tags defined in the HLS specification
61 /// that are supported here in this library _(and that were not ignored by
62 /// [`crate::config::ParsingOptions`])_. See [`hls::Tag`] for a more complete documentation of
63 /// all of the known tag types.
64 Hls(hls::Tag<'a>),
65 /// Indicates that the tag found was one matching the [`CustomTag`] definition that the user of
66 /// the library has defined. The tag is wrapped in a [`CustomTagAccess`] struct (see that struct
67 /// documentation for reasoning on why the wrapping exists) and can be borrowed via the
68 /// [`CustomTagAccess::as_ref`] method or mutably borrowed via the [`CustomTagAccess::as_mut`]
69 /// method. Refer to [`CustomTag`] for more information on how to define a custom tag.
70 Custom(CustomTagAccess<'a, Custom>),
71}
72
73/// The inner data of a parsed tag.
74///
75/// This struct is primarily useful for the [`crate::Writer`], but can be used outside of writing,
76/// if the user needs to have custom access on the byte-slice content of the tag. The slice the
77/// inner data holds may come from a data source provided during parsing, or may be an owned
78/// `Vec<u8>` if the tag was mutated or constructed using a builder method for the tag. When the
79/// inner data is a byte slice of parsed data, it may be a slice of the rest of the playlist from
80/// where the tag was found; however, the [`Self::value`] method ensures that only the relevant
81/// bytes for this line are provided.
82#[derive(Debug)]
83pub struct TagInner<'a> {
84 pub(crate) output_line: Cow<'a, [u8]>,
85}
86impl<'a> TagInner<'a> {
87 /// Provides the value of the inner data.
88 ///
89 /// The method ensures that only data from this line is provided as the value (even if the slice
90 /// of borrowed data extends past the line until the end of the playlist).
91 pub fn value(&self) -> &[u8] {
92 split_on_new_line(&self.output_line).parsed
93 }
94}
95
96/// The ability to convert self into a [`TagInner`].
97pub trait IntoInnerTag<'a> {
98 /// Consume `self` and provide [`TagInner`].
99 fn into_inner(self) -> TagInner<'a>;
100}
101
102/// Trait to define a custom tag implementation.
103///
104/// The trait comes in two parts:
105/// 1. [`CustomTag::is_known_name`] which allows the library to know whether a tag line (line
106/// prefixed with `#EXT`) should be considered a possible instance of this implementation.
107/// 2. `TryFrom<UnknownTag>` which is where the parsing into the custom tag instance is attempted.
108///
109/// The [`UnknownTag`] struct provides the name of the tag and the value (if it exists), split out
110/// and wrapped in a struct that provides parsing methods for several data types defined in the HLS
111/// specification. The concept here is that when we are converting into our known tag we have the
112/// right context to choose the best parsing method for the tag value type we expect. If we were to
113/// try and parse values up front, then we would run into issues, like trying to distinguish between
114/// an integer and a float if the mantissa (fractional part) is not present. Taking a lazy approach
115/// to parsing helps us avoid these ambiguities, and also, provdies a performance improvement as we
116/// do not waste attempts at parsing data in an unexpected format.
117///
118/// ## Single tag example
119///
120/// Suppose we have a proprietary extension of HLS where we have added the following tag:
121/// ```text
122/// EXT-X-JOKE
123///
124/// The EXT-X-JOKE tag allows a server to provide a joke to the client.
125/// It is OPTIONAL. Its format is:
126///
127/// #EXT-X-JOKE:<attribute-list>
128///
129/// The following attributes are defined:
130///
131/// TYPE
132///
133/// The value is an enumerated-string; valid strings are DAD, PUN,
134/// BAR, STORY, and KNOCK-KNOCK. This attribute is REQUIRED.
135///
136/// JOKE
137///
138/// The value is a quoted-string that includes the contents of the
139/// joke. The value MUST be hilarious. Clients SHOULD reject the joke
140/// if it does not ellicit at least a smile. If the TYPE is DAD, then
141/// the client SHOULD groan on completion of the joke. This attribute
142/// is REQUIRED.
143/// ```
144/// We may choose to model this tag as such (adding the derive attributes for convenience):
145/// ```
146/// #[derive(Debug, PartialEq, Clone)]
147/// struct JokeTag<'a> {
148/// joke_type: JokeType,
149/// joke: &'a str,
150/// }
151///
152/// #[derive(Debug, PartialEq, Clone)]
153/// enum JokeType {
154/// Dad,
155/// Pun,
156/// Bar,
157/// Story,
158/// KnockKnock,
159/// }
160/// ```
161/// The first step we must take is to implement the parsing logic for this tag. To do that we must
162/// implement the `TryFrom<unknown::Tag>` requirement. We may do this as follows:
163/// ```
164/// # use quick_m3u8::{
165/// # tag::{UnknownTag, AttributeValue},
166/// # error::{ValidationError, ParseTagValueError, ParseAttributeValueError}
167/// # };
168/// #
169/// # #[derive(Debug, PartialEq, Clone)]
170/// # struct JokeTag<'a> {
171/// # joke_type: JokeType,
172/// # joke: &'a str,
173/// # }
174/// #
175/// # #[derive(Debug, PartialEq, Clone)]
176/// # enum JokeType {
177/// # Dad,
178/// # Pun,
179/// # Bar,
180/// # Story,
181/// # KnockKnock,
182/// # }
183/// impl<'a> TryFrom<UnknownTag<'a>> for JokeTag<'a> {
184/// type Error = ValidationError;
185///
186/// fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
187/// // Ensure that the value of the tag corresponds to `<attribute-list>`
188/// let list = tag
189/// .value()
190/// .ok_or(ParseTagValueError::UnexpectedEmpty)?
191/// .try_as_attribute_list()?;
192/// // Ensure that the `JOKE` attribute exists and is of the correct type.
193/// let joke = list
194/// .get("JOKE")
195/// .and_then(AttributeValue::quoted)
196/// .ok_or(ValidationError::MissingRequiredAttribute("JOKE"))?;
197/// // Ensure that the `TYPE` attribute exists and is of the correct type. Note the
198/// // difference that this type is `Unquoted` instead of `Quoted`, and so we use the helper
199/// // method `unquoted` rather than `quoted`. This signifies the use of the HLS defined
200/// // `enumerated-string` attribute value type.
201/// let joke_type_str = list
202/// .get("TYPE")
203/// .and_then(AttributeValue::unquoted)
204/// .ok_or(ValidationError::MissingRequiredAttribute("TYPE"))?
205/// .try_as_utf_8()
206/// .map_err(|e| ValidationError::from(
207/// ParseAttributeValueError::Utf8 { attr_name: "TYPE", error: e }
208/// ))?;
209/// // Translate the enumerated string value into the enum cases we support, otherwise,
210/// // return an error.
211/// let Some(joke_type) = (match joke_type_str {
212/// "DAD" => Some(JokeType::Dad),
213/// "PUN" => Some(JokeType::Pun),
214/// "BAR" => Some(JokeType::Bar),
215/// "STORY" => Some(JokeType::Story),
216/// "KNOCK-KNOCK" => Some(JokeType::KnockKnock),
217/// _ => None,
218/// }) else {
219/// return Err(ValidationError::InvalidEnumeratedString);
220/// };
221/// // Now we have our joke.
222/// Ok(Self { joke_type, joke })
223/// }
224/// }
225/// ```
226/// Now we can simply implement the `CustomTag` requirement via the `is_known_name` method. Note
227/// that the tag name is everything after `#EXT` (and before `:`), implying that the `-X-` is
228/// included in the name:
229/// ```
230/// # use quick_m3u8::{
231/// # tag::{CustomTag, UnknownTag, AttributeValue},
232/// # error::{ValidationError, ParseTagValueError, ParseAttributeValueError}
233/// # };
234/// #
235/// # #[derive(Debug, PartialEq, Clone)]
236/// # struct JokeTag<'a> {
237/// # joke_type: JokeType,
238/// # joke: &'a str,
239/// # }
240/// #
241/// # #[derive(Debug, PartialEq, Clone)]
242/// # enum JokeType {
243/// # Dad,
244/// # Pun,
245/// # Bar,
246/// # Story,
247/// # KnockKnock,
248/// # }
249/// # impl<'a> TryFrom<UnknownTag<'a>> for JokeTag<'a> {
250/// # type Error = ValidationError;
251/// #
252/// # fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
253/// # // Ensure that the value of the tag corresponds to `<attribute-list>`
254/// # let list = tag
255/// # .value()
256/// # .ok_or(ParseTagValueError::UnexpectedEmpty)?
257/// # .try_as_attribute_list()?;
258/// # // Ensure that the `JOKE` attribute exists and is of the correct type.
259/// # let joke = list
260/// # .get("JOKE")
261/// # .and_then(AttributeValue::quoted)
262/// # .ok_or(ValidationError::MissingRequiredAttribute("JOKE"))?;
263/// # // Ensure that the `TYPE` attribute exists and is of the correct type. Note the
264/// # // difference that this type is `Unquoted` instead of `Quoted`, and so we use the helper
265/// # // method `unquoted` rather than `quoted`. This signifies the use of the HLS defined
266/// # // `enumerated-string` attribute value type.
267/// # let joke_type_str = list
268/// # .get("TYPE")
269/// # .and_then(AttributeValue::unquoted)
270/// # .ok_or(ValidationError::MissingRequiredAttribute("TYPE"))?
271/// # .try_as_utf_8()
272/// # .map_err(|e| ValidationError::from(
273/// # ParseAttributeValueError::Utf8 { attr_name: "TYPE", error: e }
274/// # ))?;
275/// # // Translate the enumerated string value into the enum cases we support, otherwise,
276/// # // return an error.
277/// # let Some(joke_type) = (match joke_type_str {
278/// # "DAD" => Some(JokeType::Dad),
279/// # "PUN" => Some(JokeType::Pun),
280/// # "BAR" => Some(JokeType::Bar),
281/// # "STORY" => Some(JokeType::Story),
282/// # "KNOCK-KNOCK" => Some(JokeType::KnockKnock),
283/// # _ => None,
284/// # }) else {
285/// # return Err(ValidationError::InvalidEnumeratedString);
286/// # };
287/// # // Now we have our joke.
288/// # Ok(Self { joke_type, joke })
289/// # }
290/// # }
291/// impl<'a> CustomTag<'a> for JokeTag<'a> {
292/// fn is_known_name(name: &str) -> bool {
293/// name == "-X-JOKE"
294/// }
295/// }
296/// ```
297/// At this stage we are ready to use our tag, for example, as part of a [`crate::Reader`]. Below we
298/// include an example playlist string and show parsing of the joke working. Note that we define our
299/// custom tag with the reader using [`std::marker::PhantomData`] and the
300/// [`crate::Reader::with_custom_from_str`] method.
301/// ```
302/// # use quick_m3u8::{
303/// # Reader, HlsLine,
304/// # config::ParsingOptions,
305/// # tag::{
306/// # CustomTag, KnownTag, UnknownTag, AttributeValue,
307/// # hls::{Version, Targetduration, M3u}
308/// # },
309/// # error::{ValidationError, ParseTagValueError, ParseAttributeValueError},
310/// # };
311/// # use std::marker::PhantomData;
312/// #
313/// # #[derive(Debug, PartialEq, Clone)]
314/// # struct JokeTag<'a> {
315/// # joke_type: JokeType,
316/// # joke: &'a str,
317/// # }
318/// #
319/// # #[derive(Debug, PartialEq, Clone)]
320/// # enum JokeType {
321/// # Dad,
322/// # Pun,
323/// # Bar,
324/// # Story,
325/// # KnockKnock,
326/// # }
327/// # impl<'a> TryFrom<UnknownTag<'a>> for JokeTag<'a> {
328/// # type Error = ValidationError;
329/// #
330/// # fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
331/// # // Ensure that the value of the tag corresponds to `<attribute-list>`
332/// # let list = tag
333/// # .value()
334/// # .ok_or(ParseTagValueError::UnexpectedEmpty)?
335/// # .try_as_attribute_list()?;
336/// # // Ensure that the `JOKE` attribute exists and is of the correct type.
337/// # let joke = list
338/// # .get("JOKE")
339/// # .and_then(AttributeValue::quoted)
340/// # .ok_or(ValidationError::MissingRequiredAttribute("JOKE"))?;
341/// # // Ensure that the `TYPE` attribute exists and is of the correct type. Note the
342/// # // difference that this type is `Unquoted` instead of `Quoted`, and so we use the helper
343/// # // method `unquoted` rather than `quoted`. This signifies the use of the HLS defined
344/// # // `enumerated-string` attribute value type.
345/// # let joke_type_str = list
346/// # .get("TYPE")
347/// # .and_then(AttributeValue::unquoted)
348/// # .ok_or(ValidationError::MissingRequiredAttribute("TYPE"))?
349/// # .try_as_utf_8()
350/// # .map_err(|e| ValidationError::from(
351/// # ParseAttributeValueError::Utf8 { attr_name: "TYPE", error: e }
352/// # ))?;
353/// # // Translate the enumerated string value into the enum cases we support, otherwise,
354/// # // return an error.
355/// # let Some(joke_type) = (match joke_type_str {
356/// # "DAD" => Some(JokeType::Dad),
357/// # "PUN" => Some(JokeType::Pun),
358/// # "BAR" => Some(JokeType::Bar),
359/// # "STORY" => Some(JokeType::Story),
360/// # "KNOCK-KNOCK" => Some(JokeType::KnockKnock),
361/// # _ => None,
362/// # }) else {
363/// # return Err(ValidationError::InvalidEnumeratedString);
364/// # };
365/// # // Now we have our joke.
366/// # Ok(Self { joke_type, joke })
367/// # }
368/// # }
369/// # impl<'a> CustomTag<'a> for JokeTag<'a> {
370/// # fn is_known_name(name: &str) -> bool {
371/// # name == "-X-JOKE"
372/// # }
373/// # }
374/// const EXAMPLE: &str = r#"#EXTM3U
375/// #EXT-X-TARGETDURATION:10
376/// #EXT-X-VERSION:3
377/// #EXT-X-JOKE:TYPE=DAD,JOKE="Why did the bicycle fall over? Because it was two-tired!"
378/// # Forgive me, I'm writing this library in my spare time during paternity leave, so this seems
379/// # appropriate to me at this stage.
380/// #EXTINF:9.009
381/// segment.0.ts
382/// "#;
383///
384/// let mut reader = Reader::with_custom_from_str(
385/// EXAMPLE,
386/// ParsingOptions::default(),
387/// PhantomData::<JokeTag>,
388/// );
389/// // First 3 tags as expected
390/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(M3u))));
391/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Targetduration::new(10)))));
392/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Version::new(3)))));
393/// // And the big reveal
394/// match reader.read_line() {
395/// Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
396/// assert_eq!(
397/// &JokeTag {
398/// joke_type: JokeType::Dad,
399/// joke: "Why did the bicycle fall over? Because it was two-tired!",
400/// },
401/// tag.as_ref()
402/// );
403/// }
404/// r => panic!("unexpected result {r:?}"),
405/// }
406/// ```
407///
408/// ## Multiple tag example
409///
410/// The same concepts extend to defining multiple custom tags. For example, in 2018 (before the
411/// standardization of LL-HLS), the good people at JWPlayer and hls.js proposed a new extension of
412/// HLS to support low latency streaming. This proposal was captured in [hlsjs-rfcs-0001]. It added
413/// two tags: `#EXT-X-PREFETCH:<URI>` and `#EXT-X-PREFETCH-DISCONTINUITY`. Below we make an attempt
414/// to implement these as custom tags. We don't break for commentary as most of this was explained
415/// in the example above. This example was chosen as the defined tag values are not `attribute-list`
416/// and so we can demonstrate different tag parsing techniques.
417/// ```
418/// # use quick_m3u8::{HlsLine, Reader, config::ParsingOptions, tag::KnownTag, tag::hls::{M3u,
419/// # Version, Targetduration, MediaSequence, DiscontinuitySequence, Inf, ProgramDateTime},
420/// # date_time, tag::CustomTag, error::{ValidationError, ParseTagValueError}, tag::UnknownTag,
421/// # tag::TagValue};
422/// # use std::marker::PhantomData;
423/// #[derive(Debug, PartialEq, Clone)]
424/// enum LHlsTag<'a> {
425/// Discontinuity,
426/// Prefetch(&'a str),
427/// }
428///
429/// impl<'a> LHlsTag<'a> {
430/// fn try_from_discontinuity(value: Option<TagValue>) -> Result<Self, ValidationError> {
431/// match value {
432/// Some(_) => Err(ValidationError::from(ParseTagValueError::NotEmpty)),
433/// None => Ok(Self::Discontinuity)
434/// }
435/// }
436///
437/// fn try_from_prefetch(value: Option<TagValue<'a>>) -> Result<Self, ValidationError> {
438/// // Note that the `TagValue` provides methods for parsing value data as defined in the
439/// // HLS specification, as extracted from the existing tag definitions (there is specific
440/// // definition for possible attribute-list value types; however, for general tag values,
441/// // this has to be inferred from what tags are defined). `TagValue` does not provide a
442/// // `try_as_utf_8` method, since the only tag that defines a text value is the
443/// // `EXT-X-PLAYLIST-TYPE` tag, but this is an enumerated string (`EVENT` or `VOD`), and
444/// // so we just offer `try_as_playlist_type`. Nevertheless, the inner data of `TagValue`
445/// // is accessible, and so we can convert to UTF-8 ourselves here, as shown below.
446/// let unparsed = value.ok_or(ParseTagValueError::UnexpectedEmpty)?;
447/// let Ok(uri) = std::str::from_utf8(unparsed.0) else {
448/// return Err(ValidationError::MissingRequiredAttribute("<URI>"));
449/// };
450/// Ok(Self::Prefetch(uri))
451/// }
452/// }
453///
454/// impl<'a> TryFrom<UnknownTag<'a>> for LHlsTag<'a> {
455/// type Error = ValidationError;
456///
457/// fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
458/// match tag.name() {
459/// "-X-PREFETCH-DISCONTINUITY" => Self::try_from_discontinuity(tag.value()),
460/// "-X-PREFETCH" => Self::try_from_prefetch(tag.value()),
461/// _ => Err(ValidationError::UnexpectedTagName),
462/// }
463/// }
464/// }
465///
466/// impl<'a> CustomTag<'a> for LHlsTag<'a> {
467/// fn is_known_name(name: &str) -> bool {
468/// name == "-X-PREFETCH" || name == "-X-PREFETCH-DISCONTINUITY"
469/// }
470/// }
471/// // This example is taken from the "Examples" section under the "Discontinuities" example.
472/// const EXAMPLE: &str = r#"#EXTM3U
473/// #EXT-X-VERSION:3
474/// #EXT-X-TARGETDURATION:2
475/// #EXT-X-MEDIA-SEQUENCE:0
476/// #EXT-X-DISCONTINUITY-SEQUENCE:0
477///
478/// #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z
479/// #EXTINF:2.000
480/// https://foo.com/bar/0.ts
481/// #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z
482/// #EXTINF:2.000
483/// https://foo.com/bar/1.ts
484///
485/// #EXT-X-PREFETCH-DISCONTINUITY
486/// #EXT-X-PREFETCH:https://foo.com/bar/5.ts
487/// #EXT-X-PREFETCH:https://foo.com/bar/6.ts"#;
488///
489/// let mut reader = Reader::with_custom_from_str(
490/// EXAMPLE,
491/// ParsingOptions::default(),
492/// PhantomData::<LHlsTag>,
493/// );
494///
495/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(M3u))));
496/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Version::new(3)))));
497/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Targetduration::new(2)))));
498/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(MediaSequence::new(0)))));
499/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(DiscontinuitySequence::new(0)))));
500/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::Blank)));
501/// assert_eq!(
502/// reader.read_line(),
503/// Ok(Some(HlsLine::from(ProgramDateTime::new(
504/// date_time!(2018-09-05 T 20:59:06.531)
505/// ))))
506/// );
507/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Inf::new(2.0, "")))));
508/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::Uri("https://foo.com/bar/0.ts".into()))));
509/// assert_eq!(
510/// reader.read_line(),
511/// Ok(Some(HlsLine::from(ProgramDateTime::new(
512/// date_time!(2018-09-05 T 20:59:08.531)
513/// ))))
514/// );
515/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Inf::new(2.0, "")))));
516/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::Uri("https://foo.com/bar/1.ts".into()))));
517/// assert_eq!(reader.read_line(), Ok(Some(HlsLine::Blank)));
518///
519/// match reader.read_line() {
520/// Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
521/// assert_eq!(&LHlsTag::Discontinuity, tag.as_ref());
522/// }
523/// r => panic!("unexpected result {r:?}"),
524/// }
525/// match reader.read_line() {
526/// Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
527/// assert_eq!(&LHlsTag::Prefetch("https://foo.com/bar/5.ts"), tag.as_ref());
528/// }
529/// r => panic!("unexpected result {r:?}"),
530/// }
531/// match reader.read_line() {
532/// Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
533/// assert_eq!(&LHlsTag::Prefetch("https://foo.com/bar/6.ts"), tag.as_ref());
534/// }
535/// r => panic!("unexpected result {r:?}"),
536/// }
537///
538/// assert_eq!(reader.read_line(), Ok(None)); // end of example
539/// ```
540///
541/// [hlsjs-rfcs-0001]: https://video-dev.github.io/hlsjs-rfcs/docs/0001-lhls
542pub trait CustomTag<'a>:
543 TryFrom<UnknownTag<'a>, Error = ValidationError> + Debug + PartialEq
544{
545 /// Check if the provided name is known for this custom tag implementation.
546 ///
547 /// This method is called before any attempt to parse the data into a CustomTag (it is the test
548 /// for whether an attempt will be made to parse to CustomTag).
549 fn is_known_name(name: &str) -> bool;
550}
551/// A custom tag implementation that allows for writing using [`crate::Writer`].
552///
553/// If there is no intention to write the parsed data then this trait does not need to be
554/// implemented for the [`CustomTag`]. We can extend the examples from [`CustomTag`] to also
555/// implement this trait so that we can demonstrate writing the data to an output.
556///
557/// ## Single tag example
558///
559/// Recall that the single tag example was for the custom defined `#EXT-X-JOKE` tag. Here we show
560/// how we may change the joke (e.g. if we are acting as a proxy) before writing to output. Note, in
561/// a real implementation we would make the stored property a [`std::borrow::Cow`] and not require
562/// the user to provide a string slice reference with the same lifetime as the parsed data, but this
563/// is just extending an existing example for information purposes.
564/// ```
565/// # use quick_m3u8::{
566/// # Reader, HlsLine, Writer,
567/// # config::ParsingOptions,
568/// # tag::{
569/// # CustomTag, KnownTag, WritableTag, WritableCustomTag, UnknownTag, WritableTagValue,
570/// # WritableAttributeValue, AttributeValue,
571/// # hls::{Version, Targetduration, M3u}
572/// # },
573/// # error::{ValidationError, ParseTagValueError, ParseAttributeValueError}
574/// # };
575/// # use std::{marker::PhantomData, borrow::Cow, collections::HashMap};
576/// #
577/// # #[derive(Debug, PartialEq, Clone)]
578/// # struct JokeTag<'a> {
579/// # joke_type: JokeType,
580/// # joke: &'a str,
581/// # }
582/// #
583/// # #[derive(Debug, PartialEq, Clone)]
584/// # enum JokeType {
585/// # Dad,
586/// # Pun,
587/// # Bar,
588/// # Story,
589/// # KnockKnock,
590/// # }
591/// # impl<'a> TryFrom<UnknownTag<'a>> for JokeTag<'a> {
592/// # type Error = ValidationError;
593/// #
594/// # fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
595/// # // Ensure that the value of the tag corresponds to `<attribute-list>`
596/// # let list = tag
597/// # .value()
598/// # .ok_or(ParseTagValueError::UnexpectedEmpty)?
599/// # .try_as_attribute_list()?;
600/// # // Ensure that the `JOKE` attribute exists and is of the correct type.
601/// # let joke = list
602/// # .get("JOKE")
603/// # .and_then(AttributeValue::quoted)
604/// # .ok_or(ValidationError::MissingRequiredAttribute("JOKE"))?;
605/// # // Ensure that the `TYPE` attribute exists and is of the correct type. Note the
606/// # // difference that this type is `Unquoted` instead of `Quoted`, and so we use the helper
607/// # // method `unquoted` rather than `quoted`. This signifies the use of the HLS defined
608/// # // `enumerated-string` attribute value type.
609/// # let joke_type_str = list
610/// # .get("TYPE")
611/// # .and_then(AttributeValue::unquoted)
612/// # .ok_or(ValidationError::MissingRequiredAttribute("TYPE"))?
613/// # .try_as_utf_8()
614/// # .map_err(|e| ValidationError::from(
615/// # ParseAttributeValueError::Utf8 { attr_name: "TYPE", error: e }
616/// # ))?;
617/// # // Translate the enumerated string value into the enum cases we support, otherwise,
618/// # // return an error.
619/// # let Some(joke_type) = (match joke_type_str {
620/// # "DAD" => Some(JokeType::Dad),
621/// # "PUN" => Some(JokeType::Pun),
622/// # "BAR" => Some(JokeType::Bar),
623/// # "STORY" => Some(JokeType::Story),
624/// # "KNOCK-KNOCK" => Some(JokeType::KnockKnock),
625/// # _ => None,
626/// # }) else {
627/// # return Err(ValidationError::InvalidEnumeratedString);
628/// # };
629/// # // Now we have our joke.
630/// # Ok(Self { joke_type, joke })
631/// # }
632/// # }
633/// # impl<'a> CustomTag<'a> for JokeTag<'a> {
634/// # fn is_known_name(name: &str) -> bool {
635/// # name == "-X-JOKE"
636/// # }
637/// # }
638/// impl JokeType {
639/// fn as_str(self) -> &'static str {
640/// match self {
641/// JokeType::Dad => "DAD",
642/// JokeType::Pun => "PUN",
643/// JokeType::Bar => "BAR",
644/// JokeType::Story => "STORY",
645/// JokeType::KnockKnock => "KNOCK-KNOCK",
646/// }
647/// }
648/// }
649/// impl<'a> JokeTag<'a> {
650/// fn set_joke(&mut self, joke: &'static str) {
651/// self.joke = joke;
652/// }
653/// }
654/// impl<'a> WritableCustomTag<'a> for JokeTag<'a> {
655/// fn into_writable_tag(self) -> WritableTag<'a> {
656/// // Note, that the `WritableTag` expects to have `name: Cow<'a, str>` and
657/// // `value: WritableTagValue<'a>`; however, the `new` method accepts
658/// // `impl Into<Cow<'a, str>>` for name, and `impl Into<WritableTagValue<'a>>` for value.
659/// // The library provides convenience `From<T>` implementations for many types of `T` to
660/// // `WritableTagValue`, so this may help in some cases with shortening how much needs to
661/// // be written. Below we make use of `From<[(K, V); N]>` where `const N: usize`,
662/// // `K: Into<Cow<'a, str>>`, and `V: Into<WritableAttributeValue>`.
663/// WritableTag::new(
664/// "-X-JOKE",
665/// [
666/// (
667/// "TYPE",
668/// WritableAttributeValue::UnquotedString(self.joke_type.as_str().into()),
669/// ),
670/// (
671/// "JOKE",
672/// WritableAttributeValue::QuotedString(self.joke.into()),
673/// ),
674/// ],
675/// )
676/// }
677/// }
678/// # const EXAMPLE: &str = r#"#EXTM3U
679/// # #EXT-X-TARGETDURATION:10
680/// # #EXT-X-VERSION:3
681/// # #EXT-X-JOKE:TYPE=DAD,JOKE="Why did the bicycle fall over? Because it was two-tired!"
682/// # #EXTINF:9.009
683/// # segment.0.ts
684/// # "#;
685/// #
686/// # let mut reader = Reader::with_custom_from_str(
687/// # EXAMPLE,
688/// # ParsingOptions::default(),
689/// # PhantomData::<JokeTag>,
690/// # );
691/// let mut writer = Writer::new(Vec::new());
692/// // First 3 tags as expected
693/// let Some(m3u) = reader.read_line()? else { return Ok(()) };
694/// writer.write_custom_line(m3u)?;
695/// let Some(targetduration) = reader.read_line()? else { return Ok(()) };
696/// writer.write_custom_line(targetduration)?;
697/// let Some(version) = reader.read_line()? else { return Ok(()) };
698/// writer.write_custom_line(version)?;
699/// // And the big reveal
700/// match reader.read_line() {
701/// Ok(Some(HlsLine::KnownTag(KnownTag::Custom(mut tag)))) => {
702/// tag.as_mut().set_joke("What happens when a frog's car breaks down? It gets toad!");
703/// writer.write_custom_line(HlsLine::from(tag))?;
704/// }
705/// r => panic!("unexpected result {r:?}"),
706/// }
707///
708/// // Because the HashMap we return does not guarantee order of the attributes, we validate that
709/// // the result is one of the expected outcomes.
710/// const EXPECTED_1: &str = r#"#EXTM3U
711/// #EXT-X-TARGETDURATION:10
712/// #EXT-X-VERSION:3
713/// #EXT-X-JOKE:TYPE=DAD,JOKE="What happens when a frog's car breaks down? It gets toad!"
714/// "#;
715/// const EXPECTED_2: &str = r#"#EXTM3U
716/// #EXT-X-TARGETDURATION:10
717/// #EXT-X-VERSION:3
718/// #EXT-X-JOKE:JOKE="What happens when a frog's car breaks down? It gets toad!",TYPE=DAD
719/// "#;
720/// let inner_bytes = writer.into_inner();
721/// let actual = std::str::from_utf8(&inner_bytes)?;
722/// assert!(actual == EXPECTED_1 || actual == EXPECTED_2);
723/// # Ok::<(), Box<dyn std::error::Error>>(())
724/// ```
725///
726/// ## Multiple tag example
727///
728/// Recall that the multiple tag example was for the [LHLS] extension to the specification. Here we
729/// show how we may change the prefetch URL (e.g. if we are acting as a proxy) before writing to
730/// output.
731/// ```
732/// # use quick_m3u8::{HlsLine, Reader, config::ParsingOptions, tag::KnownTag, tag::hls::{M3u,
733/// # Version, Targetduration, MediaSequence, DiscontinuitySequence, Inf, ProgramDateTime},
734/// # date_time, tag::CustomTag, error::{ValidationError, ParseTagValueError}, tag::UnknownTag,
735/// # tag::{WritableCustomTag, WritableTag, TagValue, WritableTagValue}, Writer};
736/// # use std::{marker::PhantomData, io::Write};
737/// #[derive(Debug, PartialEq, Clone)]
738/// # enum LHlsTag<'a> {
739/// # Discontinuity,
740/// # Prefetch(&'a str),
741/// # }
742/// #
743/// # impl<'a> LHlsTag<'a> {
744/// # fn try_from_discontinuity(value: Option<TagValue>) -> Result<Self, ValidationError> {
745/// # match value {
746/// # Some(_) => Err(ValidationError::from(ParseTagValueError::NotEmpty)),
747/// # None => Ok(Self::Discontinuity)
748/// # }
749/// # }
750/// #
751/// # fn try_from_prefetch(value: Option<TagValue<'a>>) -> Result<Self, ValidationError> {
752/// # // Note that the `TagValue` provides methods for parsing value data as defined in the
753/// # // HLS specification, as extracted from the existing tag definitions (there is specific
754/// # // definition for possible attribute-list value types; however, for general tag values,
755/// # // this has to be inferred from what tags are defined). `TagValue` does not provide a
756/// # // `try_as_utf_8` method, since the only tag that defines a text value is the
757/// # // `EXT-X-PLAYLIST-TYPE` tag, but this is an enumerated string (`EVENT` or `VOD`), and
758/// # // so we just offer `try_as_playlist_type`. Nevertheless, the inner data of `TagValue`
759/// # // is accessible, and so we can convert to UTF-8 ourselves here, as shown below.
760/// # let unparsed = value.ok_or(ParseTagValueError::UnexpectedEmpty)?;
761/// # let Ok(uri) = std::str::from_utf8(unparsed.0) else {
762/// # return Err(ValidationError::MissingRequiredAttribute("<URI>"));
763/// # };
764/// # Ok(Self::Prefetch(uri))
765/// # }
766/// # }
767/// #
768/// # impl<'a> TryFrom<UnknownTag<'a>> for LHlsTag<'a> {
769/// # type Error = ValidationError;
770/// #
771/// # fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
772/// # match tag.name() {
773/// # "-X-PREFETCH-DISCONTINUITY" => Self::try_from_discontinuity(tag.value()),
774/// # "-X-PREFETCH" => Self::try_from_prefetch(tag.value()),
775/// # _ => Err(ValidationError::UnexpectedTagName),
776/// # }
777/// # }
778/// # }
779/// #
780/// # impl<'a> CustomTag<'a> for LHlsTag<'a> {
781/// # fn is_known_name(name: &str) -> bool {
782/// # name == "-X-PREFETCH" || name == "-X-PREFETCH-DISCONTINUITY"
783/// # }
784/// # }
785/// impl<'a> WritableCustomTag<'a> for LHlsTag<'a> {
786/// fn into_writable_tag(self) -> WritableTag<'a> {
787/// // Note, as mentioned above, the `WritableTag::new` method accepts types that implement
788/// // `Into` the stored properties on the struct. Below we make use of `From<&str>` for
789/// // `WritableTagValue` in the `Prefetch` case to cut down on boilerplate.
790/// match self {
791/// Self::Discontinuity => WritableTag::new(
792/// "-X-PREFETCH-DISCONTINUITY",
793/// WritableTagValue::Empty
794/// ),
795/// Self::Prefetch(uri) => WritableTag::new("-X-PREFETCH", uri),
796/// }
797/// }
798/// }
799/// # // This example is taken from the "Examples" section under the "Discontinuities" example.
800/// # const EXAMPLE: &str = r#"#EXTM3U
801/// # #EXT-X-VERSION:3
802/// # #EXT-X-TARGETDURATION:2
803/// # #EXT-X-MEDIA-SEQUENCE:0
804/// # #EXT-X-DISCONTINUITY-SEQUENCE:0
805/// #
806/// # #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z
807/// # #EXTINF:2.000
808/// # https://foo.com/bar/0.ts
809/// # #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z
810/// # #EXTINF:2.000
811/// # https://foo.com/bar/1.ts
812/// #
813/// # #EXT-X-PREFETCH-DISCONTINUITY
814/// # #EXT-X-PREFETCH:https://foo.com/bar/5.ts
815/// # #EXT-X-PREFETCH:https://foo.com/bar/6.ts"#;
816/// #
817/// # let mut reader = Reader::with_custom_from_str(
818/// # EXAMPLE,
819/// # ParsingOptions::default(),
820/// # PhantomData::<LHlsTag>,
821/// # );
822///
823/// let mut writer = Writer::new(Vec::new());
824/// let mut last_segment_index = 0;
825/// loop {
826/// match reader.read_line() {
827/// Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
828/// match tag.as_ref() {
829/// LHlsTag::Discontinuity => {
830/// writer.write_custom_line(HlsLine::from(tag))?;
831/// }
832/// LHlsTag::Prefetch(uri) => {
833/// // For demo purposes we make the URI segment numbers sequential.
834/// if let Some(last_component) = uri.split('/').last() {
835/// let new_uri = uri.replace(
836/// last_component,
837/// format!("{}.ts", last_segment_index + 1).as_str()
838/// );
839/// writer.write_custom_tag(LHlsTag::Prefetch(new_uri.as_str()))?;
840/// } else {
841/// writer.write_custom_line(HlsLine::from(tag))?;
842/// }
843/// }
844/// };
845/// }
846/// Ok(Some(HlsLine::Uri(uri))) => {
847/// last_segment_index = uri
848/// .split('/')
849/// .last()
850/// .and_then(|file| file.split('.').next())
851/// .and_then(|n| n.parse::<u32>().ok())
852/// .unwrap_or_default();
853/// writer.write_line(HlsLine::Uri(uri))?;
854/// }
855/// Ok(Some(line)) => {
856/// writer.write_custom_line(line)?;
857/// }
858/// Ok(None) => break,
859/// Err(e) => {
860/// writer.get_mut().write_all(e.errored_line.as_bytes())?;
861/// }
862/// }
863/// }
864/// const EXPECTED: &str = r#"#EXTM3U
865/// #EXT-X-VERSION:3
866/// #EXT-X-TARGETDURATION:2
867/// #EXT-X-MEDIA-SEQUENCE:0
868/// #EXT-X-DISCONTINUITY-SEQUENCE:0
869///
870/// #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z
871/// #EXTINF:2.000
872/// https://foo.com/bar/0.ts
873/// #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z
874/// #EXTINF:2.000
875/// https://foo.com/bar/1.ts
876///
877/// #EXT-X-PREFETCH-DISCONTINUITY
878/// #EXT-X-PREFETCH:https://foo.com/bar/2.ts
879/// #EXT-X-PREFETCH:https://foo.com/bar/3.ts
880/// "#;
881/// # Ok::<(), Box<dyn std::error::Error>>(())
882/// ```
883///
884/// [LHLS]: https://video-dev.github.io/hlsjs-rfcs/docs/0001-lhls
885pub trait WritableCustomTag<'a>: CustomTag<'a> {
886 /// Takes ownership of the custom tag and provides a value that is used for writing.
887 ///
888 /// This method is only called if there was a mutable borrow of the custom tag at some stage. If
889 /// the tag was never mutably borrowed, then when writing, the library will use the original
890 /// input data (thus avoiding unnecessary allocations).
891 fn into_writable_tag(self) -> WritableTag<'a>;
892}
893
894/// Wrapper around a [`CustomTag`] implementation for access control.
895///
896/// The wrapper allows the library to selectively decide when it will call the
897/// [`WritableCustomTag::into_writable_tag`] method. When there has been no mutable reference borrow
898/// of the custom tag ([`Self::as_mut`]) then the [`Self::into_inner`] implementation will use the
899/// original parsed byte-slice directly (rather than allocate any new strings to construct a new
900/// line).
901#[derive(Debug, PartialEq, Clone)]
902pub struct CustomTagAccess<'a, Custom>
903where
904 Custom: CustomTag<'a>,
905{
906 pub(crate) custom_tag: Custom,
907 pub(crate) is_dirty: bool,
908 pub(crate) original_input: &'a [u8],
909}
910
911impl<'a, Custom> TryFrom<UnknownTag<'a>> for CustomTagAccess<'a, Custom>
912where
913 Custom: CustomTag<'a>,
914{
915 type Error = ValidationError;
916
917 fn try_from(value: UnknownTag<'a>) -> Result<Self, Self::Error> {
918 let original_input = value.original_input;
919 let custom_tag = Custom::try_from(value)?;
920 Ok(Self {
921 custom_tag,
922 is_dirty: false,
923 original_input,
924 })
925 }
926}
927impl<'a, Custom> AsRef<Custom> for CustomTagAccess<'a, Custom>
928where
929 Custom: CustomTag<'a>,
930{
931 fn as_ref(&self) -> &Custom {
932 &self.custom_tag
933 }
934}
935impl<'a, Custom> AsMut<Custom> for CustomTagAccess<'a, Custom>
936where
937 Custom: CustomTag<'a>,
938{
939 fn as_mut(&mut self) -> &mut Custom {
940 self.is_dirty = true;
941 &mut self.custom_tag
942 }
943}
944
945impl<'a, Custom> IntoInnerTag<'a> for CustomTagAccess<'a, Custom>
946where
947 Custom: WritableCustomTag<'a>,
948{
949 fn into_inner(self) -> TagInner<'a> {
950 if self.is_dirty {
951 self.custom_tag.into_inner()
952 } else {
953 TagInner {
954 output_line: Cow::Borrowed(self.original_input),
955 }
956 }
957 }
958}
959
960impl<'a, Custom> IntoInnerTag<'a> for Custom
961where
962 Custom: WritableCustomTag<'a>,
963{
964 fn into_inner(self) -> TagInner<'a> {
965 let output = calculate_output(self);
966 TagInner {
967 output_line: Cow::Owned(output.into_bytes()),
968 }
969 }
970}
971
972pub(crate) fn calculate_output<'a, Custom: WritableCustomTag<'a>>(custom_tag: Custom) -> String {
973 let tag = custom_tag.into_writable_tag();
974 match tag.value {
975 WritableTagValue::Empty => format!("#EXT{}", tag.name),
976 WritableTagValue::DecimalFloatingPointWithOptionalTitle(n, t) => {
977 if t.is_empty() {
978 format!("#EXT{}:{n}", tag.name)
979 } else {
980 format!("#EXT{}:{n},{t}", tag.name)
981 }
982 }
983 WritableTagValue::DecimalInteger(n) => format!("#EXT{}:{n}", tag.name),
984 WritableTagValue::DecimalIntegerRange(n, Some(o)) => format!("#EXT{}:{n}@{o}", tag.name),
985 WritableTagValue::DecimalIntegerRange(n, None) => format!("#EXT{}:{n}", tag.name),
986 WritableTagValue::DateTime(d) => format!("#EXT{}:{d}", tag.name),
987 WritableTagValue::AttributeList(list) => {
988 let attrs = list
989 .iter()
990 .map(|(k, v)| match v {
991 WritableAttributeValue::DecimalInteger(n) => format!("{k}={n}"),
992 WritableAttributeValue::SignedDecimalFloatingPoint(n) => {
993 format!("{k}={n:?}")
994 }
995 WritableAttributeValue::DecimalResolution(r) => {
996 format!("{k}={}x{}", r.width, r.height)
997 }
998 WritableAttributeValue::QuotedString(s) => format!("{k}=\"{s}\""),
999 WritableAttributeValue::UnquotedString(s) => format!("{k}={s}"),
1000 })
1001 .collect::<Vec<String>>();
1002 let value = attrs.join(",");
1003 format!("#EXT{}:{}", tag.name, value)
1004 }
1005 WritableTagValue::Utf8(s) => format!("#EXT{}:{s}", tag.name),
1006 }
1007}
1008
1009/// A tag representation that makes writing from custom tags easier.
1010///
1011/// This is provided so that custom tag implementations may provide an output that does not depend
1012/// on having parsed data to derive the write output from. This helps with mutability as well as
1013/// allowing for custom tags to be constructed from scratch (without being parsed from source data).
1014#[derive(Debug, PartialEq)]
1015pub struct WritableTag<'a> {
1016 /// The name of the tag.
1017 ///
1018 /// This must include everything after the `#EXT` prefix and before the `:` or new line. For
1019 /// example, `#EXTM3U` has name `M3U`, `#EXT-X-VERSION:3` has name `-X-VERSION`, etc.
1020 pub name: Cow<'a, str>,
1021 /// The value of the tag.
1022 ///
1023 /// The [`WritableTagValue`] provides data types that allow for owned data (rather than just
1024 /// borrowed references from parsed input data). See the enum documentation for more information
1025 /// on what values can be defined.
1026 pub value: WritableTagValue<'a>,
1027}
1028impl<'a> WritableTag<'a> {
1029 /// Create a new tag.
1030 ///
1031 /// ## Examples
1032 /// ### Empty
1033 /// ```
1034 /// # use quick_m3u8::tag::{WritableTag, WritableTagValue};
1035 /// WritableTag::new("-X-EXAMPLE", WritableTagValue::Empty);
1036 /// ```
1037 /// produces a tag that would write as `#EXT-X-EXAMPLE`.
1038 /// ### Integer
1039 /// ```
1040 /// # use quick_m3u8::tag::{WritableTag, WritableTagValue};
1041 /// # use std::borrow::Cow;
1042 /// let explicit = WritableTag::new(
1043 /// Cow::Borrowed("-X-EXAMPLE"),
1044 /// WritableTagValue::DecimalInteger(42),
1045 /// );
1046 /// // Or, with convenience `From<u64>`
1047 /// let terse = WritableTag::new("-X-EXAMPLE", 42);
1048 /// assert_eq!(explicit, terse);
1049 /// ```
1050 /// produces a tag that would write as `#EXT-X-EXAMPLE:42`.
1051 /// ### Integer range
1052 /// ```
1053 /// # use quick_m3u8::tag::{WritableTag, WritableTagValue};
1054 /// # use std::borrow::Cow;
1055 /// let explicit = WritableTag::new(
1056 /// Cow::Borrowed("-X-EXAMPLE"),
1057 /// WritableTagValue::DecimalIntegerRange(1024, Some(512)),
1058 /// );
1059 /// // Or, with convenience `From<(u64, Option<u64>)>`
1060 /// let terse = WritableTag::new("-X-EXAMPLE", (1024, Some(512)));
1061 /// assert_eq!(explicit, terse);
1062 /// ```
1063 /// produces a tag that would write as `#EXT-X-EXAMPLE:1024@512`.
1064 /// ### Float with title
1065 /// ```
1066 /// # use quick_m3u8::tag::{WritableTag, WritableTagValue};
1067 /// # use std::borrow::Cow;
1068 /// let explicit = WritableTag::new(
1069 /// Cow::Borrowed("-X-EXAMPLE"),
1070 /// WritableTagValue::DecimalFloatingPointWithOptionalTitle(3.14, "pi".into()),
1071 /// );
1072 /// // Or, with convenience `From<(f64, impl Into<Cow<str>>)>`
1073 /// let terse = WritableTag::new("-X-EXAMPLE", (3.14, "pi"));
1074 /// assert_eq!(explicit, terse);
1075 /// ```
1076 /// produces a tag that would write as `#EXT-X-EXAMPLE:3.14,pi`.
1077 /// ### Date time
1078 /// ```
1079 /// # use quick_m3u8::date_time;
1080 /// # use quick_m3u8::tag::{WritableTag, WritableTagValue};
1081 /// # use std::borrow::Cow;
1082 /// let explicit = WritableTag::new(
1083 /// Cow::Borrowed("-X-EXAMPLE"),
1084 /// WritableTagValue::DateTime(date_time!(2025-08-10 T 21:51:42.123 -05:00)),
1085 /// );
1086 /// // Or, with convenience `From<DateTime>`
1087 /// let terse = WritableTag::new("-X-EXAMPLE", date_time!(2025-08-10 T 21:51:42.123 -05:00));
1088 /// assert_eq!(explicit, terse);
1089 /// ```
1090 /// produces a tag that would write as `#EXT-X-EXAMPLE:2025-08-10T21:51:42.123-05:00`.
1091 /// ### Attribute list
1092 /// ```
1093 /// # use quick_m3u8::tag::{WritableTag, WritableTagValue, WritableAttributeValue};
1094 /// # use std::collections::HashMap;
1095 /// # use std::borrow::Cow;
1096 /// let explicit = WritableTag::new(
1097 /// Cow::Borrowed("-X-EXAMPLE"),
1098 /// WritableTagValue::AttributeList(HashMap::from([
1099 /// ("VALUE".into(), WritableAttributeValue::DecimalInteger(42)),
1100 /// ])),
1101 /// );
1102 /// // Or, with convenience `From<[(K, V); N]>`
1103 /// let terse = WritableTag::new("-X-EXAMPLE", [("VALUE", 42)]);
1104 /// assert_eq!(explicit, terse);
1105 /// ```
1106 /// produces a tag that would write as `#EXT-X-EXAMPLE:VALUE=42`.
1107 /// ### UTF-8
1108 /// ```
1109 /// # use quick_m3u8::tag::{WritableTag, WritableTagValue, WritableAttributeValue};
1110 /// # use std::borrow::Cow;
1111 /// let explicit = WritableTag::new(
1112 /// Cow::Borrowed("-X-EXAMPLE"),
1113 /// WritableTagValue::Utf8(Cow::Borrowed("HELLO")),
1114 /// );
1115 /// // Or, with convenience `From<&str>`
1116 /// let terse = WritableTag::new("-X-EXAMPLE", "HELLO");
1117 /// assert_eq!(explicit, terse);
1118 /// ```
1119 /// produces a tag that would write as `#EXT-X-EXAMPLE:HELLO`.
1120 pub fn new(name: impl Into<Cow<'a, str>>, value: impl Into<WritableTagValue<'a>>) -> Self {
1121 Self {
1122 name: name.into(),
1123 value: value.into(),
1124 }
1125 }
1126}
1127
1128/// Implementation of [`CustomTag`] for convenience default `HlsLine::Custom` implementation.
1129///
1130/// Given that `HlsLine` takes a generic parameter, if this struct did not exist, then the user
1131/// would always have to define some custom tag implementation to use the library. This would add
1132/// unintended complexity. Therefore, this struct comes with the library, and provides the default
1133/// implementation of `CustomTag`. This implementation ensures that it is never parsed from source
1134/// data, because [`Self::is_known_name`] always returns false.
1135#[derive(Debug, PartialEq, Clone, Copy)]
1136pub struct NoCustomTag;
1137impl TryFrom<UnknownTag<'_>> for NoCustomTag {
1138 type Error = ValidationError;
1139
1140 fn try_from(_: UnknownTag) -> Result<Self, Self::Error> {
1141 Err(ValidationError::NotImplemented)
1142 }
1143}
1144impl CustomTag<'_> for NoCustomTag {
1145 fn is_known_name(_: &str) -> bool {
1146 false
1147 }
1148}
1149impl WritableCustomTag<'_> for NoCustomTag {
1150 fn into_writable_tag(self) -> WritableTag<'static> {
1151 WritableTag::new("-NO-TAG", WritableTagValue::Empty)
1152 }
1153}
1154
1155impl<'a, Custom> TryFrom<UnknownTag<'a>> for KnownTag<'a, Custom>
1156where
1157 Custom: CustomTag<'a>,
1158{
1159 type Error = ValidationError;
1160
1161 fn try_from(tag: UnknownTag<'a>) -> Result<Self, Self::Error> {
1162 if Custom::is_known_name(tag.name) {
1163 let original_input = tag.original_input;
1164 let custom_tag = Custom::try_from(tag)?;
1165 Ok(Self::Custom(CustomTagAccess {
1166 custom_tag,
1167 is_dirty: false,
1168 original_input,
1169 }))
1170 } else {
1171 Ok(Self::Hls(hls::Tag::try_from(tag)?))
1172 }
1173 }
1174}
1175
1176impl<'a, Custom> IntoInnerTag<'a> for KnownTag<'a, Custom>
1177where
1178 Custom: WritableCustomTag<'a>,
1179{
1180 fn into_inner(self) -> TagInner<'a> {
1181 match self {
1182 KnownTag::Hls(tag) => tag.into_inner(),
1183 KnownTag::Custom(tag) => tag.into_inner(),
1184 }
1185 }
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190 use super::*;
1191 use crate::{
1192 Reader, Writer, config::ParsingOptions, error::ParseTagValueError, line::HlsLine,
1193 tag::AttributeValue,
1194 };
1195 use pretty_assertions::assert_eq;
1196 use std::marker::PhantomData;
1197
1198 #[derive(Debug, PartialEq)]
1199 struct TestTag {
1200 mutated: bool,
1201 }
1202 impl TryFrom<UnknownTag<'_>> for TestTag {
1203 type Error = ValidationError;
1204 fn try_from(tag: UnknownTag<'_>) -> Result<Self, Self::Error> {
1205 let list = tag
1206 .value()
1207 .ok_or(ParseTagValueError::UnexpectedEmpty)?
1208 .try_as_attribute_list()?;
1209 let Some(mutated_str) = list
1210 .get("MUTATED")
1211 .and_then(AttributeValue::unquoted)
1212 .and_then(|v| v.try_as_utf_8().ok())
1213 else {
1214 return Err(ValidationError::MissingRequiredAttribute("MUTATED"));
1215 };
1216 match mutated_str {
1217 "NO" => Ok(Self { mutated: false }),
1218 "YES" => Ok(Self { mutated: true }),
1219 _ => Err(ValidationError::InvalidEnumeratedString),
1220 }
1221 }
1222 }
1223 impl CustomTag<'_> for TestTag {
1224 fn is_known_name(name: &str) -> bool {
1225 name == "-X-TEST-TAG"
1226 }
1227 }
1228 impl WritableCustomTag<'_> for TestTag {
1229 fn into_writable_tag(self) -> WritableTag<'static> {
1230 let value = if self.mutated { "YES" } else { "NO" };
1231 WritableTag::new(
1232 "-X-TEST-TAG",
1233 [(
1234 "MUTATED",
1235 WritableAttributeValue::UnquotedString(value.into()),
1236 )],
1237 )
1238 }
1239 }
1240
1241 #[test]
1242 fn custom_tag_should_be_mutable() {
1243 let data = "#EXT-X-TEST-TAG:MUTATED=NO";
1244 let mut reader =
1245 Reader::with_custom_from_str(data, ParsingOptions::default(), PhantomData::<TestTag>);
1246 let mut writer = Writer::new(Vec::new());
1247 match reader.read_line() {
1248 Ok(Some(HlsLine::KnownTag(KnownTag::Custom(mut tag)))) => {
1249 assert_eq!(false, tag.as_ref().mutated);
1250 tag.as_mut().mutated = true;
1251 assert_eq!(true, tag.as_ref().mutated);
1252 writer
1253 .write_custom_line(HlsLine::from(tag))
1254 .expect("should not fail write");
1255 }
1256 l => panic!("unexpected line {l:?}"),
1257 }
1258 assert_eq!(
1259 "#EXT-X-TEST-TAG:MUTATED=YES\n",
1260 std::str::from_utf8(&writer.into_inner()).expect("should be valid str")
1261 );
1262 }
1263
1264 // This implementation we'll set the writable tag output to a value not related to the tag to
1265 // demonstrate that it is only accessed for the output when mutated.
1266 #[derive(Debug, PartialEq)]
1267 struct WeirdTag {
1268 number: f64,
1269 }
1270 impl TryFrom<UnknownTag<'_>> for WeirdTag {
1271 type Error = ValidationError;
1272 fn try_from(tag: UnknownTag<'_>) -> Result<Self, Self::Error> {
1273 let number = tag
1274 .value()
1275 .ok_or(ParseTagValueError::UnexpectedEmpty)?
1276 .try_as_decimal_floating_point()?;
1277 Ok(Self { number })
1278 }
1279 }
1280 impl CustomTag<'_> for WeirdTag {
1281 fn is_known_name(name: &str) -> bool {
1282 name == "-X-WEIRD-TAG"
1283 }
1284 }
1285 impl WritableCustomTag<'_> for WeirdTag {
1286 fn into_writable_tag(self) -> WritableTag<'static> {
1287 WritableTag::new("-X-WEIRD-TAG", [("SO-WEIRD", 999)])
1288 }
1289 }
1290
1291 #[test]
1292 fn custom_tag_should_only_use_into_writable_tag_when_mutated() {
1293 let data = "#EXT-X-WEIRD-TAG:42";
1294 let mut reader =
1295 Reader::with_custom_from_str(data, ParsingOptions::default(), PhantomData::<WeirdTag>);
1296 let mut writer = Writer::new(Vec::new());
1297 match reader.read_line() {
1298 Ok(Some(HlsLine::KnownTag(KnownTag::Custom(tag)))) => {
1299 assert_eq!(42.0, tag.as_ref().number);
1300 writer
1301 .write_custom_line(HlsLine::from(tag))
1302 .expect("should not fail write");
1303 }
1304 l => panic!("unexpected line {l:?}"),
1305 }
1306 assert_eq!(
1307 "#EXT-X-WEIRD-TAG:42\n",
1308 std::str::from_utf8(&writer.into_inner()).expect("should be valid str")
1309 );
1310
1311 // Now re-run the test with mutation
1312 let mut reader =
1313 Reader::with_custom_from_str(data, ParsingOptions::default(), PhantomData::<WeirdTag>);
1314 let mut writer = Writer::new(Vec::new());
1315 match reader.read_line() {
1316 Ok(Some(HlsLine::KnownTag(KnownTag::Custom(mut tag)))) => {
1317 assert_eq!(42.0, tag.as_ref().number);
1318 tag.as_mut().number = 69.0;
1319 writer
1320 .write_custom_line(HlsLine::from(tag))
1321 .expect("should not fail write");
1322 }
1323 l => panic!("unexpected line {l:?}"),
1324 }
1325 assert_eq!(
1326 "#EXT-X-WEIRD-TAG:SO-WEIRD=999\n",
1327 std::str::from_utf8(&writer.into_inner()).expect("should be valid str")
1328 );
1329 }
1330}