Skip to main content

quick_m3u8/tag_internal/
unknown.rs

1//! Methods for parsing unknown tag information
2//!
3//! This module also serves as a building block for the parsing of all known tags. Before a tag is
4//! parsed as known, it is first parsed as unknown, and then we attempt to specialize it. Known tags
5//! can also fall back to unknown tags if there is some issue in validating the strong type
6//! requirements of the tag.
7
8use crate::{
9    error::{UnknownTagSyntaxError, ValidationError},
10    line::{ParsedByteSlice, ParsedLineSlice},
11    tag::TagValue,
12    utils::{split_on_new_line, str_from},
13};
14use memchr::memchr2;
15use std::fmt::Debug;
16
17/// A tag that is unknown to the library found during parsing input data.
18///
19/// This may be because the tag is truly unknown (i.e., is not one of the 32 supported HLS defined
20/// tags), or because the known tag has been ignored via [`crate::config::ParsingOptions`], or also
21/// if there was an error in parsing the known tag. In the last case, the [`Self::validation_error`]
22/// will provide details on the problem encountered.
23///
24/// Despite not being "fully parsed", the [`TagValue`] provided in [`Self::value`] provides many
25/// methods useful for extracting more information from the tag value, and is what all the library
26/// defined HLS tags use to parse into more strongly defined data structures.
27///
28/// For example:
29/// ```
30/// # use quick_m3u8::{Reader, HlsLine, config::ParsingOptionsBuilder, error::ValidationError,
31/// # tag::TagValue};
32/// let lines = r#"#EXT-X-QUESTION:VALUE="Do you know who I am?"
33/// #EXT-X-PROGRAM-DATE-TIME:2025-08-05T21:59:42.417-05:00
34/// #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=10000000"#;
35///
36/// let mut reader = Reader::from_str(
37///     lines,
38///     ParsingOptionsBuilder::new()
39///         .with_parsing_for_stream_inf()
40///         .build()
41/// );
42///
43/// // #EXT-X-QUESTION:VALUE="Do you know who I am?"
44/// let Ok(Some(HlsLine::UnknownTag(tag))) = reader.read_line() else { panic!("unexpected tag") };
45/// assert_eq!("-X-QUESTION", tag.name());
46/// assert_eq!(Some(TagValue(r#"VALUE="Do you know who I am?""#.as_bytes())), tag.value());
47/// assert_eq!(None, tag.validation_error());
48/// assert_eq!(r#"#EXT-X-QUESTION:VALUE="Do you know who I am?""#.as_bytes(), tag.as_bytes());
49///
50/// // #EXT-X-PROGRAM-DATE-TIME:2025-08-05T21:59:42.417-05:00
51/// let Ok(Some(HlsLine::UnknownTag(tag))) = reader.read_line() else { panic!("unexpected tag") };
52/// assert_eq!("-X-PROGRAM-DATE-TIME", tag.name());
53/// assert_eq!(Some(TagValue("2025-08-05T21:59:42.417-05:00".as_bytes())), tag.value());
54/// assert_eq!(None, tag.validation_error());
55/// assert_eq!(
56///     "#EXT-X-PROGRAM-DATE-TIME:2025-08-05T21:59:42.417-05:00".as_bytes(),
57///     tag.as_bytes()
58/// );
59///
60/// // #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=10000000
61/// let Ok(Some(HlsLine::UnknownTag(tag))) = reader.read_line() else { panic!("unexpected tag") };
62/// assert_eq!("-X-STREAM-INF", tag.name());
63/// assert_eq!(Some(TagValue("AVERAGE-BANDWIDTH=10000000".as_bytes())), tag.value());
64/// assert_eq!(
65///     Some(ValidationError::MissingRequiredAttribute("BANDWIDTH")),
66///     tag.validation_error()
67/// );
68/// assert_eq!(
69///     "#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=10000000".as_bytes(),
70///     tag.as_bytes()
71/// );
72/// ```
73#[derive(Debug, PartialEq, Clone, Copy)]
74pub struct UnknownTag<'a> {
75    pub(crate) name: &'a str,
76    pub(crate) value: Option<TagValue<'a>>,
77    pub(crate) original_input: &'a [u8],
78    pub(crate) validation_error: Option<ValidationError>,
79}
80
81impl<'a> UnknownTag<'a> {
82    /// The name of the unknown tag.
83    ///
84    /// This includes everything after the `#EXT` prefix and before the `:` or new line. For
85    /// example, `#EXTM3U` has name `M3U`, `#EXT-X-VERSION:3` has name `-X-VERSION`, etc.
86    pub fn name(&self) -> &'a str {
87        self.name
88    }
89
90    /// The value of the unknown tag.
91    ///
92    /// This will be the entire byte-slice after the first `:` in the line. If there is no `:` then
93    /// this will be `None`. The slice borrow is wrapped in [`TagValue`] which provides many methods
94    /// for converting to a more suitable data structure depending on the tag. See the documentation
95    /// for `TagValue` for more information.
96    pub fn value(&self) -> Option<TagValue<'a>> {
97        self.value
98    }
99
100    /// The error that led to this tag being unknown.
101    ///
102    /// This value is only `Some` if the tag is unknown as the result of a problem in parsing a
103    /// known tag.
104    pub fn validation_error(&self) -> Option<ValidationError> {
105        self.validation_error
106    }
107
108    /// The raw bytes of the tag line for output.
109    ///
110    /// This is useful for when the tag needs to be writtern to an output.
111    pub fn as_bytes(&self) -> &'a [u8] {
112        split_on_new_line(self.original_input).parsed
113    }
114}
115
116/// Try to parse some input into a tag.
117///
118/// The parsing will stop at the new line. Failures are described via [`UnknownTagSyntaxError`].
119/// This method is at the root of parsing in this library and what other higher level types are
120/// built on top of. It helps by splitting the input on a new line and providing a name and value
121/// slice for the line we are parsing (assuming it is a tag line).
122pub fn parse(input: &str) -> Result<ParsedLineSlice<'_, UnknownTag<'_>>, UnknownTagSyntaxError> {
123    let input = input.as_bytes();
124    if input.get(3) == Some(&b'T') && &input[..3] == b"#EX" {
125        let ParsedByteSlice { parsed, remaining } = parse_assuming_ext_taken(&input[4..], input)?;
126        Ok(ParsedLineSlice {
127            parsed,
128            remaining: remaining.map(str_from),
129        })
130    } else {
131        Err(UnknownTagSyntaxError::InvalidTag)
132    }
133}
134
135pub(crate) fn parse_assuming_ext_taken<'a>(
136    input: &'a [u8],
137    original_input: &'a [u8],
138) -> Result<ParsedByteSlice<'a, UnknownTag<'a>>, UnknownTagSyntaxError> {
139    if input.is_empty() || input[0] == b'\n' || input[0] == b'\r' {
140        return Err(UnknownTagSyntaxError::UnexpectedNoTagName);
141    };
142    match memchr2(b':', b'\n', input) {
143        Some(n) if input[n] == b':' => {
144            let name = std::str::from_utf8(&input[..n])?;
145            let ParsedByteSlice { parsed, remaining } = split_on_new_line(&input[(n + 1)..]);
146            Ok(ParsedByteSlice {
147                parsed: UnknownTag {
148                    name,
149                    value: Some(TagValue(parsed)),
150                    original_input,
151                    validation_error: None,
152                },
153                remaining,
154            })
155        }
156        Some(n) if input[n - 1] == b'\r' => {
157            let name = std::str::from_utf8(&input[..(n - 1)])?;
158            Ok(ParsedByteSlice {
159                parsed: UnknownTag {
160                    name,
161                    value: None,
162                    original_input,
163                    validation_error: None,
164                },
165                remaining: Some(&input[(n + 1)..]),
166            })
167        }
168        Some(n) => {
169            let name = std::str::from_utf8(&input[..n])?;
170            Ok(ParsedByteSlice {
171                parsed: UnknownTag {
172                    name,
173                    value: None,
174                    original_input,
175                    validation_error: None,
176                },
177                remaining: Some(&input[(n + 1)..]),
178            })
179        }
180        None => {
181            let name = std::str::from_utf8(input)?;
182            Ok(ParsedByteSlice {
183                parsed: UnknownTag {
184                    name,
185                    value: None,
186                    original_input,
187                    validation_error: None,
188                },
189                remaining: None,
190            })
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use pretty_assertions::assert_eq;
199
200    #[test]
201    fn tag_value_empty_when_remaining_none() {
202        let tag = UnknownTag {
203            name: "-X-TEST",
204            value: None,
205            original_input: b"#EXT-X-TEST",
206            validation_error: None,
207        };
208        assert_eq!(None, tag.value());
209        assert_eq!(b"#EXT-X-TEST", tag.as_bytes());
210    }
211
212    #[test]
213    fn tag_value_empty_when_remaining_is_empty() {
214        let tag = UnknownTag {
215            name: "-X-TEST",
216            value: Some(TagValue(b"")),
217            original_input: b"#EXT-X-TEST:",
218            validation_error: None,
219        };
220        assert_eq!(Some(TagValue(b"")), tag.value());
221        assert_eq!(b"#EXT-X-TEST:", tag.as_bytes());
222    }
223
224    #[test]
225    fn tag_value_some_when_remaining_is_some() {
226        let tag = UnknownTag {
227            name: "-X-TEST",
228            value: Some(TagValue(b"42")),
229            original_input: b"#EXT-X-TEST:42",
230            validation_error: None,
231        };
232        assert_eq!(Some(TagValue(b"42")), tag.value());
233        assert_eq!(b"#EXT-X-TEST:42", tag.as_bytes());
234    }
235
236    #[test]
237    fn tag_value_remaining_is_some_when_split_by_crlf() {
238        let tag = UnknownTag {
239            name: "-X-TEST",
240            value: Some(TagValue(b"42")),
241            original_input: b"#EXT-X-TEST:42\r\n#EXT-X-NEW-TEST\r\n",
242            validation_error: None,
243        };
244        assert_eq!(Some(TagValue(b"42")), tag.value());
245        assert_eq!(b"#EXT-X-TEST:42", tag.as_bytes());
246    }
247
248    #[test]
249    fn tag_value_remaining_is_some_when_split_by_lf() {
250        let tag = UnknownTag {
251            name: "-X-TEST",
252            value: Some(TagValue(b"42")),
253            original_input: b"#EXT-X-TEST:42\n#EXT-X-NEW-TEST\n",
254            validation_error: None,
255        };
256        assert_eq!(Some(TagValue(b"42")), tag.value());
257        assert_eq!(b"#EXT-X-TEST:42", tag.as_bytes());
258    }
259
260    #[test]
261    fn parses_tag_with_no_value() {
262        assert_eq!(
263            Ok(ParsedLineSlice {
264                parsed: UnknownTag {
265                    name: "-TEST-TAG",
266                    value: None,
267                    original_input: b"#EXT-TEST-TAG",
268                    validation_error: None,
269                },
270                remaining: None
271            }),
272            parse("#EXT-TEST-TAG")
273        );
274        assert_eq!(
275            Ok(ParsedLineSlice {
276                parsed: UnknownTag {
277                    name: "-TEST-TAG",
278                    value: None,
279                    original_input: b"#EXT-TEST-TAG\r\n",
280                    validation_error: None,
281                },
282                remaining: Some("")
283            }),
284            parse("#EXT-TEST-TAG\r\n")
285        );
286        assert_eq!(
287            Ok(ParsedLineSlice {
288                parsed: UnknownTag {
289                    name: "-TEST-TAG",
290                    value: None,
291                    original_input: b"#EXT-TEST-TAG\n",
292                    validation_error: None,
293                },
294                remaining: Some("")
295            }),
296            parse("#EXT-TEST-TAG\n")
297        );
298    }
299
300    #[test]
301    fn parses_tag_with_value() {
302        assert_eq!(
303            Ok(ParsedLineSlice {
304                parsed: UnknownTag {
305                    name: "-TEST-TAG",
306                    value: Some(TagValue(b"42")),
307                    original_input: b"#EXT-TEST-TAG:42",
308                    validation_error: None,
309                },
310                remaining: None
311            }),
312            parse("#EXT-TEST-TAG:42")
313        );
314        assert_eq!(
315            Ok(ParsedLineSlice {
316                parsed: UnknownTag {
317                    name: "-TEST-TAG",
318                    value: Some(TagValue(b"42")),
319                    original_input: b"#EXT-TEST-TAG:42\r\n",
320                    validation_error: None,
321                },
322                remaining: Some("")
323            }),
324            parse("#EXT-TEST-TAG:42\r\n")
325        );
326        assert_eq!(
327            Ok(ParsedLineSlice {
328                parsed: UnknownTag {
329                    name: "-TEST-TAG",
330                    value: Some(TagValue(b"42")),
331                    original_input: b"#EXT-TEST-TAG:42\n",
332                    validation_error: None,
333                },
334                remaining: Some("")
335            }),
336            parse("#EXT-TEST-TAG:42\n")
337        );
338    }
339
340    #[test]
341    fn parse_remaining_is_some_when_split_by_crlf() {
342        assert_eq!(
343            Ok(ParsedLineSlice {
344                parsed: UnknownTag {
345                    name: "-X-TEST",
346                    value: Some(TagValue(b"42")),
347                    original_input: b"#EXT-X-TEST:42\r\n#EXT-X-NEW-TEST\r\n",
348                    validation_error: None,
349                },
350                remaining: Some("#EXT-X-NEW-TEST\r\n")
351            }),
352            parse("#EXT-X-TEST:42\r\n#EXT-X-NEW-TEST\r\n")
353        );
354    }
355
356    #[test]
357    fn parse_remaining_is_some_when_split_by_lf() {
358        assert_eq!(
359            Ok(ParsedLineSlice {
360                parsed: UnknownTag {
361                    name: "-X-TEST",
362                    value: Some(TagValue(b"42")),
363                    original_input: b"#EXT-X-TEST:42\n#EXT-X-NEW-TEST\n",
364                    validation_error: None,
365                },
366                remaining: Some("#EXT-X-NEW-TEST\n")
367            }),
368            parse("#EXT-X-TEST:42\n#EXT-X-NEW-TEST\n")
369        );
370    }
371}