diff --git a/src/attribute.rs b/src/attribute.rs index 95f99a6..228ca82 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,148 +1,144 @@ -use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; -use std::str::FromStr; - -use crate::Error; +use core::iter::FusedIterator; #[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct AttributePairs(HashMap); - -impl AttributePairs { - pub fn new() -> Self { Self::default() } +pub(crate) struct AttributePairs<'a> { + string: &'a str, + index: usize, } -impl Deref for AttributePairs { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { &self.0 } +impl<'a> AttributePairs<'a> { + pub const fn new(string: &'a str) -> Self { Self { string, index: 0 } } } -impl DerefMut for AttributePairs { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} +impl<'a> Iterator for AttributePairs<'a> { + type Item = (&'a str, &'a str); -impl IntoIterator for AttributePairs { - type IntoIter = ::std::collections::hash_map::IntoIter; - type Item = (String, String); + fn next(&mut self) -> Option { + // return `None`, if there are no more chars + self.string.as_bytes().get(self.index + 1)?; - fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } -} + let key = { + // the position in the string: + let start = self.index; + // the key ends at an `=`: + let end = self + .string + .bytes() + .skip(self.index) + .position(|i| i == b'=')? + + start; -impl<'a> IntoIterator for &'a AttributePairs { - type IntoIter = ::std::collections::hash_map::Iter<'a, String, String>; - type Item = (&'a String, &'a String); + // advance the index to the 2nd char after the end of the key + // (this will skip the `=`) + self.index = end + 1; - fn into_iter(self) -> Self::IntoIter { self.0.iter() } -} + core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() + }; -impl FromStr for AttributePairs { - type Err = Error; + let value = { + let start = self.index; + let mut end = 0; - fn from_str(input: &str) -> Result { - let mut result = Self::new(); + // find the end of the value by searching for `,`. + // it should ignore `,` that are inside double quotes. + let mut inside_quotes = false; + while let Some(item) = self.string.as_bytes().get(start + end) { + end += 1; - for line in split(input, ',') { - let pair = split(line.trim(), '='); - - if pair.len() < 2 { - continue; - } - - let key = pair[0].trim().to_uppercase(); - let value = pair[1].trim().to_string(); - if value.is_empty() { - continue; - } - - result.insert(key.trim().to_string(), value.trim().to_string()); - } - - #[cfg(test)] // this is very useful, when a test fails! - dbg!(&result); - Ok(result) - } -} - -fn split(value: &str, terminator: char) -> Vec { - let mut result = vec![]; - - let mut inside_quotes = false; - let mut temp_string = String::with_capacity(1024); - - for c in value.chars() { - match c { - '"' => { - inside_quotes = !inside_quotes; - temp_string.push(c); - } - k if (k == terminator) => { - if inside_quotes { - temp_string.push(c); - } else { - result.push(temp_string); - temp_string = String::with_capacity(1024); + if *item == b'"' { + inside_quotes = !inside_quotes; + } else if *item == b',' && !inside_quotes { + self.index += 1; + end -= 1; + break; } } - _ => { - temp_string.push(c); + + self.index += end; + end += start; + + core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() + }; + + Some((key.trim(), value.trim())) + } + + fn size_hint(&self) -> (usize, Option) { + let mut remaining = 0; + + // each `=` in the remaining str is an iteration + // this also ignores `=` inside quotes! + let mut inside_quotes = false; + for c in self.string.as_bytes().iter().skip(self.index) { + if *c == b'=' && !inside_quotes { + remaining += 1; + } else if *c == b'"' { + inside_quotes = !inside_quotes; } } - } - result.push(temp_string); - result + (remaining, Some(remaining)) + } } +impl<'a> ExactSizeIterator for AttributePairs<'a> {} +impl<'a> FusedIterator for AttributePairs<'a> {} + #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] - fn test_parser() { - let pairs = "FOO=BAR,BAR=\"baz,qux\",ABC=12.3" - .parse::() - .unwrap(); + fn test_attributes() { + let mut attributes = AttributePairs::new("KEY=VALUE,PAIR=YES"); + assert_eq!((2, Some(2)), attributes.size_hint()); + assert_eq!(Some(("KEY", "VALUE")), attributes.next()); + assert_eq!((1, Some(1)), attributes.size_hint()); + assert_eq!(Some(("PAIR", "YES")), attributes.next()); + assert_eq!((0, Some(0)), attributes.size_hint()); + assert_eq!(None, attributes.next()); - let mut iterator = pairs.iter(); - assert!(iterator.any(|(k, v)| k == "FOO" && "BAR" == v)); + let mut attributes = AttributePairs::new("garbage"); + assert_eq!((0, Some(0)), attributes.size_hint()); + assert_eq!(None, attributes.next()); - let mut iterator = pairs.iter(); - assert!(iterator.any(|(k, v)| k == "BAR" && v == "\"baz,qux\"")); + let mut attributes = AttributePairs::new("KEY=,=VALUE,=,"); + assert_eq!((3, Some(3)), attributes.size_hint()); + assert_eq!(Some(("KEY", "")), attributes.next()); + assert_eq!((2, Some(2)), attributes.size_hint()); + assert_eq!(Some(("", "VALUE")), attributes.next()); + assert_eq!((1, Some(1)), attributes.size_hint()); + assert_eq!(Some(("", "")), attributes.next()); + assert_eq!((0, Some(0)), attributes.size_hint()); + assert_eq!(None, attributes.next()); - let mut iterator = pairs.iter(); - assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3")); + // test quotes: + let mut attributes = AttributePairs::new("KEY=\"VALUE,\","); + assert_eq!((1, Some(1)), attributes.size_hint()); + assert_eq!(Some(("KEY", "\"VALUE,\"")), attributes.next()); + assert_eq!((0, Some(0)), attributes.size_hint()); + assert_eq!(None, attributes.next()); - let mut pairs = AttributePairs::new(); - pairs.insert("FOO".to_string(), "BAR".to_string()); - - assert_eq!("FOO=BAR,VAL".parse::().unwrap(), pairs); - } - - #[test] - fn test_iterator() { - let mut attrs = AttributePairs::new(); - attrs.insert("key_01".to_string(), "value_01".to_string()); - attrs.insert("key_02".to_string(), "value_02".to_string()); - - let mut iterator = attrs.iter(); - assert!(iterator.any(|(k, v)| k == "key_01" && v == "value_01")); - - let mut iterator = attrs.iter(); - assert!(iterator.any(|(k, v)| k == "key_02" && v == "value_02")); - } - - #[test] - fn test_into_iter() { - let mut map = HashMap::new(); - map.insert("k".to_string(), "v".to_string()); - - let mut attrs = AttributePairs::new(); - attrs.insert("k".to_string(), "v".to_string()); - - assert_eq!( - attrs.into_iter().collect::>(), - map.into_iter().collect::>() + // test with chars, that are larger, than 1 byte + let mut attributes = AttributePairs::new( + "LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES", ); + + assert_eq!(Some(("LANGUAGE", "\"fre\"")), attributes.next()); + assert_eq!(Some(("NAME", "\"Français\"")), attributes.next()); + assert_eq!(Some(("AUTOSELECT", "YES")), attributes.next()); + } + + #[test] + fn test_parser() { + let mut pairs = AttributePairs::new("FOO=BAR,BAR=\"baz,qux\",ABC=12.3"); + + assert_eq!(pairs.next(), Some(("FOO", "BAR"))); + assert_eq!(pairs.next(), Some(("BAR", "\"baz,qux\""))); + assert_eq!(pairs.next(), Some(("ABC", "12.3"))); + assert_eq!(pairs.next(), None); } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 1e2ddec..7f748ac 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -162,8 +162,8 @@ impl FromStr for ExtXIFrameStreamInf { let mut uri = None; - for (key, value) in input.parse::()? { - if let "URI" = key.as_str() { + for (key, value) in AttributePairs::new(input) { + if key == "URI" { uri = Some(unquote(value)); } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 6bfb3c0..027ddfb 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -733,8 +733,8 @@ impl FromStr for ExtXMedia { let mut builder = Self::builder(); - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "TYPE" => { builder.media_type(value.parse::()?); } @@ -832,12 +832,12 @@ mod test { { ExtXMedia::builder() .media_type(MediaType::Audio) + .uri("fre/prog_index.m3u8") .group_id("audio") .language("fre") .name("Français") - .is_autoselect(true) .is_default(false) - .uri("fre/prog_index.m3u8") + .is_autoselect(true) .build() .unwrap(), "#EXT-X-MEDIA:\ diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index b03b0e5..d73e9ab 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -275,8 +275,8 @@ impl FromStr for ExtXSessionData { let mut uri = None; let mut language = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "DATA-ID" => data_id = Some(unquote(value)), "VALUE" => session_value = Some(unquote(value)), "URI" => uri = Some(unquote(value)), diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index bf6aced..7fcb885 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -342,8 +342,8 @@ impl FromStr for ExtXStreamInf { let mut subtitles = None; let mut closed_captions = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "FRAME-RATE" => frame_rate = Some((value.parse())?), "AUDIO" => audio = Some(unquote(value)), "SUBTITLES" => subtitles = Some(unquote(value)), diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 87bca24..4374ef7 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -714,8 +714,8 @@ impl FromStr for ExtXDateRange { let mut client_attributes = BTreeMap::new(); - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "ID" => id = Some(unquote(value)), "CLASS" => class = Some(unquote(value)), "START-DATE" => start_date = Some(unquote(value)), diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index f63f8d5..e8237a6 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -168,8 +168,8 @@ impl FromStr for ExtXMap { let mut uri = None; let mut range = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "URI" => uri = Some(unquote(value)), "BYTERANGE" => { range = Some(unquote(value).parse()?); diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 22a3acb..42d0f07 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -133,8 +133,8 @@ impl FromStr for ExtXStart { let mut time_offset = None; let mut precise = false; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "TIME-OFFSET" => time_offset = Some((value.parse())?), "PRECISE" => precise = (parse_yes_or_no(value))?, _ => { diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 9a57fa4..f5933d3 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -293,10 +293,18 @@ impl FromStr for DecryptionKey { let mut key_format = None; let mut key_format_versions = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "METHOD" => method = Some(value.parse().map_err(Error::strum)?), - "URI" => uri = Some(unquote(value)), + "URI" => { + let unquoted_uri = unquote(value); + + if unquoted_uri.trim().is_empty() { + uri = None; + } else { + uri = Some(unquoted_uri); + } + } "IV" => iv = Some(value.parse()?), "KEYFORMAT" => key_format = Some(value.parse()?), "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()), diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index 4886249..afe462e 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -268,8 +268,8 @@ impl FromStr for StreamInf { let mut hdcp_level = None; let mut video = None; - for (key, value) in input.parse::()? { - match key.as_str() { + for (key, value) in AttributePairs::new(input) { + match key { "BANDWIDTH" => bandwidth = Some(value.parse::().map_err(Error::parse_int)?), "AVERAGE-BANDWIDTH" => { average_bandwidth = Some(value.parse::().map_err(Error::parse_int)?)