quick_m3u8/tag_internal/
known.rs

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