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}