Skip to main content

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}