mirror of
https://github.com/sile/hls_m3u8.git
synced 2025-02-16 21:25:15 +00:00
refactor attribute parsing to comply with #26
This commit is contained in:
parent
e156f6e3fd
commit
a777f74cfa
10 changed files with 140 additions and 136 deletions
226
src/attribute.rs
226
src/attribute.rs
|
@ -1,148 +1,144 @@
|
||||||
use std::collections::HashMap;
|
use core::iter::FusedIterator;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||||
pub struct AttributePairs(HashMap<String, String>);
|
pub(crate) struct AttributePairs<'a> {
|
||||||
|
string: &'a str,
|
||||||
impl AttributePairs {
|
index: usize,
|
||||||
pub fn new() -> Self { Self::default() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for AttributePairs {
|
impl<'a> AttributePairs<'a> {
|
||||||
type Target = HashMap<String, String>;
|
pub const fn new(string: &'a str) -> Self { Self { string, index: 0 } }
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target { &self.0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for AttributePairs {
|
impl<'a> Iterator for AttributePairs<'a> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
type Item = (&'a str, &'a str);
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for AttributePairs {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
type IntoIter = ::std::collections::hash_map::IntoIter<String, String>;
|
// return `None`, if there are no more chars
|
||||||
type Item = (String, String);
|
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 {
|
// advance the index to the 2nd char after the end of the key
|
||||||
type IntoIter = ::std::collections::hash_map::Iter<'a, String, String>;
|
// (this will skip the `=`)
|
||||||
type Item = (&'a String, &'a String);
|
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 {
|
let value = {
|
||||||
type Err = Error;
|
let start = self.index;
|
||||||
|
let mut end = 0;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
// find the end of the value by searching for `,`.
|
||||||
let mut result = Self::new();
|
// 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, ',') {
|
if *item == b'"' {
|
||||||
let pair = split(line.trim(), '=');
|
inside_quotes = !inside_quotes;
|
||||||
|
} else if *item == b',' && !inside_quotes {
|
||||||
if pair.len() < 2 {
|
self.index += 1;
|
||||||
continue;
|
end -= 1;
|
||||||
}
|
break;
|
||||||
|
|
||||||
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<String> {
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
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<usize>) {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_attributes() {
|
||||||
let pairs = "FOO=BAR,BAR=\"baz,qux\",ABC=12.3"
|
let mut attributes = AttributePairs::new("KEY=VALUE,PAIR=YES");
|
||||||
.parse::<AttributePairs>()
|
assert_eq!((2, Some(2)), attributes.size_hint());
|
||||||
.unwrap();
|
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();
|
let mut attributes = AttributePairs::new("garbage");
|
||||||
assert!(iterator.any(|(k, v)| k == "FOO" && "BAR" == v));
|
assert_eq!((0, Some(0)), attributes.size_hint());
|
||||||
|
assert_eq!(None, attributes.next());
|
||||||
|
|
||||||
let mut iterator = pairs.iter();
|
let mut attributes = AttributePairs::new("KEY=,=VALUE,=,");
|
||||||
assert!(iterator.any(|(k, v)| k == "BAR" && v == "\"baz,qux\""));
|
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();
|
// test quotes:
|
||||||
assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3"));
|
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();
|
// test with chars, that are larger, than 1 byte
|
||||||
pairs.insert("FOO".to_string(), "BAR".to_string());
|
let mut attributes = AttributePairs::new(
|
||||||
|
"LANGUAGE=\"fre\",\
|
||||||
assert_eq!("FOO=BAR,VAL".parse::<AttributePairs>().unwrap(), pairs);
|
NAME=\"Français\",\
|
||||||
}
|
AUTOSELECT=YES",
|
||||||
|
|
||||||
#[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::<Vec<_>>(),
|
|
||||||
map.into_iter().collect::<Vec<_>>()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,8 +162,8 @@ impl FromStr for ExtXIFrameStreamInf {
|
||||||
|
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
if let "URI" = key.as_str() {
|
if key == "URI" {
|
||||||
uri = Some(unquote(value));
|
uri = Some(unquote(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -733,8 +733,8 @@ impl FromStr for ExtXMedia {
|
||||||
|
|
||||||
let mut builder = Self::builder();
|
let mut builder = Self::builder();
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"TYPE" => {
|
"TYPE" => {
|
||||||
builder.media_type(value.parse::<MediaType>()?);
|
builder.media_type(value.parse::<MediaType>()?);
|
||||||
}
|
}
|
||||||
|
@ -832,12 +832,12 @@ mod test {
|
||||||
{
|
{
|
||||||
ExtXMedia::builder()
|
ExtXMedia::builder()
|
||||||
.media_type(MediaType::Audio)
|
.media_type(MediaType::Audio)
|
||||||
|
.uri("fre/prog_index.m3u8")
|
||||||
.group_id("audio")
|
.group_id("audio")
|
||||||
.language("fre")
|
.language("fre")
|
||||||
.name("Français")
|
.name("Français")
|
||||||
.is_autoselect(true)
|
|
||||||
.is_default(false)
|
.is_default(false)
|
||||||
.uri("fre/prog_index.m3u8")
|
.is_autoselect(true)
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
"#EXT-X-MEDIA:\
|
"#EXT-X-MEDIA:\
|
||||||
|
|
|
@ -275,8 +275,8 @@ impl FromStr for ExtXSessionData {
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut language = None;
|
let mut language = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"DATA-ID" => data_id = Some(unquote(value)),
|
"DATA-ID" => data_id = Some(unquote(value)),
|
||||||
"VALUE" => session_value = Some(unquote(value)),
|
"VALUE" => session_value = Some(unquote(value)),
|
||||||
"URI" => uri = Some(unquote(value)),
|
"URI" => uri = Some(unquote(value)),
|
||||||
|
|
|
@ -342,8 +342,8 @@ impl FromStr for ExtXStreamInf {
|
||||||
let mut subtitles = None;
|
let mut subtitles = None;
|
||||||
let mut closed_captions = None;
|
let mut closed_captions = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"FRAME-RATE" => frame_rate = Some((value.parse())?),
|
"FRAME-RATE" => frame_rate = Some((value.parse())?),
|
||||||
"AUDIO" => audio = Some(unquote(value)),
|
"AUDIO" => audio = Some(unquote(value)),
|
||||||
"SUBTITLES" => subtitles = Some(unquote(value)),
|
"SUBTITLES" => subtitles = Some(unquote(value)),
|
||||||
|
|
|
@ -714,8 +714,8 @@ impl FromStr for ExtXDateRange {
|
||||||
|
|
||||||
let mut client_attributes = BTreeMap::new();
|
let mut client_attributes = BTreeMap::new();
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"ID" => id = Some(unquote(value)),
|
"ID" => id = Some(unquote(value)),
|
||||||
"CLASS" => class = Some(unquote(value)),
|
"CLASS" => class = Some(unquote(value)),
|
||||||
"START-DATE" => start_date = Some(unquote(value)),
|
"START-DATE" => start_date = Some(unquote(value)),
|
||||||
|
|
|
@ -168,8 +168,8 @@ impl FromStr for ExtXMap {
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut range = None;
|
let mut range = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"URI" => uri = Some(unquote(value)),
|
"URI" => uri = Some(unquote(value)),
|
||||||
"BYTERANGE" => {
|
"BYTERANGE" => {
|
||||||
range = Some(unquote(value).parse()?);
|
range = Some(unquote(value).parse()?);
|
||||||
|
|
|
@ -133,8 +133,8 @@ impl FromStr for ExtXStart {
|
||||||
let mut time_offset = None;
|
let mut time_offset = None;
|
||||||
let mut precise = false;
|
let mut precise = false;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"TIME-OFFSET" => time_offset = Some((value.parse())?),
|
"TIME-OFFSET" => time_offset = Some((value.parse())?),
|
||||||
"PRECISE" => precise = (parse_yes_or_no(value))?,
|
"PRECISE" => precise = (parse_yes_or_no(value))?,
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
@ -293,10 +293,18 @@ impl FromStr for DecryptionKey {
|
||||||
let mut key_format = None;
|
let mut key_format = None;
|
||||||
let mut key_format_versions = None;
|
let mut key_format_versions = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"METHOD" => method = Some(value.parse().map_err(Error::strum)?),
|
"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()?),
|
"IV" => iv = Some(value.parse()?),
|
||||||
"KEYFORMAT" => key_format = Some(value.parse()?),
|
"KEYFORMAT" => key_format = Some(value.parse()?),
|
||||||
"KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()),
|
"KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()),
|
||||||
|
|
|
@ -268,8 +268,8 @@ impl FromStr for StreamInf {
|
||||||
let mut hdcp_level = None;
|
let mut hdcp_level = None;
|
||||||
let mut video = None;
|
let mut video = None;
|
||||||
|
|
||||||
for (key, value) in input.parse::<AttributePairs>()? {
|
for (key, value) in AttributePairs::new(input) {
|
||||||
match key.as_str() {
|
match key {
|
||||||
"BANDWIDTH" => bandwidth = Some(value.parse::<u64>().map_err(Error::parse_int)?),
|
"BANDWIDTH" => bandwidth = Some(value.parse::<u64>().map_err(Error::parse_int)?),
|
||||||
"AVERAGE-BANDWIDTH" => {
|
"AVERAGE-BANDWIDTH" => {
|
||||||
average_bandwidth = Some(value.parse::<u64>().map_err(Error::parse_int)?)
|
average_bandwidth = Some(value.parse::<u64>().map_err(Error::parse_int)?)
|
||||||
|
|
Loading…
Reference in a new issue