* added QuotedOrUnquoted enum

* implemented `Default`, `From`, `Display` traits
* updated `VariantStream`, `AlternativeMedia`, `SessionData`, `Key`, `Start` emums
* updated `from_hashmap` methods for each enum
* fixed tests
This commit is contained in:
Vadim Getmanshchuk 2022-04-14 02:07:05 -07:00
parent 5c842fd9f6
commit ac0f881eef
2 changed files with 131 additions and 72 deletions

View file

@ -12,6 +12,7 @@ use crate::playlist::*;
use nom::IResult; use nom::IResult;
use std::collections::HashMap; use std::collections::HashMap;
use std::f32; use std::f32;
use std::fmt;
use std::result::Result; use std::result::Result;
use std::str; use std::str;
use std::str::FromStr; use std::str::FromStr;
@ -542,13 +543,16 @@ fn extmap(i: &[u8]) -> IResult<&[u8], Map> {
let uri = attrs.get("URI").cloned().unwrap_or_default(); let uri = attrs.get("URI").cloned().unwrap_or_default();
let byte_range = attrs let byte_range = attrs
.get("BYTERANGE") .get("BYTERANGE")
.map(|range| match byte_range_val(range.as_bytes()) { .map(|range| match byte_range_val(range.to_string().as_bytes()) {
IResult::Ok((_, range)) => Ok(range), IResult::Ok((_, range)) => Ok(range),
IResult::Err(_) => Err("invalid byte range"), IResult::Err(_) => Err("invalid byte range"),
}) })
.transpose()?; .transpose()?;
Ok(Map { uri, byte_range }) Ok(Map {
uri: uri.to_string(),
byte_range,
})
})(i) })(i)
} }
@ -601,7 +605,7 @@ fn comment_tag(i: &[u8]) -> IResult<&[u8], String> {
// Util // Util
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap<String, String>> { fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap<String, QuotedOrUnquoted>> {
fold_many0( fold_many0(
preceded(space0, key_value_pair), preceded(space0, key_value_pair),
HashMap::new, HashMap::new,
@ -612,7 +616,41 @@ fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap<String, String>> {
)(i) )(i)
} }
fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, String)> { #[derive(Debug, PartialEq, Eq, Clone)]
pub enum QuotedOrUnquoted {
Unquoted(String),
Quoted(String),
}
impl Default for QuotedOrUnquoted {
fn default() -> Self {
QuotedOrUnquoted::Quoted(String::new())
}
}
impl From<&str> for QuotedOrUnquoted {
fn from(s: &str) -> Self {
if s.starts_with('"') && s.ends_with('"') {
return QuotedOrUnquoted::Quoted(s.trim_matches('"').to_string());
}
QuotedOrUnquoted::Unquoted(s.to_string())
}
}
impl fmt::Display for QuotedOrUnquoted {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
QuotedOrUnquoted::Unquoted(s) => s,
QuotedOrUnquoted::Quoted(u) => u,
}
)
}
}
fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, QuotedOrUnquoted)> {
map( map(
tuple(( tuple((
peek(none_of("\r\n")), peek(none_of("\r\n")),
@ -625,16 +663,16 @@ fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, String)> {
)(i) )(i)
} }
fn quoted(i: &[u8]) -> IResult<&[u8], String> { fn quoted(i: &[u8]) -> IResult<&[u8], QuotedOrUnquoted> {
delimited( delimited(
char('\"'), char('\"'),
map_res(is_not("\""), from_utf8_slice), map_res(is_not("\""), quoted_from_utf8_slice),
char('\"'), char('\"'),
)(i) )(i)
} }
fn unquoted(i: &[u8]) -> IResult<&[u8], String> { fn unquoted(i: &[u8]) -> IResult<&[u8], QuotedOrUnquoted> {
map_res(is_not(",\r\n"), from_utf8_slice)(i) map_res(is_not(",\r\n"), unquoted_from_utf8_slice)(i)
} }
fn consume_line(i: &[u8]) -> IResult<&[u8], String> { fn consume_line(i: &[u8]) -> IResult<&[u8], String> {
@ -687,6 +725,20 @@ fn from_utf8_slice(s: &[u8]) -> Result<String, string::FromUtf8Error> {
String::from_utf8(s.to_vec()) String::from_utf8(s.to_vec())
} }
fn quoted_from_utf8_slice(s: &[u8]) -> Result<QuotedOrUnquoted, string::FromUtf8Error> {
match String::from_utf8(s.to_vec()) {
Ok(q) => Ok(QuotedOrUnquoted::Quoted(q)),
Err(e) => Err(e),
}
}
fn unquoted_from_utf8_slice(s: &[u8]) -> Result<QuotedOrUnquoted, string::FromUtf8Error> {
match String::from_utf8(s.to_vec()) {
Ok(q) => Ok(QuotedOrUnquoted::Unquoted(q)),
Err(e) => Err(e),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -729,10 +781,13 @@ mod tests {
key_value_pairs(b"BANDWIDTH=395000,CODECS=\"avc1.4d001f,mp4a.40.2\"\r\nrest="), key_value_pairs(b"BANDWIDTH=395000,CODECS=\"avc1.4d001f,mp4a.40.2\"\r\nrest="),
Result::Ok(( Result::Ok((
"\r\nrest=".as_bytes(), "\r\nrest=".as_bytes(),
vec![("BANDWIDTH", "395000"), ("CODECS", "avc1.4d001f,mp4a.40.2")] vec![
.into_iter() ("BANDWIDTH", "395000"),
.map(|(k, v)| (String::from(k), String::from(v))) ("CODECS", "\"avc1.4d001f,mp4a.40.2\"")
.collect::<HashMap<_, _>>(), ]
.into_iter()
.map(|(k, v)| (String::from(k), v.into()))
.collect::<HashMap<_, _>>(),
)), )),
); );
} }
@ -745,13 +800,13 @@ mod tests {
"\nrest".as_bytes(), "\nrest".as_bytes(),
vec![ vec![
("BANDWIDTH", "86000"), ("BANDWIDTH", "86000"),
("URI", "low/iframe.m3u8"), ("URI", "\"low/iframe.m3u8\""),
("PROGRAM-ID", "1"), ("PROGRAM-ID", "1"),
("RESOLUTION", "1x1"), ("RESOLUTION", "\"1x1\""),
("VIDEO", "1") ("VIDEO", "1")
].into_iter() ].into_iter()
.map(|(k, v)| (String::from(k), String::from(v))) .map(|(k, v)| (String::from(k), v.into()))
.collect::<HashMap<String,String>>() .collect::<HashMap<_,_>>()
)) ))
); );
} }
@ -762,10 +817,13 @@ mod tests {
key_value_pairs(b"BANDWIDTH=300000,CODECS=\"avc1.42c015,mp4a.40.2\"\r\nrest"), key_value_pairs(b"BANDWIDTH=300000,CODECS=\"avc1.42c015,mp4a.40.2\"\r\nrest"),
Result::Ok(( Result::Ok((
"\r\nrest".as_bytes(), "\r\nrest".as_bytes(),
vec![("BANDWIDTH", "300000"), ("CODECS", "avc1.42c015,mp4a.40.2")] vec![
.into_iter() ("BANDWIDTH", "300000"),
.map(|(k, v)| (String::from(k), String::from(v))) ("CODECS", "\"avc1.42c015,mp4a.40.2\"")
.collect::<HashMap<String, String>>() ]
.into_iter()
.map(|(k, v)| (String::from(k), v.into()))
.collect::<HashMap<_, _>>()
)) ))
); );
} }
@ -782,8 +840,8 @@ mod tests {
("VIDEO", "1") ("VIDEO", "1")
] ]
.into_iter() .into_iter()
.map(|(k, v)| (String::from(k), String::from(v))) .map(|(k, v)| (String::from(k), v.into()))
.collect::<HashMap<String, String>>() .collect::<HashMap<_, _>>()
)) ))
); );
} }
@ -792,10 +850,7 @@ mod tests {
fn test_key_value_pair() { fn test_key_value_pair() {
assert_eq!( assert_eq!(
key_value_pair(b"PROGRAM-ID=1,rest"), key_value_pair(b"PROGRAM-ID=1,rest"),
Result::Ok(( Result::Ok(("rest".as_bytes(), ("PROGRAM-ID".to_string(), "1".into())))
"rest".as_bytes(),
("PROGRAM-ID".to_string(), "1".to_string())
))
); );
} }
@ -839,7 +894,7 @@ mod tests {
fn quotes() { fn quotes() {
assert_eq!( assert_eq!(
quoted(b"\"value\"rest"), quoted(b"\"value\"rest"),
Result::Ok(("rest".as_bytes(), "value".to_string())) Result::Ok(("rest".as_bytes(), "\"value\"".into()))
); );
} }

View file

@ -3,6 +3,7 @@
//! The main type here is the `Playlist` enum. //! The main type here is the `Playlist` enum.
//! Which is either a `MasterPlaylist` or a `MediaPlaylist`. //! Which is either a `MasterPlaylist` or a `MediaPlaylist`.
use crate::QuotedOrUnquoted;
use std::collections::HashMap; use std::collections::HashMap;
use std::f32; use std::f32;
use std::fmt; use std::fmt;
@ -140,28 +141,31 @@ pub struct VariantStream {
pub codecs: Option<String>, pub codecs: Option<String>,
pub resolution: Option<String>, pub resolution: Option<String>,
pub frame_rate: Option<String>, pub frame_rate: Option<String>,
pub hdcp_level: Option<String>, pub hdcp_level: Option<QuotedOrUnquoted>,
pub audio: Option<String>, pub audio: Option<String>,
pub video: Option<String>, pub video: Option<String>,
pub subtitles: Option<String>, pub subtitles: Option<String>,
pub closed_captions: Option<String>, pub closed_captions: Option<QuotedOrUnquoted>,
// PROGRAM-ID tag was removed in protocol version 6 // PROGRAM-ID tag was removed in protocol version 6
} }
impl VariantStream { impl VariantStream {
pub fn from_hashmap(mut attrs: HashMap<String, String>, is_i_frame: bool) -> VariantStream { pub fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>,
is_i_frame: bool,
) -> VariantStream {
VariantStream { VariantStream {
is_i_frame, is_i_frame,
uri: attrs.remove("URI").unwrap_or_default(), uri: attrs.remove("URI").unwrap_or_default().to_string(),
bandwidth: attrs.remove("BANDWIDTH").unwrap_or_default(), bandwidth: attrs.remove("BANDWIDTH").unwrap_or_default().to_string(),
average_bandwidth: attrs.remove("AVERAGE-BANDWIDTH"), average_bandwidth: attrs.remove("AVERAGE-BANDWIDTH").map(|a| a.to_string()),
codecs: attrs.remove("CODECS"), codecs: attrs.remove("CODECS").map(|c| c.to_string()),
resolution: attrs.remove("RESOLUTION"), resolution: attrs.remove("RESOLUTION").map(|r| r.to_string()),
frame_rate: attrs.remove("FRAME-RATE"), frame_rate: attrs.remove("FRAME-RATE").map(|f| f.to_string()),
hdcp_level: attrs.remove("HDCP-LEVEL"), hdcp_level: attrs.remove("HDCP-LEVEL"),
audio: attrs.remove("AUDIO"), audio: attrs.remove("AUDIO").map(|a| a.to_string()),
video: attrs.remove("VIDEO"), video: attrs.remove("VIDEO").map(|v| v.to_string()),
subtitles: attrs.remove("SUBTITLES"), subtitles: attrs.remove("SUBTITLES").map(|s| s.to_string()),
closed_captions: attrs.remove("CLOSED-CAPTIONS"), closed_captions: attrs.remove("CLOSED-CAPTIONS"),
} }
} }
@ -176,12 +180,7 @@ impl VariantStream {
self.write_stream_inf_common_attributes(w)?; self.write_stream_inf_common_attributes(w)?;
write_some_attribute_quoted!(w, ",AUDIO", &self.audio)?; write_some_attribute_quoted!(w, ",AUDIO", &self.audio)?;
write_some_attribute_quoted!(w, ",SUBTITLES", &self.subtitles)?; write_some_attribute_quoted!(w, ",SUBTITLES", &self.subtitles)?;
// handle `CLOSED-CAPTIONS=NONE` case write_some_attribute!(w, ",CLOSED-CAPTIONS", &self.closed_captions)?;
if self.closed_captions.as_deref().eq(&Some("NONE")) {
write_some_attribute!(w, ",CLOSED-CAPTIONS", &self.closed_captions)?;
} else {
write_some_attribute_quoted!(w, ",CLOSED-CAPTIONS", &self.closed_captions)?;
}
writeln!(w)?; writeln!(w)?;
writeln!(w, "{}", self.uri) writeln!(w, "{}", self.uri)
} }
@ -224,23 +223,23 @@ pub struct AlternativeMedia {
} }
impl AlternativeMedia { impl AlternativeMedia {
pub fn from_hashmap(mut attrs: HashMap<String, String>) -> AlternativeMedia { pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> AlternativeMedia {
AlternativeMedia { AlternativeMedia {
media_type: attrs media_type: attrs
.get("TYPE") .get("TYPE")
.and_then(|s| AlternativeMediaType::from_str(s).ok()) .and_then(|s| AlternativeMediaType::from_str(s.to_string().as_str()).ok())
.unwrap_or_default(), .unwrap_or_default(),
uri: attrs.remove("URI"), uri: attrs.remove("URI").map(|u| u.to_string()),
group_id: attrs.remove("GROUP-ID").unwrap_or_default(), group_id: attrs.remove("GROUP-ID").unwrap_or_default().to_string(),
language: attrs.remove("LANGUAGE"), language: attrs.remove("LANGUAGE").map(|l| l.to_string()),
assoc_language: attrs.remove("ASSOC-LANGUAGE"), assoc_language: attrs.remove("ASSOC-LANGUAGE").map(|a| a.to_string()),
name: attrs.remove("NAME").unwrap_or_default(), name: attrs.remove("NAME").unwrap_or_default().to_string(),
default: bool_default_false!(attrs.remove("DEFAULT")), default: bool_default_false!(attrs.remove("DEFAULT").map(|s| s.to_string())),
autoselect: bool_default_false!(attrs.remove("AUTOSELECT")), autoselect: bool_default_false!(attrs.remove("AUTOSELECT").map(|s| s.to_string())),
forced: bool_default_false!(attrs.remove("FORCED")), forced: bool_default_false!(attrs.remove("FORCED").map(|f| f.to_string())),
instream_id: attrs.remove("INSTREAM-ID"), instream_id: attrs.remove("INSTREAM-ID").map(|i| i.to_string()),
characteristics: attrs.remove("CHARACTERISTICS"), characteristics: attrs.remove("CHARACTERISTICS").map(|c| c.to_string()),
channels: attrs.remove("CHANNELS"), channels: attrs.remove("CHANNELS").map(|c| c.to_string()),
} }
} }
@ -346,14 +345,16 @@ pub struct SessionData {
} }
impl SessionData { impl SessionData {
pub fn from_hashmap(mut attrs: HashMap<String, String>) -> Result<SessionData, String> { pub fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<SessionData, String> {
let data_id = match attrs.remove("DATA-ID") { let data_id = match attrs.remove("DATA-ID") {
Some(data_id) => data_id, Some(data_id) => data_id,
None => return Err("EXT-X-SESSION-DATA field without DATA-ID".to_string()), None => return Err("EXT-X-SESSION-DATA field without DATA-ID".to_string()),
}; };
let value = attrs.remove("VALUE"); let value = attrs.remove("VALUE").map(|v| v.to_string());
let uri = attrs.remove("URI"); let uri = attrs.remove("URI").map(|u| u.to_string());
// SessionData must contain either a VALUE or a URI, // SessionData must contain either a VALUE or a URI,
// but not both https://tools.ietf.org/html/rfc8216#section-4.3.4.4 // but not both https://tools.ietf.org/html/rfc8216#section-4.3.4.4
@ -375,9 +376,9 @@ impl SessionData {
}; };
Ok(SessionData { Ok(SessionData {
data_id, data_id: data_id.to_string(),
field, field,
language: attrs.remove("LANGUAGE"), language: attrs.remove("LANGUAGE").map(|s| s.to_string()),
}) })
} }
@ -591,13 +592,13 @@ pub struct Key {
} }
impl Key { impl Key {
pub fn from_hashmap(mut attrs: HashMap<String, String>) -> Key { pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Key {
Key { Key {
method: attrs.remove("METHOD").unwrap_or_default(), method: attrs.remove("METHOD").unwrap_or_default().to_string(),
uri: attrs.remove("URI"), uri: attrs.remove("URI").map(|u| u.to_string()),
iv: attrs.remove("IV"), iv: attrs.remove("IV").map(|i| i.to_string()),
keyformat: attrs.remove("KEYFORMAT"), keyformat: attrs.remove("KEYFORMAT").map(|k| k.to_string()),
keyformatversions: attrs.remove("KEYFORMATVERSIONS"), keyformatversions: attrs.remove("KEYFORMATVERSIONS").map(|k| k.to_string()),
} }
} }
@ -689,10 +690,13 @@ pub struct Start {
} }
impl Start { impl Start {
pub fn from_hashmap(mut attrs: HashMap<String, String>) -> Start { pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Start {
Start { Start {
time_offset: attrs.remove("TIME-OFFSET").unwrap_or_default(), time_offset: attrs.remove("TIME-OFFSET").unwrap_or_default().to_string(),
precise: attrs.remove("PRECISE").or_else(|| Some("NO".to_string())), precise: attrs
.remove("PRECISE")
.map(|a| a.to_string())
.or_else(|| Some("NO".to_string())),
} }
} }