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