From 1966a7608de4e1323d51cefe7375ecec741472c3 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 8 Sep 2019 11:30:52 +0200 Subject: [PATCH 01/25] removed QuotedString --- src/master_playlist.rs | 10 +- .../master_playlist/i_frame_stream_inf.rs | 38 +++--- src/tags/master_playlist/media.rs | 119 +++++++++--------- src/tags/master_playlist/session_data.rs | 55 ++++---- src/tags/master_playlist/session_key.rs | 9 +- src/tags/master_playlist/stream_inf.rs | 36 +++--- src/tags/media_segment/date_range.rs | 45 +++---- src/tags/media_segment/key.rs | 10 +- src/tags/media_segment/map.rs | 31 ++--- src/types/closed_captions.rs | 12 +- src/types/decryption_key.rs | 21 ++-- src/types/mod.rs | 2 - src/types/quoted_string.rs | 62 --------- src/types/session_data.rs | 6 +- src/utils.rs | 72 ++++++++++- 15 files changed, 256 insertions(+), 272 deletions(-) delete mode 100644 src/types/quoted_string.rs diff --git a/src/master_playlist.rs b/src/master_playlist.rs index d33e0ff..40fc065 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -3,7 +3,7 @@ use crate::tags::{ ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag, }; -use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, QuotedString}; +use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::{Error, ErrorKind, Result}; use std::collections::HashSet; use std::fmt; @@ -22,6 +22,7 @@ pub struct MasterPlaylistBuilder { session_data_tags: Vec, session_key_tags: Vec, } + impl MasterPlaylistBuilder { /// Makes a new `MasterPlaylistBuilder` instance. pub fn new() -> Self { @@ -209,12 +210,13 @@ impl MasterPlaylistBuilder { Ok(()) } - fn check_media_group(&self, media_type: MediaType, group_id: &QuotedString) -> bool { + fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { self.media_tags .iter() - .any(|t| t.media_type() == media_type && t.group_id() == group_id) + .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) } } + impl Default for MasterPlaylistBuilder { fn default() -> Self { Self::new() @@ -233,6 +235,7 @@ pub struct MasterPlaylist { session_data_tags: Vec, session_key_tags: Vec, } + impl MasterPlaylist { /// Returns the `EXT-X-VERSION` tag contained in the playlist. pub fn version_tag(&self) -> ExtXVersion { @@ -274,6 +277,7 @@ impl MasterPlaylist { &self.session_key_tags } } + impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 3850e47..e287fcb 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -1,6 +1,7 @@ use crate::attribute::AttributePairs; -use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion, QuotedString}; +use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion}; use crate::utils::parse_u64; +use crate::utils::{quote, unquote}; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; @@ -10,22 +11,22 @@ use std::str::FromStr; /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXIFrameStreamInf { - uri: QuotedString, + uri: String, bandwidth: u64, average_bandwidth: Option, - codecs: Option, + codecs: Option, resolution: Option, hdcp_level: Option, - video: Option, + video: Option, } impl ExtXIFrameStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; /// Makes a new `ExtXIFrameStreamInf` tag. - pub fn new(uri: QuotedString, bandwidth: u64) -> Self { + pub fn new(uri: T, bandwidth: u64) -> Self { ExtXIFrameStreamInf { - uri, + uri: uri.to_string(), bandwidth, average_bandwidth: None, codecs: None, @@ -36,7 +37,7 @@ impl ExtXIFrameStreamInf { } /// Returns the URI that identifies the associated media playlist. - pub fn uri(&self) -> &QuotedString { + pub fn uri(&self) -> &String { &self.uri } @@ -51,7 +52,7 @@ impl ExtXIFrameStreamInf { } /// Returns a string that represents the list of codec types contained the variant stream. - pub fn codecs(&self) -> Option<&QuotedString> { + pub fn codecs(&self) -> Option<&String> { self.codecs.as_ref() } @@ -66,7 +67,7 @@ impl ExtXIFrameStreamInf { } /// Returns the group identifier for the video in the variant stream. - pub fn video(&self) -> Option<&QuotedString> { + pub fn video(&self) -> Option<&String> { self.video.as_ref() } @@ -79,13 +80,14 @@ impl ExtXIFrameStreamInf { impl fmt::Display for ExtXIFrameStreamInf { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - write!(f, "URI={}", self.uri)?; + write!(f, "URI={}", quote(&self.uri))?; write!(f, ",BANDWIDTH={}", self.bandwidth)?; + if let Some(ref x) = self.average_bandwidth { write!(f, ",AVERAGE-BANDWIDTH={}", x)?; } if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", x)?; + write!(f, ",CODECS={}", quote(x))?; } if let Some(ref x) = self.resolution { write!(f, ",RESOLUTION={}", x)?; @@ -94,7 +96,7 @@ impl fmt::Display for ExtXIFrameStreamInf { write!(f, ",HDCP-LEVEL={}", x)?; } if let Some(ref x) = self.video { - write!(f, ",VIDEO={}", x)?; + write!(f, ",VIDEO={}", quote(x))?; } Ok(()) } @@ -116,13 +118,13 @@ impl FromStr for ExtXIFrameStreamInf { for attr in attrs { let (key, value) = track!(attr)?; match key { - "URI" => uri = Some(track!(value.parse())?), + "URI" => uri = Some(unquote(value)), "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), - "CODECS" => codecs = Some(track!(value.parse())?), + "CODECS" => codecs = Some(unquote(value)), "RESOLUTION" => resolution = Some(track!(value.parse())?), "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), - "VIDEO" => video = Some(track!(value.parse())?), + "VIDEO" => video = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -150,14 +152,10 @@ mod test { #[test] fn ext_x_i_frame_stream_inf() { - let tag = ExtXIFrameStreamInf::new(quoted_string("foo"), 1000); + let tag = ExtXIFrameStreamInf::new("foo", 1000); let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V1); } - - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() - } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 82ee440..24d2c22 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -1,6 +1,6 @@ use crate::attribute::AttributePairs; -use crate::types::{InStreamId, MediaType, ProtocolVersion, QuotedString}; -use crate::utils::parse_yes_or_no; +use crate::types::{InStreamId, MediaType, ProtocolVersion}; +use crate::utils::{parse_yes_or_no, quote, unquote}; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; @@ -9,17 +9,17 @@ use std::str::FromStr; #[derive(Debug, Clone)] pub struct ExtXMediaBuilder { media_type: Option, - uri: Option, - group_id: Option, - language: Option, - assoc_language: Option, - name: Option, + uri: Option, + group_id: Option, + language: Option, + assoc_language: Option, + name: Option, default: bool, autoselect: Option, forced: Option, instream_id: Option, - characteristics: Option, - channels: Option, + characteristics: Option, + channels: Option, } impl ExtXMediaBuilder { @@ -48,32 +48,32 @@ impl ExtXMediaBuilder { } /// Sets the identifier that specifies the group to which the rendition belongs. - pub fn group_id(&mut self, group_id: QuotedString) -> &mut Self { - self.group_id = Some(group_id); + pub fn group_id(&mut self, group_id: T) -> &mut Self { + self.group_id = Some(group_id.to_string()); self } /// Sets a human-readable description of the rendition. - pub fn name(&mut self, name: QuotedString) -> &mut Self { - self.name = Some(name); + pub fn name(&mut self, name: T) -> &mut Self { + self.name = Some(name.to_string()); self } /// Sets the URI that identifies the media playlist. - pub fn uri(&mut self, uri: QuotedString) -> &mut Self { - self.uri = Some(uri); + pub fn uri(&mut self, uri: T) -> &mut Self { + self.uri = Some(uri.to_string()); self } /// Sets the name of the primary language used in the rendition. - pub fn language(&mut self, language: QuotedString) -> &mut Self { - self.language = Some(language); + pub fn language(&mut self, language: T) -> &mut Self { + self.language = Some(language.to_string()); self } /// Sets the name of a language associated with the rendition. - pub fn assoc_language(&mut self, language: QuotedString) -> &mut Self { - self.assoc_language = Some(language); + pub fn assoc_language(&mut self, language: T) -> &mut Self { + self.assoc_language = Some(language.to_string()); self } @@ -102,14 +102,14 @@ impl ExtXMediaBuilder { } /// Sets the string that represents uniform type identifiers (UTI). - pub fn characteristics(&mut self, characteristics: QuotedString) -> &mut Self { - self.characteristics = Some(characteristics); + pub fn characteristics(&mut self, characteristics: T) -> &mut Self { + self.characteristics = Some(characteristics.to_string()); self } /// Sets the string that represents the parameters of the rendition. - pub fn channels(&mut self, channels: QuotedString) -> &mut Self { - self.channels = Some(channels); + pub fn channels(&mut self, channels: T) -> &mut Self { + self.channels = Some(channels.to_string()); self } @@ -159,31 +159,31 @@ impl Default for ExtXMediaBuilder { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXMedia { media_type: MediaType, - uri: Option, - group_id: QuotedString, - language: Option, - assoc_language: Option, - name: QuotedString, + uri: Option, + group_id: String, + language: Option, + assoc_language: Option, + name: String, default: bool, autoselect: bool, forced: bool, instream_id: Option, - characteristics: Option, - channels: Option, + characteristics: Option, + channels: Option, } impl ExtXMedia { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; /// Makes a new `ExtXMedia` tag. - pub fn new(media_type: MediaType, group_id: QuotedString, name: QuotedString) -> Self { + pub fn new(media_type: MediaType, group_id: T, name: T) -> Self { ExtXMedia { media_type, uri: None, - group_id, + group_id: group_id.to_string(), language: None, assoc_language: None, - name, + name: name.to_string(), default: false, autoselect: false, forced: false, @@ -199,27 +199,27 @@ impl ExtXMedia { } /// Returns the identifier that specifies the group to which the rendition belongs. - pub fn group_id(&self) -> &QuotedString { + pub fn group_id(&self) -> &String { &self.group_id } /// Returns a human-readable description of the rendition. - pub fn name(&self) -> &QuotedString { + pub fn name(&self) -> &String { &self.name } /// Returns the URI that identifies the media playlist. - pub fn uri(&self) -> Option<&QuotedString> { + pub fn uri(&self) -> Option<&String> { self.uri.as_ref() } /// Returns the name of the primary language used in the rendition. - pub fn language(&self) -> Option<&QuotedString> { + pub fn language(&self) -> Option<&String> { self.language.as_ref() } /// Returns the name of a language associated with the rendition. - pub fn assoc_language(&self) -> Option<&QuotedString> { + pub fn assoc_language(&self) -> Option<&String> { self.assoc_language.as_ref() } @@ -247,12 +247,12 @@ impl ExtXMedia { /// Returns a string that represents uniform type identifiers (UTI). /// /// Each UTI indicates an individual characteristic of the rendition. - pub fn characteristics(&self) -> Option<&QuotedString> { + pub fn characteristics(&self) -> Option<&String> { self.characteristics.as_ref() } /// Returns a string that represents the parameters of the rendition. - pub fn channels(&self) -> Option<&QuotedString> { + pub fn channels(&self) -> Option<&String> { self.channels.as_ref() } @@ -274,16 +274,16 @@ impl fmt::Display for ExtXMedia { write!(f, "{}", Self::PREFIX)?; write!(f, "TYPE={}", self.media_type)?; if let Some(ref x) = self.uri { - write!(f, ",URI={}", x)?; + write!(f, ",URI={}", quote(x))?; } - write!(f, ",GROUP-ID={}", self.group_id)?; + write!(f, ",GROUP-ID={}", quote(&self.group_id))?; if let Some(ref x) = self.language { - write!(f, ",LANGUAGE={}", x)?; + write!(f, ",LANGUAGE={}", quote(x))?; } if let Some(ref x) = self.assoc_language { - write!(f, ",ASSOC-LANGUAGE={}", x)?; + write!(f, ",ASSOC-LANGUAGE={}", quote(x))?; } - write!(f, ",NAME={}", self.name)?; + write!(f, ",NAME={}", quote(&self.name))?; if self.default { write!(f, ",DEFAULT=YES")?; } @@ -294,13 +294,13 @@ impl fmt::Display for ExtXMedia { write!(f, ",FORCED=YES")?; } if let Some(ref x) = self.instream_id { - write!(f, ",INSTREAM-ID=\"{}\"", x)?; + write!(f, ",INSTREAM-ID={}", quote(x))?; } if let Some(ref x) = self.characteristics { - write!(f, ",CHARACTERISTICS={}", x)?; + write!(f, ",CHARACTERISTICS={}", quote(x))?; } if let Some(ref x) = self.channels { - write!(f, ",CHANNELS={}", x)?; + write!(f, ",CHANNELS={}", quote(x))?; } Ok(()) } @@ -320,19 +320,19 @@ impl FromStr for ExtXMedia { builder.media_type(track!(value.parse())?); } "URI" => { - builder.uri(track!(value.parse())?); + builder.uri(unquote(value)); } "GROUP-ID" => { - builder.group_id(track!(value.parse())?); + builder.group_id(unquote(value)); } "LANGUAGE" => { - builder.language(track!(value.parse())?); + builder.language(unquote(value)); } "ASSOC-LANGUAGE" => { - builder.assoc_language(track!(value.parse())?); + builder.assoc_language(unquote(value)); } "NAME" => { - builder.name(track!(value.parse())?); + builder.name(unquote(value)); } "DEFAULT" => { builder.default(track!(parse_yes_or_no(value))?); @@ -344,14 +344,13 @@ impl FromStr for ExtXMedia { builder.forced(track!(parse_yes_or_no(value))?); } "INSTREAM-ID" => { - let s: QuotedString = track!(value.parse())?; - builder.instream_id(track!(s.parse())?); + builder.instream_id(unquote(value).parse()?); } "CHARACTERISTICS" => { - builder.characteristics(track!(value.parse())?); + builder.characteristics(unquote(value)); } "CHANNELS" => { - builder.channels(track!(value.parse())?); + builder.channels(unquote(value)); } _ => { // [6.3.1. General Client Responsibilities] @@ -369,14 +368,10 @@ mod test { #[test] fn ext_x_media() { - let tag = ExtXMedia::new(MediaType::Audio, quoted_string("foo"), quoted_string("bar")); + let tag = ExtXMedia::new(MediaType::Audio, "foo", "bar"); let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#; assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V1); } - - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() - } } diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 3ddc153..75da90c 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -1,5 +1,6 @@ use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, QuotedString, SessionData}; +use crate::types::{ProtocolVersion, SessionData}; +use crate::utils::{quote, unquote}; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; @@ -9,34 +10,34 @@ use std::str::FromStr; /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXSessionData { - data_id: QuotedString, + data_id: String, data: SessionData, - language: Option, + language: Option, } impl ExtXSessionData { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:"; /// Makes a new `ExtXSessionData` tag. - pub fn new(data_id: QuotedString, data: SessionData) -> Self { + pub fn new(data_id: T, data: SessionData) -> Self { ExtXSessionData { - data_id, + data_id: data_id.to_string(), data, language: None, } } /// Makes a new `ExtXSessionData` with the given language. - pub fn with_language(data_id: QuotedString, data: SessionData, language: QuotedString) -> Self { + pub fn with_language(data_id: T, data: SessionData, language: T) -> Self { ExtXSessionData { - data_id, + data_id: data_id.to_string(), data, - language: Some(language), + language: Some(language.to_string()), } } /// Returns the identifier of the data. - pub fn data_id(&self) -> &QuotedString { + pub fn data_id(&self) -> &String { &self.data_id } @@ -46,7 +47,7 @@ impl ExtXSessionData { } /// Returns the language of the data. - pub fn language(&self) -> Option<&QuotedString> { + pub fn language(&self) -> Option<&String> { self.language.as_ref() } @@ -59,13 +60,13 @@ impl ExtXSessionData { impl fmt::Display for ExtXSessionData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - write!(f, "DATA-ID={}", self.data_id)?; + write!(f, "DATA-ID={}", quote(&self.data_id))?; match self.data { - SessionData::Value(ref x) => write!(f, ",VALUE={}", x)?, - SessionData::Uri(ref x) => write!(f, ",URI={}", x)?, + SessionData::Value(ref x) => write!(f, ",VALUE={}", quote(x))?, + SessionData::Uri(ref x) => write!(f, ",URI={}", quote(x))?, } if let Some(ref x) = self.language { - write!(f, ",LANGUAGE={}", x)?; + write!(f, ",LANGUAGE={}", quote(x))?; } Ok(()) } @@ -84,10 +85,10 @@ impl FromStr for ExtXSessionData { for attr in attrs { let (key, value) = track!(attr)?; match key { - "DATA-ID" => data_id = Some(track!(value.parse())?), - "VALUE" => session_value = Some(track!(value.parse())?), - "URI" => uri = Some(track!(value.parse())?), - "LANGUAGE" => language = Some(track!(value.parse())?), + "DATA-ID" => data_id = Some(unquote(value)), + "VALUE" => session_value = Some(unquote(value)), + "URI" => uri = Some(unquote(value)), + "LANGUAGE" => language = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -118,34 +119,22 @@ mod test { #[test] fn ext_x_session_data() { - let tag = ExtXSessionData::new( - quoted_string("foo"), - SessionData::Value(quoted_string("bar")), - ); + let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V1); - let tag = - ExtXSessionData::new(quoted_string("foo"), SessionData::Uri(quoted_string("bar"))); + let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V1); - let tag = ExtXSessionData::with_language( - quoted_string("foo"), - SessionData::Value(quoted_string("bar")), - quoted_string("baz"), - ); + let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V1); } - - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() - } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index dc0838d..aafae51 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -38,6 +38,7 @@ impl fmt::Display for ExtXSessionKey { impl FromStr for ExtXSessionKey { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); let suffix = s.split_at(Self::PREFIX.len()).1; @@ -49,13 +50,13 @@ impl FromStr for ExtXSessionKey { #[cfg(test)] mod test { use super::*; - use crate::types::{EncryptionMethod, InitializationVector, QuotedString}; + use crate::types::{EncryptionMethod, InitializationVector}; #[test] fn ext_x_session_key() { let tag = ExtXSessionKey::new(DecryptionKey { method: EncryptionMethod::Aes128, - uri: quoted_string("foo"), + uri: "foo".to_string(), iv: Some(InitializationVector([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ])), @@ -68,8 +69,4 @@ mod test { assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V2); } - - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() - } } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index d919e4d..28e5bcd 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -1,9 +1,9 @@ use crate::attribute::AttributePairs; use crate::types::{ ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion, - QuotedString, SingleLineString, + SingleLineString, }; -use crate::utils::parse_u64; +use crate::utils::{parse_u64, quote, unquote}; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; @@ -16,13 +16,13 @@ pub struct ExtXStreamInf { uri: SingleLineString, bandwidth: u64, average_bandwidth: Option, - codecs: Option, + codecs: Option, resolution: Option, frame_rate: Option, hdcp_level: Option, - audio: Option, - video: Option, - subtitles: Option, + audio: Option, + video: Option, + subtitles: Option, closed_captions: Option, } @@ -62,7 +62,7 @@ impl ExtXStreamInf { } /// Returns a string that represents the list of codec types contained the variant stream. - pub fn codecs(&self) -> Option<&QuotedString> { + pub fn codecs(&self) -> Option<&String> { self.codecs.as_ref() } @@ -82,17 +82,17 @@ impl ExtXStreamInf { } /// Returns the group identifier for the audio in the variant stream. - pub fn audio(&self) -> Option<&QuotedString> { + pub fn audio(&self) -> Option<&String> { self.audio.as_ref() } /// Returns the group identifier for the video in the variant stream. - pub fn video(&self) -> Option<&QuotedString> { + pub fn video(&self) -> Option<&String> { self.video.as_ref() } /// Returns the group identifier for the subtitles in the variant stream. - pub fn subtitles(&self) -> Option<&QuotedString> { + pub fn subtitles(&self) -> Option<&String> { self.subtitles.as_ref() } @@ -115,7 +115,7 @@ impl fmt::Display for ExtXStreamInf { write!(f, ",AVERAGE-BANDWIDTH={}", x)?; } if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", x)?; + write!(f, ",CODECS={}", quote(x))?; } if let Some(ref x) = self.resolution { write!(f, ",RESOLUTION={}", x)?; @@ -127,13 +127,13 @@ impl fmt::Display for ExtXStreamInf { write!(f, ",HDCP-LEVEL={}", x)?; } if let Some(ref x) = self.audio { - write!(f, ",AUDIO={}", x)?; + write!(f, ",AUDIO={}", quote(x))?; } if let Some(ref x) = self.video { - write!(f, ",VIDEO={}", x)?; + write!(f, ",VIDEO={}", quote(x))?; } if let Some(ref x) = self.subtitles { - write!(f, ",SUBTITLES={}", x)?; + write!(f, ",SUBTITLES={}", quote(x))?; } if let Some(ref x) = self.closed_captions { write!(f, ",CLOSED-CAPTIONS={}", x)?; @@ -171,13 +171,13 @@ impl FromStr for ExtXStreamInf { match key { "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), - "CODECS" => codecs = Some(track!(value.parse())?), + "CODECS" => codecs = Some(unquote(value)), "RESOLUTION" => resolution = Some(track!(value.parse())?), "FRAME-RATE" => frame_rate = Some(track!(value.parse())?), "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), - "AUDIO" => audio = Some(track!(value.parse())?), - "VIDEO" => video = Some(track!(value.parse())?), - "SUBTITLES" => subtitles = Some(track!(value.parse())?), + "AUDIO" => audio = Some(unquote(value)), + "VIDEO" => video = Some(unquote(value)), + "SUBTITLES" => subtitles = Some(unquote(value)), "CLOSED-CAPTIONS" => closed_captions = Some(track!(value.parse())?), _ => { // [6.3.1. General Client Responsibilities] diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index b406289..788bd8c 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -1,5 +1,6 @@ use crate::attribute::AttributePairs; -use crate::types::{DecimalFloatingPoint, ProtocolVersion, QuotedString}; +use crate::types::{DecimalFloatingPoint, ProtocolVersion}; +use crate::utils::{quote, unquote}; use crate::{Error, ErrorKind, Result}; use std::collections::BTreeMap; use std::fmt; @@ -14,15 +15,15 @@ use std::time::Duration; #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXDateRange { - pub id: QuotedString, - pub class: Option, - pub start_date: QuotedString, - pub end_date: Option, + pub id: String, + pub class: Option, + pub start_date: String, + pub end_date: Option, pub duration: Option, pub planned_duration: Option, - pub scte35_cmd: Option, - pub scte35_out: Option, - pub scte35_in: Option, + pub scte35_cmd: Option, + pub scte35_out: Option, + pub scte35_in: Option, pub end_on_next: bool, pub client_attributes: BTreeMap, } @@ -39,13 +40,13 @@ impl ExtXDateRange { impl fmt::Display for ExtXDateRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - write!(f, "ID={}", self.id)?; + write!(f, "ID={}", quote(&self.id))?; if let Some(ref x) = self.class { - write!(f, ",CLASS={}", x)?; + write!(f, ",CLASS={}", quote(x))?; } - write!(f, ",START-DATE={}", self.start_date)?; + write!(f, ",START-DATE={}", quote(&self.start_date))?; if let Some(ref x) = self.end_date { - write!(f, ",END-DATE={}", x)?; + write!(f, ",END-DATE={}", quote(x))?; } if let Some(x) = self.duration { write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?; @@ -58,13 +59,13 @@ impl fmt::Display for ExtXDateRange { )?; } if let Some(ref x) = self.scte35_cmd { - write!(f, ",SCTE35-CMD={}", x)?; + write!(f, ",SCTE35-CMD={}", quote(x))?; } if let Some(ref x) = self.scte35_out { - write!(f, ",SCTE35-OUT={}", x)?; + write!(f, ",SCTE35-OUT={}", quote(x))?; } if let Some(ref x) = self.scte35_in { - write!(f, ",SCTE35-IN={}", x)?; + write!(f, ",SCTE35-IN={}", quote(x))?; } if self.end_on_next { write!(f, ",END-ON-NEXT=YES",)?; @@ -96,10 +97,10 @@ impl FromStr for ExtXDateRange { for attr in attrs { let (key, value) = track!(attr)?; match key { - "ID" => id = Some(track!(value.parse())?), - "CLASS" => class = Some(track!(value.parse())?), - "START-DATE" => start_date = Some(track!(value.parse())?), - "END-DATE" => end_date = Some(track!(value.parse())?), + "ID" => id = Some(unquote(value)), + "CLASS" => class = Some(unquote(value)), + "START-DATE" => start_date = Some(unquote(value)), + "END-DATE" => end_date = Some(unquote(value)), "DURATION" => { let seconds: DecimalFloatingPoint = track!(value.parse())?; duration = Some(seconds.to_duration()); @@ -108,9 +109,9 @@ impl FromStr for ExtXDateRange { let seconds: DecimalFloatingPoint = track!(value.parse())?; planned_duration = Some(seconds.to_duration()); } - "SCTE35-CMD" => scte35_cmd = Some(track!(value.parse())?), - "SCTE35-OUT" => scte35_out = Some(track!(value.parse())?), - "SCTE35-IN" => scte35_in = Some(track!(value.parse())?), + "SCTE35-CMD" => scte35_cmd = Some(unquote(value)), + "SCTE35-OUT" => scte35_out = Some(unquote(value)), + "SCTE35-IN" => scte35_in = Some(unquote(value)), "END-ON-NEXT" => { track_assert_eq!(value, "YES", ErrorKind::InvalidInput); end_on_next = true; diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index e9e535c..9f2811f 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -77,7 +77,7 @@ impl FromStr for ExtXKey { #[cfg(test)] mod test { use super::*; - use crate::types::{EncryptionMethod, InitializationVector, QuotedString}; + use crate::types::{EncryptionMethod, InitializationVector}; #[test] fn ext_x_key() { @@ -89,7 +89,7 @@ mod test { let tag = ExtXKey::new(DecryptionKey { method: EncryptionMethod::Aes128, - uri: QuotedString::new("foo").unwrap(), + uri: "foo".to_string(), iv: None, key_format: None, key_format_versions: None, @@ -101,7 +101,7 @@ mod test { let tag = ExtXKey::new(DecryptionKey { method: EncryptionMethod::Aes128, - uri: QuotedString::new("foo").unwrap(), + uri: "foo".to_string(), iv: Some(InitializationVector([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ])), @@ -115,11 +115,11 @@ mod test { let tag = ExtXKey::new(DecryptionKey { method: EncryptionMethod::Aes128, - uri: QuotedString::new("foo").unwrap(), + uri: "foo".to_string(), iv: Some(InitializationVector([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ])), - key_format: Some(QuotedString::new("baz").unwrap()), + key_format: Some("baz".to_string()), key_format_versions: None, }); let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#; diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 53c408c..96c45f8 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -1,5 +1,6 @@ use crate::attribute::AttributePairs; -use crate::types::{ByteRange, ProtocolVersion, QuotedString}; +use crate::types::{ByteRange, ProtocolVersion}; +use crate::utils::{quote, unquote}; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; @@ -9,7 +10,7 @@ use std::str::FromStr; /// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXMap { - uri: QuotedString, + uri: String, range: Option, } @@ -17,20 +18,23 @@ impl ExtXMap { pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:"; /// Makes a new `ExtXMap` tag. - pub fn new(uri: QuotedString) -> Self { - ExtXMap { uri, range: None } + pub fn new(uri: T) -> Self { + ExtXMap { + uri: uri.to_string(), + range: None, + } } /// Makes a new `ExtXMap` tag with the given range. - pub fn with_range(uri: QuotedString, range: ByteRange) -> Self { + pub fn with_range(uri: T, range: ByteRange) -> Self { ExtXMap { - uri, + uri: uri.to_string(), range: Some(range), } } /// Returns the URI that identifies a resource that contains the media initialization section. - pub fn uri(&self) -> &QuotedString { + pub fn uri(&self) -> &String { &self.uri } @@ -48,9 +52,9 @@ impl ExtXMap { impl fmt::Display for ExtXMap { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - write!(f, "URI={}", self.uri)?; + write!(f, "URI={}", quote(&self.uri))?; if let Some(ref x) = self.range { - write!(f, ",BYTERANGE=\"{}\"", x)?; + write!(f, ",BYTERANGE={}", quote(x))?; } Ok(()) } @@ -67,10 +71,9 @@ impl FromStr for ExtXMap { for attr in attrs { let (key, value) = track!(attr)?; match key { - "URI" => uri = Some(track!(value.parse())?), + "URI" => uri = Some(unquote(value)), "BYTERANGE" => { - let s: QuotedString = track!(value.parse())?; - range = Some(track!(s.parse())?); + range = Some(track!(unquote(value).parse())?); } _ => { // [6.3.1. General Client Responsibilities] @@ -90,14 +93,14 @@ mod test { #[test] fn ext_x_map() { - let tag = ExtXMap::new(QuotedString::new("foo").unwrap()); + let tag = ExtXMap::new("foo"); let text = r#"#EXT-X-MAP:URI="foo""#; assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V6); let tag = ExtXMap::with_range( - QuotedString::new("foo").unwrap(), + "foo", ByteRange { length: 9, start: Some(2), diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index fb4ed85..7f51d9f 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -1,4 +1,4 @@ -use crate::types::QuotedString; +use crate::utils::{quote, unquote}; use crate::{Error, Result}; use std::fmt; use std::str::{self, FromStr}; @@ -11,14 +11,14 @@ use std::str::{self, FromStr}; #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ClosedCaptions { - GroupId(QuotedString), + GroupId(String), None, } impl fmt::Display for ClosedCaptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - ClosedCaptions::GroupId(ref x) => x.fmt(f), + ClosedCaptions::GroupId(ref x) => write!(f, "{}", quote(x)), ClosedCaptions::None => "NONE".fmt(f), } } @@ -30,7 +30,7 @@ impl FromStr for ClosedCaptions { if s == "NONE" { Ok(ClosedCaptions::None) } else { - Ok(ClosedCaptions::GroupId(track!(s.parse())?)) + Ok(ClosedCaptions::GroupId(unquote(s))) } } } @@ -44,7 +44,7 @@ mod tests { let closed_captions = ClosedCaptions::None; assert_eq!(closed_captions.to_string(), "NONE".to_string()); - let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap()); + let closed_captions = ClosedCaptions::GroupId("value".into()); assert_eq!(closed_captions.to_string(), "\"value\"".to_string()); } @@ -53,7 +53,7 @@ mod tests { let closed_captions = ClosedCaptions::None; assert_eq!(closed_captions, "NONE".parse::().unwrap()); - let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap()); + let closed_captions = ClosedCaptions::GroupId("value".into()); assert_eq!( closed_captions, "\"value\"".parse::().unwrap() diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index ac57237..15a6f35 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -1,5 +1,6 @@ use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, QuotedString}; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; +use crate::utils::{quote, unquote}; use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::{self, FromStr}; @@ -13,10 +14,10 @@ use std::str::{self, FromStr}; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct DecryptionKey { pub method: EncryptionMethod, - pub uri: QuotedString, + pub uri: String, pub iv: Option, - pub key_format: Option, - pub key_format_versions: Option, + pub key_format: Option, + pub key_format_versions: Option, } impl DecryptionKey { @@ -34,15 +35,15 @@ impl DecryptionKey { impl fmt::Display for DecryptionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "METHOD={}", self.method)?; - write!(f, ",URI={}", self.uri)?; + write!(f, ",URI={}", quote(&self.uri))?; if let Some(ref x) = self.iv { write!(f, ",IV={}", x)?; } if let Some(ref x) = self.key_format { - write!(f, ",KEYFORMAT={}", x)?; + write!(f, ",KEYFORMAT={}", quote(x))?; } if let Some(ref x) = self.key_format_versions { - write!(f, ",KEYFORMATVERSIONS={}", x)?; + write!(f, ",KEYFORMATVERSIONS={}", quote(x))?; } Ok(()) } @@ -61,10 +62,10 @@ impl FromStr for DecryptionKey { let (key, value) = track!(attr)?; match key { "METHOD" => method = Some(track!(value.parse())?), - "URI" => uri = Some(track!(value.parse())?), + "URI" => uri = Some(unquote(value)), "IV" => iv = Some(track!(value.parse())?), - "KEYFORMAT" => key_format = Some(track!(value.parse())?), - "KEYFORMATVERSIONS" => key_format_versions = Some(track!(value.parse())?), + "KEYFORMAT" => key_format = Some(unquote(value)), + "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. diff --git a/src/types/mod.rs b/src/types/mod.rs index d3f30d2..1b1830d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -12,7 +12,6 @@ mod initialization_vector; mod media_type; mod playlist_type; mod protocol_version; -mod quoted_string; mod session_data; mod signed_decimal_floating_point; mod single_line_string; @@ -30,7 +29,6 @@ pub use initialization_vector::*; pub use media_type::*; pub use playlist_type::*; pub use protocol_version::*; -pub use quoted_string::*; pub use session_data::*; pub use signed_decimal_floating_point::*; pub use single_line_string::*; diff --git a/src/types/quoted_string.rs b/src/types/quoted_string.rs deleted file mode 100644 index 07c6118..0000000 --- a/src/types/quoted_string.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::ops::Deref; -use std::str::{self, FromStr}; - -/// Quoted string. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct QuotedString(String); - -impl QuotedString { - /// Makes a new `QuotedString` instance. - /// - /// # Errors - /// - /// If the given string contains any control characters or double-quote character, - /// this function will return an error which has the kind `ErrorKind::InvalidInput`. - pub fn new>(s: T) -> Result { - let s = s.into(); - track_assert!( - !s.chars().any(|c| c.is_control() || c == '"'), - ErrorKind::InvalidInput - ); - Ok(QuotedString(s)) - } -} - -impl Deref for QuotedString { - type Target = str; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef for QuotedString { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for QuotedString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -impl FromStr for QuotedString { - type Err = Error; - fn from_str(s: &str) -> Result { - let len = s.len(); - let bytes = s.as_bytes(); - track_assert!(len >= 2, ErrorKind::InvalidInput); - track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput); - track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput); - - let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) }; - track!(QuotedString::new(s)) - } -} diff --git a/src/types/session_data.rs b/src/types/session_data.rs index 25c90e1..4875b35 100644 --- a/src/types/session_data.rs +++ b/src/types/session_data.rs @@ -1,5 +1,3 @@ -use crate::types::QuotedString; - /// Session data. /// /// See: [4.3.4.4. EXT-X-SESSION-DATA] @@ -8,6 +6,6 @@ use crate::types::QuotedString; #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SessionData { - Value(QuotedString), - Uri(QuotedString), + Value(String), + Uri(String), } diff --git a/src/utils.rs b/src/utils.rs index b313c66..605eb1d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,15 +1,77 @@ use crate::{ErrorKind, Result}; use trackable::error::ErrorKindExt; -pub fn parse_yes_or_no(s: &str) -> Result { - match s { +pub(crate) fn parse_yes_or_no>(s: T) -> Result { + match s.as_ref() { "YES" => Ok(true), "NO" => Ok(false), - _ => track_panic!(ErrorKind::InvalidInput, "Unexpected value: {:?}", s), + _ => track_panic!( + ErrorKind::InvalidInput, + "Unexpected value: {:?}", + s.as_ref() + ), } } -pub fn parse_u64(s: &str) -> Result { - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; +pub(crate) fn parse_u64>(s: T) -> Result { + let n = track!(s + .as_ref() + .parse() + .map_err(|e| ErrorKind::InvalidInput.cause(e)))?; Ok(n) } + +/// According to the documentation the following characters are forbidden +/// inside a quoted string: +/// - carriage return (`\r`) +/// - new line (`\n`) +/// - double quotes (`"`) +/// +/// Therefore it is safe to simply remove any occurence of those characters. +/// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2) +pub(crate) fn unquote(value: T) -> String { + value + .to_string() + .replace("\"", "") + .replace("\n", "") + .replace("\r", "") +} + +/// Puts a string inside quotes. +pub(crate) fn quote(value: T) -> String { + // the replace is for the case, that quote is called on an already quoted string, which could + // cause problems! + format!("\"{}\"", value.to_string().replace("\"", "")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_yes_or_no() { + assert!(parse_yes_or_no("YES").unwrap()); + assert!(!parse_yes_or_no("NO").unwrap()); + // TODO: test for error + } + + #[test] + fn test_parse_u64() { + assert_eq!(parse_u64("1").unwrap(), 1); + assert_eq!(parse_u64("25").unwrap(), 25); + // TODO: test for error + } + + #[test] + fn test_unquote() { + assert_eq!(unquote("\"TestValue\""), "TestValue".to_string()); + assert_eq!(unquote("\"TestValue\n\""), "TestValue".to_string()); + assert_eq!(unquote("\"TestValue\n\r\""), "TestValue".to_string()); + } + + #[test] + fn test_quote() { + assert_eq!(quote("value"), "\"value\"".to_string()); + assert_eq!(quote("\"value\""), "\"value\"".to_string()); + } +} From cf97a45f60669d8dba4df224801755b23d2f1f55 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 8 Sep 2019 12:23:33 +0200 Subject: [PATCH 02/25] fixed clippy warnings --- Cargo.toml | 3 ++- src/lib.rs | 5 ++++ src/line.rs | 2 +- src/master_playlist.rs | 6 ++--- src/media_playlist.rs | 25 ++++++++++++------- src/media_segment.rs | 12 ++++++--- src/tags/basic/m3u.rs | 6 ++++- src/tags/basic/version.rs | 6 ++--- .../master_playlist/i_frame_stream_inf.rs | 13 +++++----- src/tags/master_playlist/media.rs | 17 +++++++------ src/tags/master_playlist/session_data.rs | 7 +++--- src/tags/master_playlist/session_key.rs | 4 +-- src/tags/master_playlist/stream_inf.rs | 17 +++++++------ .../media_playlist/discontinuity_sequence.rs | 7 +++--- src/tags/media_playlist/end_list.rs | 5 +++- src/tags/media_playlist/i_frames_only.rs | 3 ++- src/tags/media_playlist/media_sequence.rs | 7 +++--- src/tags/media_playlist/playlist_type.rs | 7 +++--- src/tags/media_playlist/target_duration.rs | 6 ++--- src/tags/media_segment/byte_range.rs | 7 +++--- src/tags/media_segment/date_range.rs | 2 +- src/tags/media_segment/discontinuity.rs | 4 ++- src/tags/media_segment/inf.rs | 6 ++--- src/tags/media_segment/key.rs | 5 ++-- src/tags/media_segment/map.rs | 7 +++--- src/tags/media_segment/program_date_time.rs | 7 +++--- src/tags/shared/independent_segments.rs | 3 ++- src/tags/shared/start.rs | 11 ++++---- src/types/decimal_floating_point.rs | 2 +- src/types/signed_decimal_floating_point.rs | 2 +- 30 files changed, 127 insertions(+), 87 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e05780a..f71b574 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ readme = "README.md" license = "MIT" keywords = ["hls", "m3u8"] edition = "2018" +categories = ["parser"] [badges] travis-ci = {repository = "sile/hls_m3u8"} @@ -18,4 +19,4 @@ codecov = {repository = "sile/hls_m3u8"} trackable = "0.2" [dev-dependencies] -clap = "2" \ No newline at end of file +clap = "2" diff --git a/src/lib.rs b/src/lib.rs index 889d757..409ba59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,8 @@ +#![warn( + //clippy::pedantic, + clippy::nursery, + clippy::cargo +)] //! [HLS] m3u8 parser/generator. //! //! [HLS]: https://tools.ietf.org/html/rfc8216 diff --git a/src/line.rs b/src/line.rs index 6b1b1f6..974d01d 100644 --- a/src/line.rs +++ b/src/line.rs @@ -9,7 +9,7 @@ pub struct Lines<'a> { input: &'a str, } impl<'a> Lines<'a> { - pub fn new(input: &'a str) -> Self { + pub const fn new(input: &'a str) -> Self { Lines { input } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 40fc065..48a4453 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -238,17 +238,17 @@ pub struct MasterPlaylist { impl MasterPlaylist { /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub fn version_tag(&self) -> ExtXVersion { + pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. - pub fn independent_segments_tag(&self) -> Option { + pub const fn independent_segments_tag(&self) -> Option { self.independent_segments_tag } /// Returns the `EXT-X-START` tag contained in the playlist. - pub fn start_tag(&self) -> Option { + pub const fn start_tag(&self) -> Option { self.start_tag } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index a25d816..2f5b7a4 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -27,6 +27,7 @@ pub struct MediaPlaylistBuilder { segments: Vec, options: MediaPlaylistOptions, } + impl MediaPlaylistBuilder { /// Makes a new `MediaPlaylistBuilder` instance. pub fn new() -> Self { @@ -180,6 +181,7 @@ impl MediaPlaylistBuilder { .unwrap_or(ProtocolVersion::V1) } } + impl Default for MediaPlaylistBuilder { fn default() -> Self { Self::new() @@ -200,49 +202,50 @@ pub struct MediaPlaylist { end_list_tag: Option, segments: Vec, } + impl MediaPlaylist { /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub fn version_tag(&self) -> ExtXVersion { + pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } /// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist. - pub fn target_duration_tag(&self) -> ExtXTargetDuration { + pub const fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag } /// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist. - pub fn media_sequence_tag(&self) -> Option { + pub const fn media_sequence_tag(&self) -> Option { self.media_sequence_tag } /// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the playlist. - pub fn discontinuity_sequence_tag(&self) -> Option { + pub const fn discontinuity_sequence_tag(&self) -> Option { self.discontinuity_sequence_tag } /// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist. - pub fn playlist_type_tag(&self) -> Option { + pub const fn playlist_type_tag(&self) -> Option { self.playlist_type_tag } /// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist. - pub fn i_frames_only_tag(&self) -> Option { + pub const fn i_frames_only_tag(&self) -> Option { self.i_frames_only_tag } /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. - pub fn independent_segments_tag(&self) -> Option { + pub const fn independent_segments_tag(&self) -> Option { self.independent_segments_tag } /// Returns the `EXT-X-START` tag contained in the playlist. - pub fn start_tag(&self) -> Option { + pub const fn start_tag(&self) -> Option { self.start_tag } /// Returns the `EXT-X-ENDLIST` tag contained in the playlist. - pub fn end_list_tag(&self) -> Option { + pub const fn end_list_tag(&self) -> Option { self.end_list_tag } @@ -251,6 +254,7 @@ impl MediaPlaylist { &self.segments } } + impl fmt::Display for MediaPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; @@ -285,6 +289,7 @@ impl fmt::Display for MediaPlaylist { Ok(()) } } + impl FromStr for MediaPlaylist { type Err = Error; fn from_str(s: &str) -> Result { @@ -297,6 +302,7 @@ impl FromStr for MediaPlaylist { pub struct MediaPlaylistOptions { allowable_excess_duration: Duration, } + impl MediaPlaylistOptions { /// Makes a new `MediaPlaylistOptions` with the default settings. pub fn new() -> Self { @@ -450,6 +456,7 @@ impl MediaPlaylistOptions { track!(builder.finish()) } } + impl Default for MediaPlaylistOptions { fn default() -> Self { Self::new() diff --git a/src/media_segment.rs b/src/media_segment.rs index 7794819..602b51d 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -19,6 +19,7 @@ pub struct MediaSegmentBuilder { inf_tag: Option, uri: Option, } + impl MediaSegmentBuilder { /// Makes a new `MediaSegmentBuilder` instance. pub fn new() -> Self { @@ -70,6 +71,7 @@ impl MediaSegmentBuilder { }) } } + impl Default for MediaSegmentBuilder { fn default() -> Self { Self::new() @@ -88,6 +90,7 @@ pub struct MediaSegment { inf_tag: ExtInf, uri: SingleLineString, } + impl fmt::Display for MediaSegment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for t in &self.key_tags { @@ -113,19 +116,20 @@ impl fmt::Display for MediaSegment { Ok(()) } } + impl MediaSegment { /// Returns the URI of the media segment. - pub fn uri(&self) -> &SingleLineString { + pub const fn uri(&self) -> &SingleLineString { &self.uri } /// Returns the `EXT-X-INF` tag associated with the media segment. - pub fn inf_tag(&self) -> &ExtInf { + pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag } /// Returns the `EXT-X-BYTERANGE` tag associated with the media segment. - pub fn byte_range_tag(&self) -> Option { + pub const fn byte_range_tag(&self) -> Option { self.byte_range_tag } @@ -135,7 +139,7 @@ impl MediaSegment { } /// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment. - pub fn discontinuity_tag(&self) -> Option { + pub const fn discontinuity_tag(&self) -> Option { self.discontinuity_tag } diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 5d44b90..9595a18 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -8,21 +8,25 @@ use std::str::FromStr; /// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtM3u; + impl ExtM3u { pub(crate) const PREFIX: &'static str = "#EXTM3U"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } + impl fmt::Display for ExtM3u { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } + impl FromStr for ExtM3u { type Err = Error; + fn from_str(s: &str) -> Result { track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); Ok(ExtM3u) diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index f0ab35f..c321e21 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -15,17 +15,17 @@ impl ExtXVersion { pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:"; /// Makes a new `ExtXVersion` tag. - pub fn new(version: ProtocolVersion) -> Self { + pub const fn new(version: ProtocolVersion) -> Self { ExtXVersion { version } } /// Returns the protocol compatibility version of the playlist containing this tag. - pub fn version(self) -> ProtocolVersion { + pub const fn version(&self) -> ProtocolVersion { self.version } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index e287fcb..f10082f 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -37,17 +37,17 @@ impl ExtXIFrameStreamInf { } /// Returns the URI that identifies the associated media playlist. - pub fn uri(&self) -> &String { + pub const fn uri(&self) -> &String { &self.uri } /// Returns the peak segment bit rate of the variant stream. - pub fn bandwidth(&self) -> u64 { + pub const fn bandwidth(&self) -> u64 { self.bandwidth } /// Returns the average segment bit rate of the variant stream. - pub fn average_bandwidth(&self) -> Option { + pub const fn average_bandwidth(&self) -> Option { self.average_bandwidth } @@ -57,12 +57,12 @@ impl ExtXIFrameStreamInf { } /// Returns the optimal pixel resolution at which to display all the video in the variant stream. - pub fn resolution(&self) -> Option { + pub const fn resolution(&self) -> Option { self.resolution } /// Returns the HDCP level of the variant stream. - pub fn hdcp_level(&self) -> Option { + pub const fn hdcp_level(&self) -> Option { self.hdcp_level } @@ -72,7 +72,7 @@ impl ExtXIFrameStreamInf { } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -104,6 +104,7 @@ impl fmt::Display for ExtXIFrameStreamInf { impl FromStr for ExtXIFrameStreamInf { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 24d2c22..7c17b3b 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -24,7 +24,7 @@ pub struct ExtXMediaBuilder { impl ExtXMediaBuilder { /// Makes a `ExtXMediaBuilder` instance. - pub fn new() -> Self { + pub const fn new() -> Self { ExtXMediaBuilder { media_type: None, uri: None, @@ -194,17 +194,17 @@ impl ExtXMedia { } /// Returns the type of the media associated with this tag. - pub fn media_type(&self) -> MediaType { + pub const fn media_type(&self) -> MediaType { self.media_type } /// Returns the identifier that specifies the group to which the rendition belongs. - pub fn group_id(&self) -> &String { + pub const fn group_id(&self) -> &String { &self.group_id } /// Returns a human-readable description of the rendition. - pub fn name(&self) -> &String { + pub const fn name(&self) -> &String { &self.name } @@ -224,23 +224,23 @@ impl ExtXMedia { } /// Returns whether this is the default rendition. - pub fn default(&self) -> bool { + pub const fn default(&self) -> bool { self.default } /// Returns whether the client may choose to /// play this rendition in the absence of explicit user preference. - pub fn autoselect(&self) -> bool { + pub const fn autoselect(&self) -> bool { self.autoselect } /// Returns whether the rendition contains content that is considered essential to play. - pub fn forced(&self) -> bool { + pub const fn forced(&self) -> bool { self.forced } /// Returns the identifier that specifies a rendition within the segments in the media playlist. - pub fn instream_id(&self) -> Option { + pub const fn instream_id(&self) -> Option { self.instream_id } @@ -308,6 +308,7 @@ impl fmt::Display for ExtXMedia { impl FromStr for ExtXMedia { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 75da90c..32b1633 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -37,12 +37,12 @@ impl ExtXSessionData { } /// Returns the identifier of the data. - pub fn data_id(&self) -> &String { + pub const fn data_id(&self) -> &String { &self.data_id } /// Returns the session data. - pub fn data(&self) -> &SessionData { + pub const fn data(&self) -> &SessionData { &self.data } @@ -52,7 +52,7 @@ impl ExtXSessionData { } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -74,6 +74,7 @@ impl fmt::Display for ExtXSessionData { impl FromStr for ExtXSessionData { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index aafae51..26cea97 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -15,12 +15,12 @@ impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; /// Makes a new `ExtXSessionKey` tag. - pub fn new(key: DecryptionKey) -> Self { + pub const fn new(key: DecryptionKey) -> Self { ExtXSessionKey { key } } /// Returns a decryption key for the playlist. - pub fn key(&self) -> &DecryptionKey { + pub const fn key(&self) -> &DecryptionKey { &self.key } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 28e5bcd..81c8c28 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -30,7 +30,7 @@ impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; /// Makes a new `ExtXStreamInf` tag. - pub fn new(uri: SingleLineString, bandwidth: u64) -> Self { + pub const fn new(uri: SingleLineString, bandwidth: u64) -> Self { ExtXStreamInf { uri, bandwidth, @@ -47,17 +47,17 @@ impl ExtXStreamInf { } /// Returns the URI that identifies the associated media playlist. - pub fn uri(&self) -> &SingleLineString { + pub const fn uri(&self) -> &SingleLineString { &self.uri } /// Returns the peak segment bit rate of the variant stream. - pub fn bandwidth(&self) -> u64 { + pub const fn bandwidth(&self) -> u64 { self.bandwidth } /// Returns the average segment bit rate of the variant stream. - pub fn average_bandwidth(&self) -> Option { + pub const fn average_bandwidth(&self) -> Option { self.average_bandwidth } @@ -67,17 +67,17 @@ impl ExtXStreamInf { } /// Returns the optimal pixel resolution at which to display all the video in the variant stream. - pub fn resolution(&self) -> Option { + pub const fn resolution(&self) -> Option { self.resolution } /// Returns the maximum frame rate for all the video in the variant stream. - pub fn frame_rate(&self) -> Option { + pub const fn frame_rate(&self) -> Option { self.frame_rate } /// Returns the HDCP level of the variant stream. - pub fn hdcp_level(&self) -> Option { + pub const fn hdcp_level(&self) -> Option { self.hdcp_level } @@ -102,7 +102,7 @@ impl ExtXStreamInf { } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -145,6 +145,7 @@ impl fmt::Display for ExtXStreamInf { impl FromStr for ExtXStreamInf { type Err = Error; + fn from_str(s: &str) -> Result { let mut lines = s.splitn(2, '\n'); let first_line = lines.next().expect("Never fails").trim_end_matches('\r'); diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 9aa6539..07b245b 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -16,18 +16,18 @@ impl ExtXDiscontinuitySequence { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; /// Makes a new `ExtXDiscontinuitySequence` tag. - pub fn new(seq_num: u64) -> Self { + pub const fn new(seq_num: u64) -> Self { ExtXDiscontinuitySequence { seq_num } } /// Returns the discontinuity sequence number of /// the first media segment that appears in the associated playlist. - pub fn seq_num(self) -> u64 { + pub const fn seq_num(self) -> u64 { self.seq_num } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -40,6 +40,7 @@ impl fmt::Display for ExtXDiscontinuitySequence { impl FromStr for ExtXDiscontinuitySequence { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index f8e333f..ddeb5c0 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -12,17 +12,20 @@ impl ExtXEndList { pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V1 } } + impl fmt::Display for ExtXEndList { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } + impl FromStr for ExtXEndList { type Err = Error; + fn from_str(s: &str) -> Result { track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); Ok(ExtXEndList) diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 4bea636..90db7f4 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -13,7 +13,7 @@ impl ExtXIFramesOnly { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V4 } } @@ -26,6 +26,7 @@ impl fmt::Display for ExtXIFramesOnly { impl FromStr for ExtXIFramesOnly { type Err = Error; + fn from_str(s: &str) -> Result { track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); Ok(ExtXIFramesOnly) diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 99be333..47f75bf 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -16,17 +16,17 @@ impl ExtXMediaSequence { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:"; /// Makes a new `ExtXMediaSequence` tag. - pub fn new(seq_num: u64) -> Self { + pub const fn new(seq_num: u64) -> Self { ExtXMediaSequence { seq_num } } /// Returns the sequence number of the first media segment that appears in the associated playlist. - pub fn seq_num(self) -> u64 { + pub const fn seq_num(self) -> u64 { self.seq_num } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -39,6 +39,7 @@ impl fmt::Display for ExtXMediaSequence { impl FromStr for ExtXMediaSequence { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index e2a5f99..16d9672 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -16,17 +16,17 @@ impl ExtXPlaylistType { pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; /// Makes a new `ExtXPlaylistType` tag. - pub fn new(playlist_type: PlaylistType) -> Self { + pub const fn new(playlist_type: PlaylistType) -> Self { ExtXPlaylistType { playlist_type } } /// Returns the type of the associated media playlist. - pub fn playlist_type(self) -> PlaylistType { + pub const fn playlist_type(self) -> PlaylistType { self.playlist_type } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -39,6 +39,7 @@ impl fmt::Display for ExtXPlaylistType { impl FromStr for ExtXPlaylistType { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index a2af667..2f759b9 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -19,18 +19,18 @@ impl ExtXTargetDuration { /// Makes a new `ExtXTargetduration` tag. /// /// Note that the nanoseconds part of the `duration` will be discarded. - pub fn new(duration: Duration) -> Self { + pub const fn new(duration: Duration) -> Self { let duration = Duration::from_secs(duration.as_secs()); ExtXTargetDuration { duration } } /// Returns the maximum media segment duration in the associated playlist. - pub fn duration(&self) -> Duration { + pub const fn duration(&self) -> Duration { self.duration } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 52397ba..bbc5ab1 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -16,17 +16,17 @@ impl ExtXByteRange { pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; /// Makes a new `ExtXByteRange` tag. - pub fn new(range: ByteRange) -> Self { + pub const fn new(range: ByteRange) -> Self { ExtXByteRange { range } } /// Returns the range of the associated media segment. - pub fn range(&self) -> ByteRange { + pub const fn range(&self) -> ByteRange { self.range } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } @@ -39,6 +39,7 @@ impl fmt::Display for ExtXByteRange { impl FromStr for ExtXByteRange { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 788bd8c..dae13ce 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -32,7 +32,7 @@ impl ExtXDateRange { pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 8e23325..d0f3563 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -12,15 +12,17 @@ impl ExtXDiscontinuity { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V1 } } + impl fmt::Display for ExtXDiscontinuity { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } + impl FromStr for ExtXDiscontinuity { type Err = Error; fn from_str(s: &str) -> Result { diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 3543f69..ac215af 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -18,7 +18,7 @@ impl ExtInf { pub(crate) const PREFIX: &'static str = "#EXTINF:"; /// Makes a new `ExtInf` tag. - pub fn new(duration: Duration) -> Self { + pub const fn new(duration: Duration) -> Self { ExtInf { duration, title: None, @@ -26,7 +26,7 @@ impl ExtInf { } /// Makes a new `ExtInf` tag with the given title. - pub fn with_title(duration: Duration, title: SingleLineString) -> Self { + pub const fn with_title(duration: Duration, title: SingleLineString) -> Self { ExtInf { duration, title: Some(title), @@ -34,7 +34,7 @@ impl ExtInf { } /// Returns the duration of the associated media segment. - pub fn duration(&self) -> Duration { + pub const fn duration(&self) -> Duration { self.duration } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index 9f2811f..e9dda27 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -16,14 +16,14 @@ impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; /// Makes a new `ExtXKey` tag. - pub fn new(key: DecryptionKey) -> Self { + pub const fn new(key: DecryptionKey) -> Self { ExtXKey { key: Some(key) } } /// Makes a new `ExtXKey` tag without a decryption key. /// /// This tag has the `METHDO=NONE` attribute. - pub fn new_without_key() -> Self { + pub const fn new_without_key() -> Self { ExtXKey { key: None } } @@ -54,6 +54,7 @@ impl fmt::Display for ExtXKey { impl FromStr for ExtXKey { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); let suffix = s.split_at(Self::PREFIX.len()).1; diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 96c45f8..c5c9933 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -34,17 +34,17 @@ impl ExtXMap { } /// Returns the URI that identifies a resource that contains the media initialization section. - pub fn uri(&self) -> &String { + pub const fn uri(&self) -> &String { &self.uri } /// Returns the range of the media initialization section. - pub fn range(&self) -> Option { + pub const fn range(&self) -> Option { self.range } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V6 } } @@ -62,6 +62,7 @@ impl fmt::Display for ExtXMap { impl FromStr for ExtXMap { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index b694c93..625fdeb 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -15,17 +15,17 @@ impl ExtXProgramDateTime { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; /// Makes a new `ExtXProgramDateTime` tag. - pub fn new(date_time: SingleLineString) -> Self { + pub const fn new(date_time: SingleLineString) -> Self { ExtXProgramDateTime { date_time } } /// Returns the date-time of the first sample of the associated media segment. - pub fn date_time(&self) -> &SingleLineString { + pub const fn date_time(&self) -> &SingleLineString { &self.date_time } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -38,6 +38,7 @@ impl fmt::Display for ExtXProgramDateTime { impl FromStr for ExtXProgramDateTime { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); let suffix = s.split_at(Self::PREFIX.len()).1; diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index d4916f9..2aa3e82 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -12,7 +12,7 @@ impl ExtXIndependentSegments { pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -25,6 +25,7 @@ impl fmt::Display for ExtXIndependentSegments { impl FromStr for ExtXIndependentSegments { type Err = Error; + fn from_str(s: &str) -> Result { track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); Ok(ExtXIndependentSegments) diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 5c301c1..560b144 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -18,7 +18,7 @@ impl ExtXStart { pub(crate) const PREFIX: &'static str = "#EXT-X-START:"; /// Makes a new `ExtXStart` tag. - pub fn new(time_offset: SignedDecimalFloatingPoint) -> Self { + pub const fn new(time_offset: SignedDecimalFloatingPoint) -> Self { ExtXStart { time_offset, precise: false, @@ -26,7 +26,7 @@ impl ExtXStart { } /// Makes a new `ExtXStart` tag with the given `precise` flag. - pub fn with_precise(time_offset: SignedDecimalFloatingPoint, precise: bool) -> Self { + pub const fn with_precise(time_offset: SignedDecimalFloatingPoint, precise: bool) -> Self { ExtXStart { time_offset, precise, @@ -34,18 +34,18 @@ impl ExtXStart { } /// Returns the time offset of the media segments in the playlist. - pub fn time_offset(&self) -> SignedDecimalFloatingPoint { + pub const fn time_offset(&self) -> SignedDecimalFloatingPoint { self.time_offset } /// Returns whether clients should not render media stream whose presentation times are /// prior to the specified time offset. - pub fn precise(&self) -> bool { + pub const fn precise(&self) -> bool { self.precise } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -63,6 +63,7 @@ impl fmt::Display for ExtXStart { impl FromStr for ExtXStart { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 5cd33cc..aa555e9 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -26,7 +26,7 @@ impl DecimalFloatingPoint { } /// Converts `DecimalFloatingPoint` to `f64`. - pub fn as_f64(self) -> f64 { + pub const fn as_f64(self) -> f64 { self.0 } diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 3c50998..999d0a2 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -24,7 +24,7 @@ impl SignedDecimalFloatingPoint { } /// Converts `DecimalFloatingPoint` to `f64`. - pub fn as_f64(self) -> f64 { + pub const fn as_f64(self) -> f64 { self.0 } } From 91c6698f16a87241831040e01cde793f71793a3d Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 8 Sep 2019 12:49:22 +0200 Subject: [PATCH 03/25] added more tests --- src/tags/basic/m3u.rs | 12 ++++- src/tags/basic/version.rs | 48 ++++++++++++++----- .../master_playlist/i_frame_stream_inf.rs | 29 +++++++++-- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 9595a18..2a2c1e2 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -38,9 +38,17 @@ mod test { use super::*; #[test] - fn extm3u() { + fn test_display() { + assert_eq!(ExtM3u.to_string(), "#EXTM3U".to_string()); + } + + #[test] + fn test_parser() { assert_eq!("#EXTM3U".parse::().ok(), Some(ExtM3u)); - assert_eq!(ExtM3u.to_string(), "#EXTM3U"); + } + + #[test] + fn test_requires_version() { assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1); } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index c321e21..cfb968d 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -7,21 +7,19 @@ use std::str::FromStr; /// /// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXVersion { - version: ProtocolVersion, -} +pub struct ExtXVersion(ProtocolVersion); impl ExtXVersion { pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:"; /// Makes a new `ExtXVersion` tag. pub const fn new(version: ProtocolVersion) -> Self { - ExtXVersion { version } + Self(version) } /// Returns the protocol compatibility version of the playlist containing this tag. pub const fn version(&self) -> ProtocolVersion { - self.version + self.0 } /// Returns the protocol compatibility version that this tag requires. @@ -32,17 +30,18 @@ impl ExtXVersion { impl fmt::Display for ExtXVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.version) + write!(f, "{}{}", Self::PREFIX, self.0) } } impl FromStr for ExtXVersion { type Err = Error; + fn from_str(s: &str) -> Result { track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); let suffix = s.split_at(Self::PREFIX.len()).1; let version = track!(suffix.parse())?; - Ok(ExtXVersion { version }) + Ok(ExtXVersion::new(version)) } } @@ -51,11 +50,34 @@ mod test { use super::*; #[test] - fn ext_x_version() { - let tag = ExtXVersion::new(ProtocolVersion::V6); - assert_eq!("#EXT-X-VERSION:6".parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), "#EXT-X-VERSION:6"); - assert_eq!(tag.version(), ProtocolVersion::V6); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_display() { + assert_eq!( + ExtXVersion::new(ProtocolVersion::V6).to_string(), + "#EXT-X-VERSION:6" + ); + } + + #[test] + fn test_parser() { + assert_eq!( + "#EXT-X-VERSION:6".parse().ok(), + Some(ExtXVersion::new(ProtocolVersion::V6)) + ); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ExtXVersion::new(ProtocolVersion::V6).requires_version(), + ProtocolVersion::V1 + ); + } + + #[test] + fn test_version() { + assert_eq!( + ExtXVersion::new(ProtocolVersion::V6).version(), + ProtocolVersion::V6 + ); } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index f10082f..8b64a3b 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -152,11 +152,30 @@ mod test { use super::*; #[test] - fn ext_x_i_frame_stream_inf() { - let tag = ExtXIFrameStreamInf::new("foo", 1000); + fn test_display() { let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + assert_eq!(ExtXIFrameStreamInf::new("foo", 1000).to_string(), text); + } + + #[test] + fn test_parser() { + let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; + let i_frame_stream_inf = ExtXIFrameStreamInf::new("foo", 1000); + assert_eq!( + text.parse::().unwrap(), + i_frame_stream_inf.clone() + ); + + assert_eq!(i_frame_stream_inf.uri(), "foo"); + assert_eq!(i_frame_stream_inf.bandwidth(), 1000); + // TODO: test all the optional fields + } + + #[test] + fn test_requires_version() { + assert_eq!( + ExtXIFrameStreamInf::new("foo", 1000).requires_version(), + ProtocolVersion::V1 + ); } } From 1a35463185b10bfed54a58e484fcb1cbf8bbb3e6 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 10 Sep 2019 11:05:20 +0200 Subject: [PATCH 04/25] updated parser --- Cargo.toml | 1 + src/media_playlist.rs | 2 +- src/tags/basic/m3u.rs | 10 +- src/tags/basic/version.rs | 12 +- .../master_playlist/i_frame_stream_inf.rs | 67 +++------ src/tags/master_playlist/media.rs | 12 +- src/tags/master_playlist/session_data.rs | 68 +++++---- src/tags/master_playlist/session_key.rs | 15 +- src/tags/master_playlist/stream_inf.rs | 25 ++-- .../media_playlist/discontinuity_sequence.rs | 15 +- src/tags/media_segment/byte_range.rs | 131 ++++++++++++++---- src/tags/media_segment/discontinuity.rs | 12 +- src/tags/media_segment/map.rs | 8 +- src/types/byte_range.rs | 56 +++++--- src/utils.rs | 30 ++++ 15 files changed, 289 insertions(+), 175 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f71b574..6bc713e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ codecov = {repository = "sile/hls_m3u8"} [dependencies] trackable = "0.2" +getset = "0.0.8" [dev-dependencies] clap = "2" diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 2f5b7a4..5eca286 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -141,7 +141,7 @@ impl MediaPlaylistBuilder { // CHECK: `#EXT-X-BYTE-RANGE` if let Some(tag) = s.byte_range_tag() { - if tag.range().start.is_none() { + if tag.to_range().start().is_none() { let last_uri = track_assert_some!(last_range_uri, ErrorKind::InvalidInput); track_assert_eq!(last_uri, s.uri(), ErrorKind::InvalidInput); } else { diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 2a2c1e2..07f95ec 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -1,8 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.1.1. EXTM3U] /// /// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 @@ -27,8 +29,8 @@ impl fmt::Display for ExtM3u { impl FromStr for ExtM3u { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtM3u) } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index cfb968d..d81db3f 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -1,8 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.1.2. EXT-X-VERSION] /// /// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 @@ -37,10 +39,8 @@ impl fmt::Display for ExtXVersion { impl FromStr for ExtXVersion { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; - let version = track!(suffix.parse())?; + fn from_str(input: &str) -> Result { + let version = tag(input, Self::PREFIX)?.parse()?; Ok(ExtXVersion::new(version)) } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 8b64a3b..f9e38dc 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -1,22 +1,35 @@ +use std::fmt; +use std::str::FromStr; + +use getset::{Getters, MutGetters, Setters}; + use crate::attribute::AttributePairs; use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion}; use crate::utils::parse_u64; -use crate::utils::{quote, unquote}; -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::str::FromStr; +use crate::utils::{quote, tag, unquote}; +use crate::{Error, ErrorKind}; /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] /// /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Getters, Setters, MutGetters, Debug, Clone, PartialEq, Eq, Hash)] +#[get = "pub"] +#[set = "pub"] +#[get_mut = "pub"] pub struct ExtXIFrameStreamInf { + /// The URI, that identifies the associated media playlist. uri: String, + /// The peak segment bit rate of the variant stream. bandwidth: u64, + /// The average segment bit rate of the variant stream. average_bandwidth: Option, + /// A string that represents the list of codec types contained the variant stream. codecs: Option, + /// The optimal pixel resolution at which to display all the video in the variant stream. resolution: Option, + /// The HDCP level of the variant stream. hdcp_level: Option, + /// The group identifier for the video in the variant stream. video: Option, } @@ -36,41 +49,6 @@ impl ExtXIFrameStreamInf { } } - /// Returns the URI that identifies the associated media playlist. - pub const fn uri(&self) -> &String { - &self.uri - } - - /// Returns the peak segment bit rate of the variant stream. - pub const fn bandwidth(&self) -> u64 { - self.bandwidth - } - - /// Returns the average segment bit rate of the variant stream. - pub const fn average_bandwidth(&self) -> Option { - self.average_bandwidth - } - - /// Returns a string that represents the list of codec types contained the variant stream. - pub fn codecs(&self) -> Option<&String> { - self.codecs.as_ref() - } - - /// Returns the optimal pixel resolution at which to display all the video in the variant stream. - pub const fn resolution(&self) -> Option { - self.resolution - } - - /// Returns the HDCP level of the variant stream. - pub const fn hdcp_level(&self) -> Option { - self.hdcp_level - } - - /// Returns the group identifier for the video in the variant stream. - pub fn video(&self) -> Option<&String> { - self.video.as_ref() - } - /// Returns the protocol compatibility version that this tag requires. pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 @@ -105,8 +83,8 @@ impl fmt::Display for ExtXIFrameStreamInf { impl FromStr for ExtXIFrameStreamInf { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut uri = None; let mut bandwidth = None; @@ -115,7 +93,8 @@ impl FromStr for ExtXIFrameStreamInf { let mut resolution = None; let mut hdcp_level = None; let mut video = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + + let attrs = AttributePairs::parse(input); for attr in attrs { let (key, value) = track!(attr)?; match key { @@ -167,7 +146,7 @@ mod test { ); assert_eq!(i_frame_stream_inf.uri(), "foo"); - assert_eq!(i_frame_stream_inf.bandwidth(), 1000); + assert_eq!(*i_frame_stream_inf.bandwidth(), 1000); // TODO: test all the optional fields } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 7c17b3b..59e2910 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -1,7 +1,7 @@ use crate::attribute::AttributePairs; use crate::types::{InStreamId, MediaType, ProtocolVersion}; -use crate::utils::{parse_yes_or_no, quote, unquote}; -use crate::{Error, ErrorKind, Result}; +use crate::utils::{parse_yes_or_no, quote, tag, unquote}; +use crate::{Error, ErrorKind}; use std::fmt; use std::str::FromStr; @@ -114,7 +114,7 @@ impl ExtXMediaBuilder { } /// Builds a `ExtXMedia` instance. - pub fn finish(self) -> Result { + pub fn finish(self) -> crate::Result { let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput); let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput); let name = track_assert_some!(self.name, ErrorKind::InvalidInput); @@ -309,11 +309,11 @@ impl fmt::Display for ExtXMedia { impl FromStr for ExtXMedia { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut builder = ExtXMediaBuilder::new(); - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + let attrs = AttributePairs::parse(input); for attr in attrs { let (key, value) = track!(attr)?; match key { diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 32b1633..97da40d 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -1,17 +1,26 @@ -use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, SessionData}; -use crate::utils::{quote, unquote}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use getset::{Getters, MutGetters, Setters}; + +use crate::attribute::AttributePairs; +use crate::types::{ProtocolVersion, SessionData}; +use crate::utils::{quote, tag, unquote}; +use crate::{Error, ErrorKind}; + /// [4.3.4.4. EXT-X-SESSION-DATA] /// /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Getters, MutGetters, Setters, Debug, Clone, PartialEq, Eq, Hash)] +#[get = "pub"] +#[set = "pub"] +#[get_mut = "pub"] pub struct ExtXSessionData { + /// The identifier of the data. data_id: String, + /// The session data. data: SessionData, + /// The language of the data. language: Option, } @@ -36,21 +45,6 @@ impl ExtXSessionData { } } - /// Returns the identifier of the data. - pub const fn data_id(&self) -> &String { - &self.data_id - } - - /// Returns the session data. - pub const fn data(&self) -> &SessionData { - &self.data - } - - /// Returns the language of the data. - pub fn language(&self) -> Option<&String> { - self.language.as_ref() - } - /// Returns the protocol compatibility version that this tag requires. pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 @@ -75,14 +69,15 @@ impl fmt::Display for ExtXSessionData { impl FromStr for ExtXSessionData { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut data_id = None; let mut session_value = None; let mut uri = None; let mut language = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + + let attrs = AttributePairs::parse(input); for attr in attrs { let (key, value) = track!(attr)?; match key { @@ -119,23 +114,38 @@ mod test { use super::*; #[test] - fn ext_x_session_data() { + fn test_display() { let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); + } + + #[test] + fn test_parser() { + let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; + assert_eq!(text.parse::().unwrap(), tag); + + let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; + assert_eq!(text.parse::().unwrap(), tag); + + let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; + assert_eq!(text.parse::().unwrap(), tag); + } + + #[test] + fn test_requires_version() { + let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); assert_eq!(tag.requires_version(), ProtocolVersion::V1); } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 26cea97..e2d29ba 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,8 +1,9 @@ -use crate::types::{DecryptionKey, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::{DecryptionKey, ProtocolVersion}; +use crate::utils::tag; + /// [4.3.4.5. EXT-X-SESSION-KEY] /// /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 @@ -37,13 +38,11 @@ impl fmt::Display for ExtXSessionKey { } impl FromStr for ExtXSessionKey { - type Err = Error; + type Err = crate::Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; - let key = track!(suffix.parse())?; - Ok(ExtXSessionKey { key }) + fn from_str(input: &str) -> Result { + let key = tag(input, Self::PREFIX)?.parse()?; + Ok(Self::new(key)) } } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 81c8c28..dbeeef9 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -1,12 +1,13 @@ +use std::fmt; +use std::str::FromStr; + use crate::attribute::AttributePairs; use crate::types::{ ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion, SingleLineString, }; -use crate::utils::{parse_u64, quote, unquote}; -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::str::FromStr; +use crate::utils::{parse_u64, quote, tag, unquote}; +use crate::{Error, ErrorKind}; /// [4.3.4.2. EXT-X-STREAM-INF] /// @@ -146,16 +147,15 @@ impl fmt::Display for ExtXStreamInf { impl FromStr for ExtXStreamInf { type Err = Error; - fn from_str(s: &str) -> Result { - let mut lines = s.splitn(2, '\n'); - let first_line = lines.next().expect("Never fails").trim_end_matches('\r'); - let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + let mut lines = input.lines(); + let first_line = lines.next().ok_or(ErrorKind::InvalidInput)?; // TODO! + let second_line = lines.next().ok_or(ErrorKind::InvalidInput)?; // TODO! + + tag(first_line, Self::PREFIX)?; - track_assert!( - first_line.starts_with(Self::PREFIX), - ErrorKind::InvalidInput - ); let uri = track!(SingleLineString::new(second_line))?; + let mut bandwidth = None; let mut average_bandwidth = None; let mut codecs = None; @@ -186,6 +186,7 @@ impl FromStr for ExtXStreamInf { } } } + let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); Ok(ExtXStreamInf { uri, diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 07b245b..6619db1 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -1,8 +1,8 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; -use trackable::error::ErrorKindExt; + +use crate::types::ProtocolVersion; +use crate::utils::tag; /// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// @@ -39,12 +39,11 @@ impl fmt::Display for ExtXDiscontinuitySequence { } impl FromStr for ExtXDiscontinuitySequence { - type Err = Error; + type Err = crate::Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXDiscontinuitySequence { seq_num }) + fn from_str(input: &str) -> Result { + let seq_num = tag(input, Self::PREFIX)?.parse().unwrap(); // TODO! + Ok(Self::new(seq_num)) } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index bbc5ab1..9afddfa 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -1,39 +1,72 @@ +use std::fmt; +use std::ops::Deref; +use std::str::FromStr; + +use trackable::error::ErrorKindExt; + use crate::types::{ByteRange, ProtocolVersion}; use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::str::FromStr; -use trackable::error::ErrorKindExt; /// [4.3.2.2. EXT-X-BYTERANGE] /// /// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXByteRange { - range: ByteRange, -} +pub struct ExtXByteRange(ByteRange); impl ExtXByteRange { pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; /// Makes a new `ExtXByteRange` tag. - pub const fn new(range: ByteRange) -> Self { - ExtXByteRange { range } + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXByteRange; + /// + /// let byte_range = ExtXByteRange::new(20, Some(5)); + /// ``` + pub const fn new(length: usize, start: Option) -> Self { + Self(ByteRange::new(length, start)) } - /// Returns the range of the associated media segment. - pub const fn range(&self) -> ByteRange { - self.range + /// Converts the [ExtXByteRange] to a [ByteRange]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXByteRange; + /// use hls_m3u8::types::ByteRange; + /// + /// let byte_range = ExtXByteRange::new(20, Some(5)); + /// let range: ByteRange = byte_range.to_range(); + /// ``` + pub const fn to_range(&self) -> ByteRange { + self.0 } /// Returns the protocol compatibility version that this tag requires. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXByteRange; + /// use hls_m3u8::types::ProtocolVersion; + /// + /// let byte_range = ExtXByteRange::new(20, Some(5)); + /// assert_eq!(byte_range.requires_version(), ProtocolVersion::V4); + /// ``` pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } +impl Deref for ExtXByteRange { + type Target = ByteRange; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl fmt::Display for ExtXByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.range) + write!(f, "{}", Self::PREFIX)?; + write!(f, "{}", self.0)?; + Ok(()) } } @@ -41,9 +74,30 @@ impl FromStr for ExtXByteRange { type Err = Error; fn from_str(s: &str) -> Result { + // check if the string starts with the PREFIX track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXByteRange { range }) + let byte_range = s.split_at(Self::PREFIX.len()).1; + let tokens = byte_range.splitn(2, '@').collect::>(); + if tokens.is_empty() { + Err(ErrorKind::InvalidInput)?; + } + + let length = tokens[0] + .parse() + .map_err(|e| ErrorKind::InvalidInput.cause(e))?; + let start = { + let mut result = None; + if tokens.len() == 2 { + result = Some( + tokens[1] + .parse() + .map_err(|e| ErrorKind::InvalidInput.cause(e))?, + ); + } + result + }; + + Ok(ExtXByteRange::new(length, start)) } } @@ -52,21 +106,40 @@ mod test { use super::*; #[test] - fn ext_x_byterange() { - let tag = ExtXByteRange::new(ByteRange { - length: 3, - start: None, - }); - assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3"); - assert_eq!(tag.requires_version(), ProtocolVersion::V4); + fn test_display() { + let byte_range = ExtXByteRange::new(0, Some(5)); + assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:0@5".to_string()); - let tag = ExtXByteRange::new(ByteRange { - length: 3, - start: Some(5), - }); - assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5"); - assert_eq!(tag.requires_version(), ProtocolVersion::V4); + let byte_range = ExtXByteRange::new(99999, Some(2)); + assert_eq!( + byte_range.to_string(), + "#EXT-X-BYTERANGE:99999@2".to_string() + ); + + let byte_range = ExtXByteRange::new(99999, None); + assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:99999".to_string()); + } + + #[test] + fn test_parse() { + let byte_range = ExtXByteRange::new(99999, Some(2)); + assert_eq!( + byte_range, + "#EXT-X-BYTERANGE:99999@2".parse::().unwrap() + ); + + let byte_range = ExtXByteRange::new(99999, None); + assert_eq!( + byte_range, + "#EXT-X-BYTERANGE:99999".parse::().unwrap() + ); + } + + #[test] + fn test_deref() { + let byte_range = ExtXByteRange::new(0, Some(22)); + + assert_eq!(*byte_range.length(), 0); + assert_eq!(*byte_range.start(), Some(22)); } } diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index d0f3563..9571f2c 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -1,13 +1,16 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::{Error, Result}; + /// [4.3.2.3. EXT-X-DISCONTINUITY] /// /// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtXDiscontinuity; + impl ExtXDiscontinuity { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY"; @@ -25,8 +28,9 @@ impl fmt::Display for ExtXDiscontinuity { impl FromStr for ExtXDiscontinuity { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtXDiscontinuity) } } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index c5c9933..1ef468b 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -100,13 +100,7 @@ mod test { assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V6); - let tag = ExtXMap::with_range( - "foo", - ByteRange { - length: 9, - start: Some(2), - }, - ); + let tag = ExtXMap::with_range("foo", ByteRange::new(9, Some(2))); let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#; track_try_unwrap!(ExtXMap::from_str(text)); assert_eq!(text.parse().ok(), Some(tag.clone())); diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index af26870..443ab53 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -1,18 +1,32 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::{self, FromStr}; + +use getset::{Getters, MutGetters, Setters}; use trackable::error::ErrorKindExt; +use crate::{Error, ErrorKind, Result}; + /// Byte range. /// /// See: [4.3.2.2. EXT-X-BYTERANGE] /// /// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Getters, Setters, MutGetters, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[get = "pub"] +#[set = "pub"] +#[get_mut = "pub"] pub struct ByteRange { - pub length: usize, - pub start: Option, + /// The length of the range. + length: usize, + /// The start of the range. + start: Option, +} + +impl ByteRange { + /// Creates a new [ByteRange]. + pub const fn new(length: usize, start: Option) -> Self { + Self { length, start } + } } impl fmt::Display for ByteRange { @@ -27,20 +41,28 @@ impl fmt::Display for ByteRange { impl FromStr for ByteRange { type Err = Error; + fn from_str(s: &str) -> Result { - let mut tokens = s.splitn(2, '@'); - let length = tokens.next().expect("Never fails"); - let start = if let Some(start) = tokens.next() { - Some(track!(start - .parse() - .map_err(|e| ErrorKind::InvalidInput.cause(e)))?) - } else { - None + let tokens = s.splitn(2, '@').collect::>(); + if tokens.is_empty() { + Err(ErrorKind::InvalidInput)?; + } + + let length = tokens[0] + .parse() + .map_err(|e| ErrorKind::InvalidInput.cause(e))?; + let start = { + let mut result = None; + if tokens.len() == 2 { + result = Some( + tokens[1] + .parse() + .map_err(|e| ErrorKind::InvalidInput.cause(e))?, + ); + } + result }; - Ok(ByteRange { - length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, - start, - }) + Ok(ByteRange::new(length, start)) } } diff --git a/src/utils.rs b/src/utils.rs index 605eb1d..946dab9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -44,6 +44,19 @@ pub(crate) fn quote(value: T) -> String { format!("\"{}\"", value.to_string().replace("\"", "")) } +/// Checks, if the given tag is at the start of the input. If this is the case, it will remove it +/// return the rest of the input, otherwise it will return an error. +pub(crate) fn tag(input: &str, tag: T) -> crate::Result<&str> +where + T: AsRef, +{ + if !input.starts_with(tag.as_ref()) { + Err(ErrorKind::InvalidInput)?; // TODO! + } + let result = input.split_at(tag.as_ref().len()).1; + Ok(result) +} + #[cfg(test)] mod tests { use super::*; @@ -74,4 +87,21 @@ mod tests { assert_eq!(quote("value"), "\"value\"".to_string()); assert_eq!(quote("\"value\""), "\"value\"".to_string()); } + + #[test] + fn test_tag() { + let input = "HelloMyFriendThisIsASampleString"; + + let input = tag(input, "Hello").unwrap(); + assert_eq!(input, "MyFriendThisIsASampleString"); + + let input = tag(input, "My").unwrap(); + assert_eq!(input, "FriendThisIsASampleString"); + + let input = tag(input, "FriendThisIs").unwrap(); + assert_eq!(input, "ASampleString"); + + let input = tag(input, "A").unwrap(); + assert_eq!(input, "SampleString"); + } } From c8f3df1228adeedecba7a7b0271b9370793e21d2 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Fri, 13 Sep 2019 16:06:52 +0200 Subject: [PATCH 05/25] New Error type --- Cargo.toml | 2 +- examples/parse.rs | 11 +- src/attribute.rs | 27 +-- src/error.rs | 186 +++++++++++++++++- src/lib.rs | 7 +- src/line.rs | 77 ++++---- src/master_playlist.rs | 143 +++++++------- src/media_playlist.rs | 128 ++++++------ src/media_segment.rs | 14 +- .../master_playlist/i_frame_stream_inf.rs | 17 +- src/tags/master_playlist/media.rs | 52 +++-- src/tags/master_playlist/session_data.rs | 26 ++- src/tags/master_playlist/stream_inf.rs | 26 +-- src/tags/media_playlist/end_list.rs | 10 +- src/tags/media_playlist/i_frames_only.rs | 10 +- src/tags/media_playlist/media_sequence.rs | 15 +- src/tags/media_playlist/playlist_type.rs | 15 +- src/tags/media_playlist/target_duration.rs | 15 +- src/tags/media_segment/byte_range.rs | 27 +-- src/tags/media_segment/date_range.rs | 36 ++-- src/tags/media_segment/inf.rs | 27 +-- src/tags/media_segment/key.rs | 28 +-- src/tags/media_segment/map.rs | 24 +-- src/tags/media_segment/program_date_time.rs | 16 +- src/tags/mod.rs | 6 - src/tags/shared/independent_segments.rs | 10 +- src/tags/shared/start.rs | 26 +-- src/types/byte_range.rs | 20 +- src/types/decimal_floating_point.rs | 25 +-- src/types/decimal_resolution.rs | 18 +- src/types/decryption_key.rs | 25 ++- src/types/encryption_method.rs | 17 +- src/types/hdcp_level.rs | 12 +- src/types/hexadecimal_sequence.rs | 36 ++-- src/types/in_stream_id.rs | 12 +- src/types/initialization_vector.rs | 24 +-- src/types/media_type.rs | 12 +- src/types/playlist_type.rs | 12 +- src/types/protocol_version.rs | 10 +- src/types/signed_decimal_floating_point.rs | 23 ++- src/types/single_line_string.rs | 11 +- src/utils.rs | 22 +-- 42 files changed, 731 insertions(+), 529 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6bc713e..65c0cf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,8 @@ travis-ci = {repository = "sile/hls_m3u8"} codecov = {repository = "sile/hls_m3u8"} [dependencies] -trackable = "0.2" getset = "0.0.8" +failure = "0.1.5" [dev-dependencies] clap = "2" diff --git a/examples/parse.rs b/examples/parse.rs index 44a8d16..226a22d 100644 --- a/examples/parse.rs +++ b/examples/parse.rs @@ -1,12 +1,9 @@ extern crate clap; extern crate hls_m3u8; -#[macro_use] -extern crate trackable; use clap::{App, Arg}; use hls_m3u8::{MasterPlaylist, MediaPlaylist}; use std::io::{self, Read}; -use trackable::error::Failure; fn main() { let matches = App::new("parse") @@ -19,17 +16,15 @@ fn main() { ) .get_matches(); let mut m3u8 = String::new(); - track_try_unwrap!(io::stdin() - .read_to_string(&mut m3u8) - .map_err(Failure::from_error)); + io::stdin().read_to_string(&mut m3u8).unwrap(); match matches.value_of("M3U8_TYPE").unwrap() { "media" => { - let playlist: MediaPlaylist = track_try_unwrap!(m3u8.parse()); + let playlist: MediaPlaylist = m3u8.parse().unwrap(); println!("{}", playlist); } "master" => { - let playlist: MasterPlaylist = track_try_unwrap!(m3u8.parse()); + let playlist: MasterPlaylist = m3u8.parse().unwrap(); println!("{}", playlist); } _ => unreachable!(), diff --git a/src/attribute.rs b/src/attribute.rs index 8ef7450..212728b 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,4 +1,4 @@ -use crate::{ErrorKind, Result}; +use crate::{Error, Result}; use std::collections::HashSet; use std::str; @@ -25,18 +25,13 @@ impl<'a> AttributePairs<'a> { return Ok(key); } b'A'..=b'Z' | b'0'..=b'9' | b'-' => {} - _ => track_panic!( - ErrorKind::InvalidInput, - "Malformed attribute name: {:?}", - self.input - ), + _ => { + return Err(Error::invalid_attribute(self.input.to_string())); + } } } - track_panic!( - ErrorKind::InvalidInput, - "No attribute value: {:?}", - self.input - ); + + Err(Error::missing_value(self.input.to_string())) } fn parse_raw_value(&mut self) -> &'a str { @@ -64,19 +59,15 @@ impl<'a> AttributePairs<'a> { } impl<'a> Iterator for AttributePairs<'a> { type Item = Result<(&'a str, &'a str)>; + fn next(&mut self) -> Option { if self.input.is_empty() { return None; } let result = || -> Result<(&'a str, &'a str)> { - let key = track!(self.parse_name())?; - track_assert!( - self.visited_keys.insert(key), - ErrorKind::InvalidInput, - "Duplicate attribute key: {:?}", - key - ); + let key = self.parse_name()?; + self.visited_keys.insert(key); let value = self.parse_raw_value(); Ok((key, value)) diff --git a/src/error.rs b/src/error.rs index b803bd1..ff1fddf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,13 +1,179 @@ -use trackable::error::{ErrorKind as TrackableErrorKind, TrackableError}; +use std::error; +use std::fmt; -/// This crate specific `Error` type. -#[derive(Debug, Clone, TrackableError)] -pub struct Error(TrackableError); +use failure::{Backtrace, Context, Fail}; -/// Possible error kinds. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[allow(missing_docs)] -pub enum ErrorKind { - InvalidInput, +/// This crate specific `Result` type. +pub type Result = std::result::Result; + +#[derive(Debug, Fail, Clone)] +pub enum AttributeError { + #[fail(display = "The attribute has an invalid name; {:?}", _0)] + InvalidAttribute(String), + #[fail(display = "A value is missing for the attribute: {}", _0)] + MissingValue(String), +} + +#[derive(Debug, Fail, Clone)] +pub enum ErrorKind { + #[fail(display = "AttributeError: {}", _0)] + AttributeError(AttributeError), + + #[fail(display = "UnknownError: {}", _0)] + UnknownError(String), + + #[fail(display = "A value is missing for the attribute {}", _0)] + MissingValue(String), + + #[fail(display = "Invalid Input")] + InvalidInput, + + #[fail(display = "ParseIntError: {}", _0)] + ParseIntError(String), + + #[fail(display = "ParseFloatError: {}", _0)] + ParseFloatError(String), + + #[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)] + MissingTag { tag: String, input: String }, + + #[fail(display = "CustomError: {}", _0)] + Custom(String), + + #[fail(display = "Unmatched Group: {:?}", _0)] + UnmatchedGroup(String), + + #[fail(display = "Unknown Protocol version: {:?}", _0)] + UnknownProtocolVersion(String), + + /// Hints that destructuring should not be exhaustive. + /// + /// This enum may grow additional variants, so this makes sure clients + /// don't count on exhaustive matching. (Otherwise, adding a new variant + /// could break existing code.) + #[doc(hidden)] + #[fail(display = "Invalid error")] + __Nonexhaustive, +} + +#[derive(Debug)] +pub struct Error { + inner: Context, +} + +impl Fail for Error { + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error::from(Context::new(kind)) + } +} + +impl From> for Error { + fn from(inner: Context) -> Error { + Error { inner } + } +} + +macro_rules! from_error { + ( $( $f:tt ),* ) => { + $( + impl From<$f> for ErrorKind { + fn from(value: $f) -> Self { + Self::$f(value) + } + } + )* + } +} + +from_error!(AttributeError); + +impl Error { + pub(crate) fn invalid_attribute(value: T) -> Self { + Self::from(ErrorKind::from(AttributeError::InvalidAttribute( + value.to_string(), + ))) + } + + pub(crate) fn missing_attribute_value(value: T) -> Self { + Self::from(ErrorKind::from(AttributeError::MissingValue( + value.to_string(), + ))) + } + + pub(crate) fn unknown(value: T) -> Self + where + T: error::Error, + { + Self::from(ErrorKind::UnknownError(value.to_string())) + } + + pub(crate) fn missing_value(value: T) -> Self { + Self::from(ErrorKind::MissingValue(value.to_string())) + } + + pub(crate) fn invalid_input() -> Self { + Self::from(ErrorKind::InvalidInput) + } + + pub(crate) fn parse_int_error(value: T) -> Self { + Self::from(ErrorKind::ParseIntError(value.to_string())) + } + + pub(crate) fn parse_float_error(value: T) -> Self { + Self::from(ErrorKind::ParseFloatError(value.to_string())) + } + + pub(crate) fn missing_tag(tag: T, input: U) -> Self + where + T: ToString, + U: ToString, + { + Self::from(ErrorKind::MissingTag { + tag: tag.to_string(), + input: input.to_string(), + }) + } + + pub(crate) fn unmatched_group(value: T) -> Self { + Self::from(ErrorKind::UnmatchedGroup(value.to_string())) + } + + pub(crate) fn custom(value: T) -> Self + where + T: fmt::Display, + { + Self::from(ErrorKind::Custom(value.to_string())) + } + + pub(crate) fn unknown_protocol_version(value: T) -> Self { + Self::from(ErrorKind::UnknownProtocolVersion(value.to_string())) + } +} + +impl From for Error { + fn from(value: ::std::num::ParseIntError) -> Self { + Error::parse_int_error(value) + } +} + +impl From for Error { + fn from(value: ::std::num::ParseFloatError) -> Self { + Error::parse_float_error(value) + } } -impl TrackableErrorKind for ErrorKind {} diff --git a/src/lib.rs b/src/lib.rs index 409ba59..135c9ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ clippy::nursery, clippy::cargo )] +#![warn(missing_docs)] //! [HLS] m3u8 parser/generator. //! //! [HLS]: https://tools.ietf.org/html/rfc8216 @@ -25,9 +26,6 @@ //! //! assert!(m3u8.parse::().is_ok()); //! ``` -#![warn(missing_docs)] -#[macro_use] -extern crate trackable; pub use error::{Error, ErrorKind}; pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder}; @@ -45,5 +43,4 @@ mod media_playlist; mod media_segment; mod utils; -/// This crate specific `Result` type. -pub type Result = std::result::Result; +pub use error::Result; diff --git a/src/line.rs b/src/line.rs index 974d01d..0641bc5 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,9 +1,10 @@ -use crate::tags; -use crate::types::SingleLineString; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::tags; +use crate::types::SingleLineString; +use crate::Error; + #[derive(Debug)] pub struct Lines<'a> { input: &'a str, @@ -13,11 +14,12 @@ impl<'a> Lines<'a> { Lines { input } } - fn read_line(&mut self) -> Result> { + fn read_line(&mut self) -> crate::Result> { let mut end = self.input.len(); let mut next_start = self.input.len(); let mut adjust = 0; let mut next_line_of_ext_x_stream_inf = false; + for (i, c) in self.input.char_indices() { match c { '\n' => { @@ -36,33 +38,39 @@ impl<'a> Lines<'a> { adjust = 1; } _ => { - track_assert!(!c.is_control(), ErrorKind::InvalidInput); + if c.is_control() { + return Err(Error::invalid_input()); + } adjust = 0; } } } let raw_line = &self.input[..end]; + let line = if raw_line.is_empty() { Line::Blank } else if raw_line.starts_with("#EXT") { - Line::Tag(track!(raw_line.parse())?) + Line::Tag((raw_line.parse())?) } else if raw_line.starts_with('#') { Line::Comment(raw_line) } else { - let uri = track!(SingleLineString::new(raw_line))?; + let uri = SingleLineString::new(raw_line)?; Line::Uri(uri) }; + self.input = &self.input[next_start..]; Ok(line) } } impl<'a> Iterator for Lines<'a> { - type Item = Result>; + type Item = crate::Result>; + fn next(&mut self) -> Option { if self.input.is_empty() { return None; } - match track!(self.read_line()) { + + match self.read_line() { Err(e) => Some(Err(e)), Ok(line) => Some(Ok(line)), } @@ -105,6 +113,7 @@ pub enum Tag { ExtXStart(tags::ExtXStart), Unknown(SingleLineString), } + impl fmt::Display for Tag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -134,55 +143,57 @@ impl fmt::Display for Tag { } } } + impl FromStr for Tag { type Err = Error; - fn from_str(s: &str) -> Result { + + fn from_str(s: &str) -> Result { if s.starts_with(tags::ExtM3u::PREFIX) { - track!(s.parse().map(Tag::ExtM3u)) + (s.parse().map(Tag::ExtM3u)) } else if s.starts_with(tags::ExtXVersion::PREFIX) { - track!(s.parse().map(Tag::ExtXVersion)) + (s.parse().map(Tag::ExtXVersion)) } else if s.starts_with(tags::ExtInf::PREFIX) { - track!(s.parse().map(Tag::ExtInf)) + (s.parse().map(Tag::ExtInf)) } else if s.starts_with(tags::ExtXByteRange::PREFIX) { - track!(s.parse().map(Tag::ExtXByteRange)) + (s.parse().map(Tag::ExtXByteRange)) } else if s.starts_with(tags::ExtXDiscontinuity::PREFIX) { - track!(s.parse().map(Tag::ExtXDiscontinuity)) + (s.parse().map(Tag::ExtXDiscontinuity)) } else if s.starts_with(tags::ExtXKey::PREFIX) { - track!(s.parse().map(Tag::ExtXKey)) + (s.parse().map(Tag::ExtXKey)) } else if s.starts_with(tags::ExtXMap::PREFIX) { - track!(s.parse().map(Tag::ExtXMap)) + (s.parse().map(Tag::ExtXMap)) } else if s.starts_with(tags::ExtXProgramDateTime::PREFIX) { - track!(s.parse().map(Tag::ExtXProgramDateTime)) + (s.parse().map(Tag::ExtXProgramDateTime)) } else if s.starts_with(tags::ExtXTargetDuration::PREFIX) { - track!(s.parse().map(Tag::ExtXTargetDuration)) + (s.parse().map(Tag::ExtXTargetDuration)) } else if s.starts_with(tags::ExtXDateRange::PREFIX) { - track!(s.parse().map(Tag::ExtXDateRange)) + (s.parse().map(Tag::ExtXDateRange)) } else if s.starts_with(tags::ExtXMediaSequence::PREFIX) { - track!(s.parse().map(Tag::ExtXMediaSequence)) + (s.parse().map(Tag::ExtXMediaSequence)) } else if s.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) { - track!(s.parse().map(Tag::ExtXDiscontinuitySequence)) + (s.parse().map(Tag::ExtXDiscontinuitySequence)) } else if s.starts_with(tags::ExtXEndList::PREFIX) { - track!(s.parse().map(Tag::ExtXEndList)) + (s.parse().map(Tag::ExtXEndList)) } else if s.starts_with(tags::ExtXPlaylistType::PREFIX) { - track!(s.parse().map(Tag::ExtXPlaylistType)) + (s.parse().map(Tag::ExtXPlaylistType)) } else if s.starts_with(tags::ExtXIFramesOnly::PREFIX) { - track!(s.parse().map(Tag::ExtXIFramesOnly)) + (s.parse().map(Tag::ExtXIFramesOnly)) } else if s.starts_with(tags::ExtXMedia::PREFIX) { - track!(s.parse().map(Tag::ExtXMedia)) + (s.parse().map(Tag::ExtXMedia)) } else if s.starts_with(tags::ExtXStreamInf::PREFIX) { - track!(s.parse().map(Tag::ExtXStreamInf)) + (s.parse().map(Tag::ExtXStreamInf)) } else if s.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { - track!(s.parse().map(Tag::ExtXIFrameStreamInf)) + (s.parse().map(Tag::ExtXIFrameStreamInf)) } else if s.starts_with(tags::ExtXSessionData::PREFIX) { - track!(s.parse().map(Tag::ExtXSessionData)) + (s.parse().map(Tag::ExtXSessionData)) } else if s.starts_with(tags::ExtXSessionKey::PREFIX) { - track!(s.parse().map(Tag::ExtXSessionKey)) + (s.parse().map(Tag::ExtXSessionKey)) } else if s.starts_with(tags::ExtXIndependentSegments::PREFIX) { - track!(s.parse().map(Tag::ExtXIndependentSegments)) + (s.parse().map(Tag::ExtXIndependentSegments)) } else if s.starts_with(tags::ExtXStart::PREFIX) { - track!(s.parse().map(Tag::ExtXStart)) + (s.parse().map(Tag::ExtXStart)) } else { - track!(SingleLineString::new(s)).map(Tag::Unknown) + SingleLineString::new(s).map(Tag::Unknown) } } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 48a4453..5ce9a38 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,14 +1,15 @@ +use std::collections::HashSet; +use std::fmt; +use std::iter; +use std::str::FromStr; + use crate::line::{Line, Lines, Tag}; use crate::tags::{ ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; -use std::collections::HashSet; -use std::fmt; -use std::iter; -use std::str::FromStr; +use crate::{Error, Result}; /// Master playlist builder. #[derive(Debug, Clone)] @@ -71,18 +72,16 @@ impl MasterPlaylistBuilder { pub fn finish(self) -> Result { let required_version = self.required_version(); let specified_version = self.version.unwrap_or(required_version); - track_assert!( - required_version <= specified_version, - ErrorKind::InvalidInput, - "required_version:{}, specified_version:{}", - required_version, - specified_version, - ); - track!(self.validate_stream_inf_tags())?; - track!(self.validate_i_frame_stream_inf_tags())?; - track!(self.validate_session_data_tags())?; - track!(self.validate_session_key_tags())?; + if required_version <= specified_version { + // "required_version:{}, specified_version:{}" + return Err(Error::invalid_input()); + } + + (self.validate_stream_inf_tags())?; + (self.validate_i_frame_stream_inf_tags())?; + (self.validate_session_data_tags())?; + (self.validate_session_key_tags())?; Ok(MasterPlaylist { version_tag: ExtXVersion::new(specified_version), @@ -121,37 +120,25 @@ impl MasterPlaylistBuilder { let mut has_none_closed_captions = false; for t in &self.stream_inf_tags { if let Some(group_id) = t.audio() { - track_assert!( - self.check_media_group(MediaType::Audio, group_id), - ErrorKind::InvalidInput, - "Unmatched audio group: {:?}", - group_id - ); + if !self.check_media_group(MediaType::Audio, group_id) { + return Err(Error::unmatched_group(group_id)); + } } if let Some(group_id) = t.video() { - track_assert!( - self.check_media_group(MediaType::Video, group_id), - ErrorKind::InvalidInput, - "Unmatched video group: {:?}", - group_id - ); + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id)); + } } if let Some(group_id) = t.subtitles() { - track_assert!( - self.check_media_group(MediaType::Subtitles, group_id), - ErrorKind::InvalidInput, - "Unmatched subtitles group: {:?}", - group_id - ); + if !self.check_media_group(MediaType::Subtitles, group_id) { + return Err(Error::unmatched_group(group_id)); + } } match t.closed_captions() { Some(&ClosedCaptions::GroupId(ref group_id)) => { - track_assert!( - self.check_media_group(MediaType::ClosedCaptions, group_id), - ErrorKind::InvalidInput, - "Unmatched closed-captions group: {:?}", - group_id - ); + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { + return Err(Error::unmatched_group(group_id)); + } } Some(&ClosedCaptions::None) => { has_none_closed_captions = true; @@ -160,53 +147,49 @@ impl MasterPlaylistBuilder { } } if has_none_closed_captions { - track_assert!( - self.stream_inf_tags - .iter() - .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)), - ErrorKind::InvalidInput - ); + if !self + .stream_inf_tags + .iter() + .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) + { + return Err(Error::invalid_input()); + } } + Ok(()) } fn validate_i_frame_stream_inf_tags(&self) -> Result<()> { for t in &self.i_frame_stream_inf_tags { if let Some(group_id) = t.video() { - track_assert!( - self.check_media_group(MediaType::Video, group_id), - ErrorKind::InvalidInput, - "Unmatched video group: {:?}", - group_id - ); + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id)); + } } } + Ok(()) } fn validate_session_data_tags(&self) -> Result<()> { let mut set = HashSet::new(); for t in &self.session_data_tags { - track_assert!( - set.insert((t.data_id(), t.language())), - ErrorKind::InvalidInput, - "Conflict: {}", - t - ); + if !set.insert((t.data_id(), t.language())) { + return Err(Error::custom(format!("Conflict: {}", t))); + } } + Ok(()) } fn validate_session_key_tags(&self) -> Result<()> { let mut set = HashSet::new(); for t in &self.session_key_tags { - track_assert!( - set.insert(t.key()), - ErrorKind::InvalidInput, - "Conflict: {}", - t - ); + if !set.insert(t.key()) { + return Err(Error::custom(format!("Conflict: {}", t))); + } } + Ok(()) } @@ -308,24 +291,29 @@ impl fmt::Display for MasterPlaylist { Ok(()) } } + impl FromStr for MasterPlaylist { type Err = Error; fn from_str(s: &str) -> Result { let mut builder = MasterPlaylistBuilder::new(); for (i, line) in Lines::new(s).enumerate() { - match track!(line)? { + match (line)? { Line::Blank | Line::Comment(_) => {} Line::Tag(tag) => { if i == 0 { - track_assert_eq!(tag, Tag::ExtM3u(ExtM3u), ErrorKind::InvalidInput); + if tag != Tag::ExtM3u(ExtM3u) { + return Err(Error::invalid_input()); + } continue; } match tag { Tag::ExtM3u(_) => { - track_panic!(ErrorKind::InvalidInput); + return Err(Error::invalid_input()); } Tag::ExtXVersion(t) => { - track_assert_eq!(builder.version, None, ErrorKind::InvalidInput); + if builder.version.is_some() { + return Err(Error::invalid_input()); + } builder.version(t.version()); } Tag::ExtInf(_) @@ -341,7 +329,7 @@ impl FromStr for MasterPlaylist { | Tag::ExtXEndList(_) | Tag::ExtXPlaylistType(_) | Tag::ExtXIFramesOnly(_) => { - track_panic!(ErrorKind::InvalidInput, "{}", tag) + return Err(Error::invalid_input()); // TODO: why? } Tag::ExtXMedia(t) => { builder.tag(t); @@ -359,28 +347,29 @@ impl FromStr for MasterPlaylist { builder.tag(t); } Tag::ExtXIndependentSegments(t) => { - track_assert_eq!( - builder.independent_segments_tag, - None, - ErrorKind::InvalidInput - ); + if builder.independent_segments_tag.is_some() { + return Err(Error::invalid_input()); + } builder.tag(t); } Tag::ExtXStart(t) => { - track_assert_eq!(builder.start_tag, None, ErrorKind::InvalidInput); + if builder.start_tag.is_some() { + return Err(Error::invalid_input()); + } builder.tag(t); } Tag::Unknown(_) => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. + // TODO: collect custom tags } } } Line::Uri(uri) => { - track_panic!(ErrorKind::InvalidInput, "Unexpected URI: {:?}", uri); + return Err(Error::custom(format!("Unexpected URI: {:?}", uri))); } } } - track!(builder.finish()) + builder.finish() } } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 5eca286..0b09225 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -1,3 +1,8 @@ +use std::fmt; +use std::iter; +use std::str::FromStr; +use std::time::Duration; + use crate::line::{Line, Lines, Tag}; use crate::media_segment::{MediaSegment, MediaSegmentBuilder}; use crate::tags::{ @@ -6,11 +11,7 @@ use crate::tags::{ MediaPlaylistTag, }; use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::iter; -use std::str::FromStr; -use std::time::Duration; +use crate::Error; /// Media playlist builder. #[derive(Debug, Clone)] @@ -89,20 +90,18 @@ impl MediaPlaylistBuilder { } /// Builds a `MediaPlaylist` instance. - pub fn finish(self) -> Result { + pub fn finish(self) -> crate::Result { let required_version = self.required_version(); let specified_version = self.version.unwrap_or(required_version); - track_assert!( - required_version <= specified_version, - ErrorKind::InvalidInput, - "required_version:{}, specified_version:{}", - required_version, - specified_version, - ); + if !(required_version <= specified_version) { + return Err(Error::custom(format!( + "required_version:{}, specified_version:{}", + required_version, specified_version + ))); + } - let target_duration_tag = - track_assert_some!(self.target_duration_tag, ErrorKind::InvalidInput); - track!(self.validate_media_segments(target_duration_tag.duration()))?; + let target_duration_tag = self.target_duration_tag.ok_or(Error::invalid_input())?; + self.validate_media_segments(target_duration_tag.duration())?; Ok(MediaPlaylist { version_tag: ExtXVersion::new(specified_version), @@ -118,7 +117,7 @@ impl MediaPlaylistBuilder { }) } - fn validate_media_segments(&self, target_duration: Duration) -> Result<()> { + fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> { let mut last_range_uri = None; for s in &self.segments { // CHECK: `#EXT-X-TARGETDURATION` @@ -129,21 +128,24 @@ impl MediaPlaylistBuilder { Duration::from_secs(segment_duration.as_secs() + 1) }; let max_segment_duration = target_duration + self.options.allowable_excess_duration; - track_assert!( - rounded_segment_duration <= max_segment_duration, - ErrorKind::InvalidInput, - "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", - segment_duration, - max_segment_duration, - target_duration, - s.uri() - ); + + if !(rounded_segment_duration <= max_segment_duration) { + return Err(Error::custom(format!( + "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", + segment_duration, + max_segment_duration, + target_duration, + s.uri() + ))); + } // CHECK: `#EXT-X-BYTE-RANGE` if let Some(tag) = s.byte_range_tag() { if tag.to_range().start().is_none() { - let last_uri = track_assert_some!(last_range_uri, ErrorKind::InvalidInput); - track_assert_eq!(last_uri, s.uri(), ErrorKind::InvalidInput); + let last_uri = last_range_uri.ok_or(Error::invalid_input())?; + if last_uri != s.uri() { + return Err(Error::invalid_input()); + } } else { last_range_uri = Some(s.uri()); } @@ -292,8 +294,9 @@ impl fmt::Display for MediaPlaylist { impl FromStr for MediaPlaylist { type Err = Error; - fn from_str(s: &str) -> Result { - track!(MediaPlaylistOptions::new().parse(s)) + + fn from_str(input: &str) -> Result { + MediaPlaylistOptions::new().parse(input) } } @@ -305,7 +308,7 @@ pub struct MediaPlaylistOptions { impl MediaPlaylistOptions { /// Makes a new `MediaPlaylistOptions` with the default settings. - pub fn new() -> Self { + pub const fn new() -> Self { MediaPlaylistOptions { allowable_excess_duration: Duration::from_secs(0), } @@ -327,7 +330,7 @@ impl MediaPlaylistOptions { } /// Parses the given M3U8 text with the specified settings. - pub fn parse(&self, m3u8: &str) -> Result { + pub fn parse(&self, m3u8: &str) -> crate::Result { let mut builder = MediaPlaylistBuilder::new(); builder.options(self.clone()); @@ -335,17 +338,21 @@ impl MediaPlaylistOptions { let mut has_partial_segment = false; let mut has_discontinuity_tag = false; for (i, line) in Lines::new(m3u8).enumerate() { - match track!(line)? { + match (line)? { Line::Blank | Line::Comment(_) => {} Line::Tag(tag) => { if i == 0 { - track_assert_eq!(tag, Tag::ExtM3u(ExtM3u), ErrorKind::InvalidInput); + if tag != Tag::ExtM3u(ExtM3u) { + return Err(Error::invalid_input()); + } continue; } match tag { - Tag::ExtM3u(_) => track_panic!(ErrorKind::InvalidInput), + Tag::ExtM3u(_) => return Err(Error::invalid_input()), Tag::ExtXVersion(t) => { - track_assert_eq!(builder.version, None, ErrorKind::InvalidInput); + if builder.version.is_some() { + return Err(Error::invalid_input()); + } builder.version(t.version()); } Tag::ExtInf(t) => { @@ -378,45 +385,30 @@ impl MediaPlaylistOptions { segment.tag(t); } Tag::ExtXTargetDuration(t) => { - track_assert_eq!( - builder.target_duration_tag, - None, - ErrorKind::InvalidInput - ); builder.tag(t); } Tag::ExtXMediaSequence(t) => { - track_assert_eq!( - builder.media_sequence_tag, - None, - ErrorKind::InvalidInput - ); - track_assert!(builder.segments.is_empty(), ErrorKind::InvalidInput); + if builder.segments.is_empty() { + return Err(Error::invalid_input()); + } builder.tag(t); } Tag::ExtXDiscontinuitySequence(t) => { - track_assert!(builder.segments.is_empty(), ErrorKind::InvalidInput); - track_assert!(!has_discontinuity_tag, ErrorKind::InvalidInput); + if builder.segments.is_empty() { + return Err(Error::invalid_input()); + } + if has_discontinuity_tag { + return Err(Error::invalid_input()); + } builder.tag(t); } Tag::ExtXEndList(t) => { - track_assert_eq!(builder.end_list_tag, None, ErrorKind::InvalidInput); builder.tag(t); } Tag::ExtXPlaylistType(t) => { - track_assert_eq!( - builder.playlist_type_tag, - None, - ErrorKind::InvalidInput - ); builder.tag(t); } Tag::ExtXIFramesOnly(t) => { - track_assert_eq!( - builder.i_frames_only_tag, - None, - ErrorKind::InvalidInput - ); builder.tag(t); } Tag::ExtXMedia(_) @@ -424,18 +416,12 @@ impl MediaPlaylistOptions { | Tag::ExtXIFrameStreamInf(_) | Tag::ExtXSessionData(_) | Tag::ExtXSessionKey(_) => { - track_panic!(ErrorKind::InvalidInput, "{}", tag) + return Err(Error::custom(tag)); } Tag::ExtXIndependentSegments(t) => { - track_assert_eq!( - builder.independent_segments_tag, - None, - ErrorKind::InvalidInput - ); builder.tag(t); } Tag::ExtXStart(t) => { - track_assert_eq!(builder.start_tag, None, ErrorKind::InvalidInput); builder.tag(t); } Tag::Unknown(_) => { @@ -446,14 +432,16 @@ impl MediaPlaylistOptions { } Line::Uri(uri) => { segment.uri(uri); - builder.segment(track!(segment.finish())?); + builder.segment((segment.finish())?); segment = MediaSegmentBuilder::new(); has_partial_segment = false; } } } - track_assert!(!has_partial_segment, ErrorKind::InvalidInput); - track!(builder.finish()) + if has_partial_segment { + return Err(Error::invalid_input()); + } + builder.finish() } } diff --git a/src/media_segment.rs b/src/media_segment.rs index 602b51d..3f45f5b 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -1,11 +1,12 @@ +use std::fmt; +use std::iter; + use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, MediaSegmentTag, }; use crate::types::{ProtocolVersion, SingleLineString}; -use crate::{ErrorKind, Result}; -use std::fmt; -use std::iter; +use crate::Error; /// Media segment builder. #[derive(Debug, Clone)] @@ -56,9 +57,10 @@ impl MediaSegmentBuilder { } /// Builds a `MediaSegment` instance. - pub fn finish(self) -> Result { - let uri = track_assert_some!(self.uri, ErrorKind::InvalidInput); - let inf_tag = track_assert_some!(self.inf_tag, ErrorKind::InvalidInput); + pub fn finish(self) -> crate::Result { + let uri = self.uri.ok_or(Error::missing_value("self.uri"))?; + let inf_tag = self.inf_tag.ok_or(Error::missing_value("self.inf_tag"))?; + Ok(MediaSegment { key_tags: self.key_tags, map_tag: self.map_tag, diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index f9e38dc..7f9abd3 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -7,7 +7,7 @@ use crate::attribute::AttributePairs; use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion}; use crate::utils::parse_u64; use crate::utils::{quote, tag, unquote}; -use crate::{Error, ErrorKind}; +use crate::Error; /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] /// @@ -96,14 +96,14 @@ impl FromStr for ExtXIFrameStreamInf { let attrs = AttributePairs::parse(input); for attr in attrs { - let (key, value) = track!(attr)?; + let (key, value) = attr?; match key { "URI" => uri = Some(unquote(value)), - "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), + "BANDWIDTH" => bandwidth = Some(parse_u64(value)?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some(parse_u64(value)?), "CODECS" => codecs = Some(unquote(value)), - "RESOLUTION" => resolution = Some(track!(value.parse())?), - "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), + "RESOLUTION" => resolution = Some(value.parse()?), + "HDCP-LEVEL" => hdcp_level = Some(value.parse()?), "VIDEO" => video = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] @@ -112,8 +112,9 @@ impl FromStr for ExtXIFrameStreamInf { } } - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); - let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); + let uri = uri.ok_or(Error::missing_value("URI"))?; + let bandwidth = bandwidth.ok_or(Error::missing_value("BANDWIDTH"))?; + Ok(ExtXIFrameStreamInf { uri, bandwidth, diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 59e2910..40d1e41 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -1,9 +1,10 @@ +use std::fmt; +use std::str::FromStr; + use crate::attribute::AttributePairs; use crate::types::{InStreamId, MediaType, ProtocolVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; -use crate::{Error, ErrorKind}; -use std::fmt; -use std::str::FromStr; +use crate::Error; /// `ExtXMedia` builder. #[derive(Debug, Clone)] @@ -115,21 +116,38 @@ impl ExtXMediaBuilder { /// Builds a `ExtXMedia` instance. pub fn finish(self) -> crate::Result { - let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput); - let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput); - let name = track_assert_some!(self.name, ErrorKind::InvalidInput); + let media_type = self + .media_type + .ok_or(Error::missing_value("self.media_type"))?; + let group_id = self.group_id.ok_or(Error::missing_value("self.group_id"))?; + let name = self.name.ok_or(Error::missing_value("self.name"))?; + if MediaType::ClosedCaptions == media_type { - track_assert_ne!(self.uri, None, ErrorKind::InvalidInput); - track_assert!(self.instream_id.is_some(), ErrorKind::InvalidInput); + if let None = self.uri { + return Err(Error::missing_value("self.uri")); + } + self.instream_id + .ok_or(Error::missing_value("self.instream_id"))?; } else { - track_assert!(self.instream_id.is_none(), ErrorKind::InvalidInput); + if let Some(_) = &self.instream_id { + Err(Error::invalid_input())?; + } } + if self.default && self.autoselect.is_some() { - track_assert_eq!(self.autoselect, Some(true), ErrorKind::InvalidInput); + if let Some(value) = &self.autoselect { + if *value { + Err(Error::invalid_input())?; + } + } } + if MediaType::Subtitles != media_type { - track_assert_eq!(self.forced, None, ErrorKind::InvalidInput); + if self.forced.is_some() { + Err(Error::invalid_input())?; + } } + Ok(ExtXMedia { media_type, uri: self.uri, @@ -315,10 +333,10 @@ impl FromStr for ExtXMedia { let mut builder = ExtXMediaBuilder::new(); let attrs = AttributePairs::parse(input); for attr in attrs { - let (key, value) = track!(attr)?; + let (key, value) = attr?; match key { "TYPE" => { - builder.media_type(track!(value.parse())?); + builder.media_type(value.parse()?); } "URI" => { builder.uri(unquote(value)); @@ -336,13 +354,13 @@ impl FromStr for ExtXMedia { builder.name(unquote(value)); } "DEFAULT" => { - builder.default(track!(parse_yes_or_no(value))?); + builder.default((parse_yes_or_no(value))?); } "AUTOSELECT" => { - builder.autoselect(track!(parse_yes_or_no(value))?); + builder.autoselect((parse_yes_or_no(value))?); } "FORCED" => { - builder.forced(track!(parse_yes_or_no(value))?); + builder.forced((parse_yes_or_no(value))?); } "INSTREAM-ID" => { builder.instream_id(unquote(value).parse()?); @@ -359,7 +377,7 @@ impl FromStr for ExtXMedia { } } } - track!(builder.finish()) + (builder.finish()) } } diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 97da40d..f246862 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -6,7 +6,7 @@ use getset::{Getters, MutGetters, Setters}; use crate::attribute::AttributePairs; use crate::types::{ProtocolVersion, SessionData}; use crate::utils::{quote, tag, unquote}; -use crate::{Error, ErrorKind}; +use crate::Error; /// [4.3.4.4. EXT-X-SESSION-DATA] /// @@ -79,7 +79,7 @@ impl FromStr for ExtXSessionData { let attrs = AttributePairs::parse(input); for attr in attrs { - let (key, value) = track!(attr)?; + let (key, value) = attr?; match key { "DATA-ID" => data_id = Some(unquote(value)), "VALUE" => session_value = Some(unquote(value)), @@ -92,15 +92,21 @@ impl FromStr for ExtXSessionData { } } - let data_id = track_assert_some!(data_id, ErrorKind::InvalidInput); - let data = if let Some(value) = session_value { - track_assert_eq!(uri, None, ErrorKind::InvalidInput); - SessionData::Value(value) - } else if let Some(uri) = uri { - SessionData::Uri(uri) - } else { - track_panic!(ErrorKind::InvalidInput); + let data_id = data_id.ok_or(Error::missing_value("DATA-ID"))?; + let data = { + if let Some(value) = session_value { + if uri.is_some() { + return Err(Error::invalid_input()); + } else { + SessionData::Value(value) + } + } else if let Some(uri) = uri { + SessionData::Uri(uri) + } else { + return Err(Error::invalid_input()); + } }; + Ok(ExtXSessionData { data_id, data, diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index dbeeef9..e69df8b 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -7,7 +7,7 @@ use crate::types::{ SingleLineString, }; use crate::utils::{parse_u64, quote, tag, unquote}; -use crate::{Error, ErrorKind}; +use crate::Error; /// [4.3.4.2. EXT-X-STREAM-INF] /// @@ -149,12 +149,12 @@ impl FromStr for ExtXStreamInf { fn from_str(input: &str) -> Result { let mut lines = input.lines(); - let first_line = lines.next().ok_or(ErrorKind::InvalidInput)?; // TODO! - let second_line = lines.next().ok_or(ErrorKind::InvalidInput)?; // TODO! + let first_line = lines.next().ok_or(Error::invalid_input())?; // TODO! + let second_line = lines.next().ok_or(Error::invalid_input())?; // TODO! tag(first_line, Self::PREFIX)?; - let uri = track!(SingleLineString::new(second_line))?; + let uri = SingleLineString::new(second_line)?; let mut bandwidth = None; let mut average_bandwidth = None; @@ -166,20 +166,21 @@ impl FromStr for ExtXStreamInf { let mut video = None; let mut subtitles = None; let mut closed_captions = None; + let attrs = AttributePairs::parse(first_line.split_at(Self::PREFIX.len()).1); for attr in attrs { - let (key, value) = track!(attr)?; + let (key, value) = (attr)?; match key { - "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), + "BANDWIDTH" => bandwidth = Some((parse_u64(value))?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some((parse_u64(value))?), "CODECS" => codecs = Some(unquote(value)), - "RESOLUTION" => resolution = Some(track!(value.parse())?), - "FRAME-RATE" => frame_rate = Some(track!(value.parse())?), - "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), + "RESOLUTION" => resolution = Some((value.parse())?), + "FRAME-RATE" => frame_rate = Some((value.parse())?), + "HDCP-LEVEL" => hdcp_level = Some((value.parse())?), "AUDIO" => audio = Some(unquote(value)), "VIDEO" => video = Some(unquote(value)), "SUBTITLES" => subtitles = Some(unquote(value)), - "CLOSED-CAPTIONS" => closed_captions = Some(track!(value.parse())?), + "CLOSED-CAPTIONS" => closed_captions = Some((value.parse())?), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -187,7 +188,8 @@ impl FromStr for ExtXStreamInf { } } - let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); + let bandwidth = bandwidth.ok_or(Error::missing_value("BANDWIDTH"))?; + Ok(ExtXStreamInf { uri, bandwidth, diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index ddeb5c0..b34b357 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -1,8 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.3.4. EXT-X-ENDLIST] /// /// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4 @@ -26,8 +28,8 @@ impl fmt::Display for ExtXEndList { impl FromStr for ExtXEndList { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtXEndList) } } diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 90db7f4..dfe3abc 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -1,8 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.3.6. EXT-X-I-FRAMES-ONLY] /// /// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6 @@ -27,8 +29,8 @@ impl fmt::Display for ExtXIFramesOnly { impl FromStr for ExtXIFramesOnly { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtXIFramesOnly) } } diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 47f75bf..1f328f0 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -1,8 +1,9 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; -use trackable::error::ErrorKindExt; + +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; /// [4.3.3.2. EXT-X-MEDIA-SEQUENCE] /// @@ -40,10 +41,10 @@ impl fmt::Display for ExtXMediaSequence { impl FromStr for ExtXMediaSequence { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXMediaSequence { seq_num }) + fn from_str(input: &str) -> Result { + let seq_num = tag(input, Self::PREFIX)?.parse()?; + + Ok(ExtXMediaSequence::new(seq_num)) } } diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index 16d9672..f9911b7 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -1,8 +1,9 @@ -use crate::types::{PlaylistType, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; -use trackable::error::ErrorKindExt; + +use crate::types::{PlaylistType, ProtocolVersion}; +use crate::utils::tag; +use crate::Error; /// [4.3.3.5. EXT-X-PLAYLIST-TYPE] /// @@ -40,10 +41,10 @@ impl fmt::Display for ExtXPlaylistType { impl FromStr for ExtXPlaylistType { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXPlaylistType { playlist_type }) + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?.parse()?; + + Ok(ExtXPlaylistType::new(input)) } } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 2f759b9..32ca2a9 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -1,9 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; use std::time::Duration; -use trackable::error::ErrorKindExt; + +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; /// [4.3.3.1. EXT-X-TARGETDURATION] /// @@ -43,11 +44,11 @@ impl fmt::Display for ExtXTargetDuration { impl FromStr for ExtXTargetDuration { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?.parse()?; Ok(ExtXTargetDuration { - duration: Duration::from_secs(duration), + duration: Duration::from_secs(input), }) } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 9afddfa..e7a895a 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -2,10 +2,9 @@ use std::fmt; use std::ops::Deref; use std::str::FromStr; -use trackable::error::ErrorKindExt; - use crate::types::{ByteRange, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; +use crate::utils::tag; +use crate::Error; /// [4.3.2.2. EXT-X-BYTERANGE] /// @@ -73,26 +72,20 @@ impl fmt::Display for ExtXByteRange { impl FromStr for ExtXByteRange { type Err = Error; - fn from_str(s: &str) -> Result { - // check if the string starts with the PREFIX - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let byte_range = s.split_at(Self::PREFIX.len()).1; - let tokens = byte_range.splitn(2, '@').collect::>(); + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; + + let tokens = input.splitn(2, '@').collect::>(); if tokens.is_empty() { - Err(ErrorKind::InvalidInput)?; + return Err(Error::invalid_input()); } - let length = tokens[0] - .parse() - .map_err(|e| ErrorKind::InvalidInput.cause(e))?; + let length = tokens[0].parse()?; + let start = { let mut result = None; if tokens.len() == 2 { - result = Some( - tokens[1] - .parse() - .map_err(|e| ErrorKind::InvalidInput.cause(e))?, - ); + result = Some(tokens[1].parse()?); } result }; diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index dae13ce..8fa6f11 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -1,12 +1,13 @@ -use crate::attribute::AttributePairs; -use crate::types::{DecimalFloatingPoint, ProtocolVersion}; -use crate::utils::{quote, unquote}; -use crate::{Error, ErrorKind, Result}; use std::collections::BTreeMap; use std::fmt; use std::str::FromStr; use std::time::Duration; +use crate::attribute::AttributePairs; +use crate::types::{DecimalFloatingPoint, ProtocolVersion}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; + /// [4.3.2.7. EXT-X-DATERANGE] /// /// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 @@ -79,8 +80,9 @@ impl fmt::Display for ExtXDateRange { impl FromStr for ExtXDateRange { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut id = None; let mut class = None; @@ -92,28 +94,32 @@ impl FromStr for ExtXDateRange { let mut scte35_out = None; let mut scte35_in = None; let mut end_on_next = false; + let mut client_attributes = BTreeMap::new(); - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + let attrs = AttributePairs::parse(input); + for attr in attrs { - let (key, value) = track!(attr)?; + let (key, value) = attr?; match key { "ID" => id = Some(unquote(value)), "CLASS" => class = Some(unquote(value)), "START-DATE" => start_date = Some(unquote(value)), "END-DATE" => end_date = Some(unquote(value)), "DURATION" => { - let seconds: DecimalFloatingPoint = track!(value.parse())?; + let seconds: DecimalFloatingPoint = (value.parse())?; duration = Some(seconds.to_duration()); } "PLANNED-DURATION" => { - let seconds: DecimalFloatingPoint = track!(value.parse())?; + let seconds: DecimalFloatingPoint = (value.parse())?; planned_duration = Some(seconds.to_duration()); } "SCTE35-CMD" => scte35_cmd = Some(unquote(value)), "SCTE35-OUT" => scte35_out = Some(unquote(value)), "SCTE35-IN" => scte35_in = Some(unquote(value)), "END-ON-NEXT" => { - track_assert_eq!(value, "YES", ErrorKind::InvalidInput); + if value != "YES" { + return Err(Error::invalid_input()); + } end_on_next = true; } _ => { @@ -127,10 +133,12 @@ impl FromStr for ExtXDateRange { } } - let id = track_assert_some!(id, ErrorKind::InvalidInput); - let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput); + let id = id.ok_or(Error::missing_value("EXT-X-ID"))?; + let start_date = start_date.ok_or(Error::missing_value("EXT-X-START-DATE"))?; if end_on_next { - track_assert!(class.is_some(), ErrorKind::InvalidInput); + if class.is_none() { + return Err(Error::invalid_input()); + } } Ok(ExtXDateRange { id, diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index ac215af..ddf305c 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -1,9 +1,10 @@ -use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; use std::time::Duration; -use trackable::error::ErrorKindExt; + +use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString}; +use crate::utils::tag; +use crate::Error; /// [4.3.2.1. EXTINF] /// @@ -70,18 +71,20 @@ impl fmt::Display for ExtInf { impl FromStr for ExtInf { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ','); - let seconds: DecimalFloatingPoint = - may_invalid!(tokens.next().expect("Never fails").parse())?; + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; + let mut tokens = input.splitn(2, ','); + + let seconds: DecimalFloatingPoint = tokens.next().expect("Never fails").parse()?; let duration = seconds.to_duration(); - let title = if let Some(title) = tokens.next() { - Some(track!(SingleLineString::new(title))?) - } else { - None + let title = { + if let Some(title) = tokens.next() { + Some((SingleLineString::new(title))?) + } else { + None + } }; Ok(ExtInf { duration, title }) } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index e9dda27..fb47c83 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,9 +1,11 @@ -use crate::attribute::AttributePairs; -use crate::types::{DecryptionKey, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::attribute::AttributePairs; +use crate::types::{DecryptionKey, ProtocolVersion}; +use crate::utils::tag; +use crate::Error; + /// [4.3.2.4. EXT-X-KEY] /// /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 @@ -55,21 +57,19 @@ impl fmt::Display for ExtXKey { impl FromStr for ExtXKey { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; - if AttributePairs::parse(suffix).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) { - for attr in AttributePairs::parse(suffix) { - let (key, _) = track!(attr)?; - track_assert_ne!(key, "URI", ErrorKind::InvalidInput); - track_assert_ne!(key, "IV", ErrorKind::InvalidInput); - track_assert_ne!(key, "KEYFORMAT", ErrorKind::InvalidInput); - track_assert_ne!(key, "KEYFORMATVERSIONS", ErrorKind::InvalidInput); + if AttributePairs::parse(input).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) { + for attr in AttributePairs::parse(input) { + let (key, _) = attr?; + if key == "URI" || key == "IV" || key == "KEYFORMAT" || key == "KEYFORMATVERSIONS" { + return Err(Error::invalid_input()); + } } Ok(ExtXKey { key: None }) } else { - let key = track!(suffix.parse())?; + let key = input.parse()?; Ok(ExtXKey { key: Some(key) }) } } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 1ef468b..728afa4 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -1,10 +1,11 @@ -use crate::attribute::AttributePairs; -use crate::types::{ByteRange, ProtocolVersion}; -use crate::utils::{quote, unquote}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::attribute::AttributePairs; +use crate::types::{ByteRange, ProtocolVersion}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; + /// [4.3.2.5. EXT-X-MAP] /// /// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 @@ -63,18 +64,18 @@ impl fmt::Display for ExtXMap { impl FromStr for ExtXMap { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut uri = None; let mut range = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + let attrs = AttributePairs::parse(input); for attr in attrs { - let (key, value) = track!(attr)?; + let (key, value) = (attr)?; match key { "URI" => uri = Some(unquote(value)), "BYTERANGE" => { - range = Some(track!(unquote(value).parse())?); + range = Some((unquote(value).parse())?); } _ => { // [6.3.1. General Client Responsibilities] @@ -83,7 +84,7 @@ impl FromStr for ExtXMap { } } - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + let uri = uri.ok_or(Error::missing_value("EXT-X-URI"))?; Ok(ExtXMap { uri, range }) } } @@ -102,7 +103,8 @@ mod test { let tag = ExtXMap::with_range("foo", ByteRange::new(9, Some(2))); let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#; - track_try_unwrap!(ExtXMap::from_str(text)); + ExtXMap::from_str(text).unwrap(); + assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V6); diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 625fdeb..a4be50d 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,8 +1,10 @@ -use crate::types::{ProtocolVersion, SingleLineString}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::{ProtocolVersion, SingleLineString}; +use crate::utils::tag; +use crate::Error; + /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] /// /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 @@ -39,11 +41,13 @@ impl fmt::Display for ExtXProgramDateTime { impl FromStr for ExtXProgramDateTime { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; + + // TODO: parse with chrono + Ok(ExtXProgramDateTime { - date_time: track!(SingleLineString::new(suffix))?, + date_time: (SingleLineString::new(input))?, }) } } diff --git a/src/tags/mod.rs b/src/tags/mod.rs index b4a72fa..ebad0b7 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -2,12 +2,6 @@ //! //! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3 -macro_rules! may_invalid { - ($expr:expr) => { - $expr.map_err(|e| track!(Error::from(ErrorKind::InvalidInput.cause(e)))) - }; -} - macro_rules! impl_from { ($to:ident, $from:ident) => { impl From<$from> for $to { diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 2aa3e82..5268dcd 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -1,8 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] /// /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1 @@ -26,8 +28,8 @@ impl fmt::Display for ExtXIndependentSegments { impl FromStr for ExtXIndependentSegments { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtXIndependentSegments) } } diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 560b144..f946992 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -1,10 +1,11 @@ -use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; -use crate::utils::parse_yes_or_no; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::attribute::AttributePairs; +use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; +use crate::utils::{parse_yes_or_no, tag}; +use crate::Error; + /// [4.3.5.2. EXT-X-START] /// /// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2 @@ -64,17 +65,19 @@ impl fmt::Display for ExtXStart { impl FromStr for ExtXStart { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut time_offset = None; let mut precise = false; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + + let attrs = AttributePairs::parse(input); + for attr in attrs { - let (key, value) = track!(attr)?; + let (key, value) = (attr)?; match key { - "TIME-OFFSET" => time_offset = Some(track!(value.parse())?), - "PRECISE" => precise = track!(parse_yes_or_no(value))?, + "TIME-OFFSET" => time_offset = Some((value.parse())?), + "PRECISE" => precise = (parse_yes_or_no(value))?, _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -82,7 +85,8 @@ impl FromStr for ExtXStart { } } - let time_offset = track_assert_some!(time_offset, ErrorKind::InvalidInput); + let time_offset = time_offset.ok_or(Error::missing_value("EXT-X-TIME-OFFSET"))?; + Ok(ExtXStart { time_offset, precise, diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index 443ab53..09f3535 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -1,10 +1,9 @@ use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; use getset::{Getters, MutGetters, Setters}; -use trackable::error::ErrorKindExt; -use crate::{Error, ErrorKind, Result}; +use crate::Error; /// Byte range. /// @@ -42,23 +41,18 @@ impl fmt::Display for ByteRange { impl FromStr for ByteRange { type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { let tokens = s.splitn(2, '@').collect::>(); if tokens.is_empty() { - Err(ErrorKind::InvalidInput)?; + return Err(Error::invalid_input()); } - let length = tokens[0] - .parse() - .map_err(|e| ErrorKind::InvalidInput.cause(e))?; + let length = tokens[0].parse()?; + let start = { let mut result = None; if tokens.len() == 2 { - result = Some( - tokens[1] - .parse() - .map_err(|e| ErrorKind::InvalidInput.cause(e))?, - ); + result = Some(tokens[1].parse()?); } result }; diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index aa555e9..b044cc4 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -1,8 +1,8 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; use std::time::Duration; -use trackable::error::ErrorKindExt; + +use crate::Error; /// Non-negative decimal floating-point number. /// @@ -19,9 +19,10 @@ impl DecimalFloatingPoint { /// /// The given value must have a positive sign and be finite, /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. - pub fn new(n: f64) -> Result { - track_assert!(n.is_sign_positive(), ErrorKind::InvalidInput); - track_assert!(n.is_finite(), ErrorKind::InvalidInput); + pub fn new(n: f64) -> crate::Result { + if !n.is_sign_positive() || !n.is_finite() { + return Err(Error::invalid_input()); + } Ok(DecimalFloatingPoint(n)) } @@ -59,12 +60,12 @@ impl fmt::Display for DecimalFloatingPoint { impl FromStr for DecimalFloatingPoint { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!( - s.chars().all(|c| c.is_digit(10) || c == '.'), - ErrorKind::InvalidInput - ); - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + + fn from_str(input: &str) -> Result { + if !input.chars().all(|c| c.is_digit(10) || c == '.') { + return Err(Error::invalid_input()); + } + let n = input.parse()?; Ok(DecimalFloatingPoint(n)) } } diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index f323577..67128ff 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -1,7 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; + +use crate::Error; /// Decimal resolution. /// @@ -25,13 +25,15 @@ impl fmt::Display for DecimalResolution { impl FromStr for DecimalResolution { type Err = Error; - fn from_str(s: &str) -> Result { - let mut tokens = s.splitn(2, 'x'); - let width = tokens.next().expect("Never fails"); - let height = track_assert_some!(tokens.next(), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + let mut tokens = input.splitn(2, 'x'); + let width = tokens.next().ok_or(Error::missing_value("width"))?; + let height = tokens.next().ok_or(Error::missing_value("height"))?; + Ok(DecimalResolution { - width: track!(width.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, - height: track!(height.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, + width: width.parse().map_err(|e| Error::custom(e))?, + height: height.parse().map_err(|e| Error::custom(e))?, }) } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 15a6f35..87c7116 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -1,9 +1,10 @@ +use std::fmt; +use std::str::{self, FromStr}; + use crate::attribute::AttributePairs; use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; use crate::utils::{quote, unquote}; -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::str::{self, FromStr}; +use crate::Error; /// Decryption key. /// @@ -51,19 +52,21 @@ impl fmt::Display for DecryptionKey { impl FromStr for DecryptionKey { type Err = Error; - fn from_str(s: &str) -> Result { + + fn from_str(input: &str) -> Result { let mut method = None; let mut uri = None; let mut iv = None; let mut key_format = None; let mut key_format_versions = None; - let attrs = AttributePairs::parse(s); + + let attrs = AttributePairs::parse(input); for attr in attrs { - let (key, value) = track!(attr)?; + let (key, value) = (attr)?; match key { - "METHOD" => method = Some(track!(value.parse())?), + "METHOD" => method = Some((value.parse())?), "URI" => uri = Some(unquote(value)), - "IV" => iv = Some(track!(value.parse())?), + "IV" => iv = Some((value.parse())?), "KEYFORMAT" => key_format = Some(unquote(value)), "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), _ => { @@ -72,8 +75,10 @@ impl FromStr for DecryptionKey { } } } - let method = track_assert_some!(method, ErrorKind::InvalidInput); - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + + let method = method.ok_or(Error::missing_value("EXT-X-METHOD"))?; + let uri = uri.ok_or(Error::missing_value("EXT-X-URI"))?; + Ok(DecryptionKey { method, uri, diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 971e7bb..379e251 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// Encryption method. /// @@ -25,15 +26,15 @@ impl fmt::Display for EncryptionMethod { impl FromStr for EncryptionMethod { type Err = Error; - fn from_str(s: &str) -> Result { - match s { + + fn from_str(input: &str) -> Result { + match input { "AES-128" => Ok(EncryptionMethod::Aes128), "SAMPLE-AES" => Ok(EncryptionMethod::SampleAes), - _ => track_panic!( - ErrorKind::InvalidInput, + _ => Err(Error::custom(format!( "Unknown encryption method: {:?}", - s - ), + input + ))), } } } diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index 436178f..fc67f9e 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// HDCP level. /// @@ -25,11 +26,12 @@ impl fmt::Display for HdcpLevel { impl FromStr for HdcpLevel { type Err = Error; - fn from_str(s: &str) -> Result { - match s { + + fn from_str(input: &str) -> Result { + match input { "TYPE-0" => Ok(HdcpLevel::Type0), "NONE" => Ok(HdcpLevel::None), - _ => track_panic!(ErrorKind::InvalidInput, "Unknown HDCP level: {:?}", s), + _ => Err(Error::custom(format!("Unknown HDCP level: {:?}", input))), } } } diff --git a/src/types/hexadecimal_sequence.rs b/src/types/hexadecimal_sequence.rs index 930adca..fd6d69e 100644 --- a/src/types/hexadecimal_sequence.rs +++ b/src/types/hexadecimal_sequence.rs @@ -1,8 +1,8 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::ops::Deref; -use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; +use std::str::FromStr; + +use crate::Error; /// Hexadecimal sequence. /// @@ -49,20 +49,24 @@ impl fmt::Display for HexadecimalSequence { impl FromStr for HexadecimalSequence { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!( - s.starts_with("0x") || s.starts_with("0X"), - ErrorKind::InvalidInput - ); - track_assert!(s.len() % 2 == 0, ErrorKind::InvalidInput); - let mut v = Vec::with_capacity(s.len() / 2 - 1); - for c in s.as_bytes().chunks(2).skip(1) { - let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - let b = - track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - v.push(b); + fn from_str(input: &str) -> Result { + if !(input.starts_with("0x") || input.starts_with("0X")) { + return Err(Error::invalid_input()); } - Ok(HexadecimalSequence(v)) + + if input.len() % 2 != 0 { + return Err(Error::invalid_input()); + } + + let mut result = Vec::with_capacity(input.len() / 2 - 1); + + for c in input.as_bytes().chunks(2).skip(1) { + let d = String::from_utf8(c.to_vec()).map_err(|e| Error::custom(e))?; + let b = u8::from_str_radix(d.as_str(), 16)?; + result.push(b); + } + + Ok(HexadecimalSequence(result)) } } diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index 8b4aa56..cdff1f5 100644 --- a/src/types/in_stream_id.rs +++ b/src/types/in_stream_id.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// Identifier of a rendition within the segments in a media playlist. /// @@ -87,8 +88,9 @@ impl fmt::Display for InStreamId { impl FromStr for InStreamId { type Err = Error; - fn from_str(s: &str) -> Result { - Ok(match s { + + fn from_str(input: &str) -> Result { + Ok(match input { "CC1" => InStreamId::Cc1, "CC2" => InStreamId::Cc2, "CC3" => InStreamId::Cc3, @@ -156,7 +158,7 @@ impl FromStr for InStreamId { "SERVICE61" => InStreamId::Service61, "SERVICE62" => InStreamId::Service62, "SERVICE63" => InStreamId::Service63, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown instream id: {:?}", s), + _ => return Err(Error::custom(format!("Unknown instream id: {:?}", input))), }) } } diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 9cb64fe..9743f34 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -1,8 +1,8 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::ops::Deref; use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; + +use crate::Error; /// Initialization vector. /// @@ -37,20 +37,22 @@ impl fmt::Display for InitializationVector { impl FromStr for InitializationVector { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!( - s.starts_with("0x") || s.starts_with("0X"), - ErrorKind::InvalidInput - ); - track_assert_eq!(s.len() - 2, 32, ErrorKind::InvalidInput); + + fn from_str(s: &str) -> Result { + if !(s.starts_with("0x") || s.starts_with("0X")) { + return Err(Error::invalid_input()); + } + if s.len() - 2 != 32 { + return Err(Error::invalid_input()); + } let mut v = [0; 16]; for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() { - let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - let b = - track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + let d = str::from_utf8(c).map_err(|e| Error::custom(e))?; + let b = u8::from_str_radix(d, 16).map_err(|e| Error::custom(e))?; v[i] = b; } + Ok(InitializationVector(v)) } } diff --git a/src/types/media_type.rs b/src/types/media_type.rs index 103abde..f47b7d3 100644 --- a/src/types/media_type.rs +++ b/src/types/media_type.rs @@ -1,7 +1,8 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::{self, FromStr}; +use crate::Error; + /// Media type. /// /// See: [4.3.4.1. EXT-X-MEDIA] @@ -29,13 +30,16 @@ impl fmt::Display for MediaType { impl FromStr for MediaType { type Err = Error; - fn from_str(s: &str) -> Result { - Ok(match s { + + fn from_str(input: &str) -> Result { + Ok(match input { "AUDIO" => MediaType::Audio, "VIDEO" => MediaType::Video, "SUBTITLES" => MediaType::Subtitles, "CLOSED-CAPTIONS" => MediaType::ClosedCaptions, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown media type: {:?}", s), + _ => { + return Err(Error::invalid_input()); + } }) } } diff --git a/src/types/playlist_type.rs b/src/types/playlist_type.rs index e9dbb00..29efa54 100644 --- a/src/types/playlist_type.rs +++ b/src/types/playlist_type.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// Playlist type. /// @@ -25,11 +26,12 @@ impl fmt::Display for PlaylistType { impl FromStr for PlaylistType { type Err = Error; - fn from_str(s: &str) -> Result { - match s { + + fn from_str(input: &str) -> Result { + match input { "EVENT" => Ok(PlaylistType::Event), "VOD" => Ok(PlaylistType::Vod), - _ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s), + _ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))), } } } diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 312707b..626e13e 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -1,7 +1,8 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::{self, FromStr}; +use crate::Error; + /// [7. Protocol Version Compatibility] /// /// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7 @@ -32,8 +33,9 @@ impl fmt::Display for ProtocolVersion { } impl FromStr for ProtocolVersion { type Err = Error; - fn from_str(s: &str) -> Result { - Ok(match s { + + fn from_str(input: &str) -> Result { + Ok(match input { "1" => ProtocolVersion::V1, "2" => ProtocolVersion::V2, "3" => ProtocolVersion::V3, @@ -41,7 +43,7 @@ impl FromStr for ProtocolVersion { "5" => ProtocolVersion::V5, "6" => ProtocolVersion::V6, "7" => ProtocolVersion::V7, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s), + _ => return Err(Error::unknown_protocol_version(input)), }) } } diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 999d0a2..0c3691c 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -1,7 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; + +use crate::Error; /// Signed decimal floating-point number. /// @@ -18,9 +18,12 @@ impl SignedDecimalFloatingPoint { /// /// The given value must be finite, /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. - pub fn new(n: f64) -> Result { - track_assert!(n.is_finite(), ErrorKind::InvalidInput); - Ok(SignedDecimalFloatingPoint(n)) + pub fn new(n: f64) -> crate::Result { + if !n.is_finite() { + Err(Error::invalid_input()) + } else { + Ok(SignedDecimalFloatingPoint(n)) + } } /// Converts `DecimalFloatingPoint` to `f64`. @@ -45,12 +48,8 @@ impl fmt::Display for SignedDecimalFloatingPoint { impl FromStr for SignedDecimalFloatingPoint { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!( - s.chars().all(|c| c.is_digit(10) || c == '.' || c == '-'), - ErrorKind::InvalidInput - ); - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - Ok(SignedDecimalFloatingPoint(n)) + + fn from_str(input: &str) -> Result { + SignedDecimalFloatingPoint::new(input.parse().map_err(Error::parse_float_error)?) } } diff --git a/src/types/single_line_string.rs b/src/types/single_line_string.rs index 3f99d35..fea3dbb 100644 --- a/src/types/single_line_string.rs +++ b/src/types/single_line_string.rs @@ -1,4 +1,4 @@ -use crate::{ErrorKind, Result}; +use crate::Error; use std::fmt; use std::ops::Deref; @@ -17,10 +17,13 @@ impl SingleLineString { /// /// If the given string contains any control characters, /// this function will return an error which has the kind `ErrorKind::InvalidInput`. - pub fn new>(s: T) -> Result { + pub fn new>(s: T) -> crate::Result { let s = s.into(); - track_assert!(!s.chars().any(|c| c.is_control()), ErrorKind::InvalidInput); - Ok(SingleLineString(s)) + if s.chars().any(|c| c.is_control()) { + Err(Error::invalid_input()) + } else { + Ok(SingleLineString(s)) + } } } diff --git a/src/utils.rs b/src/utils.rs index 946dab9..7f557ec 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,23 +1,15 @@ -use crate::{ErrorKind, Result}; -use trackable::error::ErrorKindExt; +use crate::{Error, Result}; pub(crate) fn parse_yes_or_no>(s: T) -> Result { match s.as_ref() { "YES" => Ok(true), "NO" => Ok(false), - _ => track_panic!( - ErrorKind::InvalidInput, - "Unexpected value: {:?}", - s.as_ref() - ), + _ => Err(Error::invalid_input()), } } pub(crate) fn parse_u64>(s: T) -> Result { - let n = track!(s - .as_ref() - .parse() - .map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + let n = s.as_ref().parse().map_err(Error::unknown)?; // TODO: Error::number Ok(n) } @@ -45,13 +37,17 @@ pub(crate) fn quote(value: T) -> String { } /// Checks, if the given tag is at the start of the input. If this is the case, it will remove it -/// return the rest of the input, otherwise it will return an error. +/// and return the rest of the input. +/// +/// # Error +/// This function will return `Error::MissingTag`, if the input doesn't start with the tag, that +/// has been passed to this function. pub(crate) fn tag(input: &str, tag: T) -> crate::Result<&str> where T: AsRef, { if !input.starts_with(tag.as_ref()) { - Err(ErrorKind::InvalidInput)?; // TODO! + return Err(Error::missing_tag(tag.as_ref(), input)); } let result = input.split_at(tag.as_ref().len()).1; Ok(result) From 3721106795c846088c1ae98dbab1a13e960323c8 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 11:31:16 +0200 Subject: [PATCH 06/25] Rewrote `AttributePairs` --- src/attribute.rs | 196 +++++++++++------- src/error.rs | 13 ++ .../master_playlist/i_frame_stream_inf.rs | 6 +- src/tags/master_playlist/media.rs | 7 +- src/tags/master_playlist/session_data.rs | 8 +- src/tags/master_playlist/stream_inf.rs | 14 +- src/tags/media_segment/date_range.rs | 6 +- src/tags/media_segment/key.rs | 27 ++- src/tags/media_segment/map.rs | 7 +- src/tags/shared/start.rs | 7 +- src/types/decryption_key.rs | 6 +- 11 files changed, 168 insertions(+), 129 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index 212728b..338fed5 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,94 +1,136 @@ -use crate::{Error, Result}; -use std::collections::HashSet; -use std::str; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; -#[derive(Debug)] -pub struct AttributePairs<'a> { - input: &'a str, - visited_keys: HashSet<&'a str>, -} -impl<'a> AttributePairs<'a> { - pub fn parse(input: &'a str) -> Self { - AttributePairs { - input, - visited_keys: HashSet::new(), - } - } +use crate::Error; - fn parse_name(&mut self) -> Result<&'a str> { - for i in 0..self.input.len() { - match self.input.as_bytes()[i] { - b'=' => { - let (key, _) = self.input.split_at(i); - let (_, rest) = self.input.split_at(i + 1); - self.input = rest; - return Ok(key); - } - b'A'..=b'Z' | b'0'..=b'9' | b'-' => {} - _ => { - return Err(Error::invalid_attribute(self.input.to_string())); - } - } - } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AttributePairs(HashMap); - Err(Error::missing_value(self.input.to_string())) - } - - fn parse_raw_value(&mut self) -> &'a str { - let mut in_quote = false; - let mut value_end = self.input.len(); - let mut next = self.input.len(); - for (i, c) in self.input.bytes().enumerate() { - match c { - b'"' => { - in_quote = !in_quote; - } - b',' if !in_quote => { - value_end = i; - next = i + 1; - break; - } - _ => {} - } - } - let (value, _) = self.input.split_at(value_end); - let (_, rest) = self.input.split_at(next); - self.input = rest; - value +impl AttributePairs { + pub fn new() -> Self { + Self::default() } } -impl<'a> Iterator for AttributePairs<'a> { - type Item = Result<(&'a str, &'a str)>; - fn next(&mut self) -> Option { - if self.input.is_empty() { - return None; - } +impl Deref for AttributePairs { + type Target = HashMap; - let result = || -> Result<(&'a str, &'a str)> { - let key = self.parse_name()?; - self.visited_keys.insert(key); - - let value = self.parse_raw_value(); - Ok((key, value)) - }(); - Some(result) + fn deref(&self) -> &Self::Target { + &self.0 } } +impl DerefMut for AttributePairs { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl IntoIterator for AttributePairs { + type Item = (String, String); + type IntoIter = ::std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a AttributePairs { + type Item = (&'a String, &'a String); + type IntoIter = ::std::collections::hash_map::Iter<'a, String, String>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl FromStr for AttributePairs { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut result = AttributePairs::new(); + + for line in split(input) { + let pair = line.trim().split("=").collect::>(); + + if pair.len() < 2 { + return Err(Error::invalid_input()); + } + + let key = pair[0].to_uppercase(); + let value = pair[1].to_string(); + + result.insert(key.to_string(), value.to_string()); + } + Ok(result) + } +} + +fn split(value: &str) -> Vec { + let mut result = vec![]; + + let mut inside_quotes = false; + let mut temp_string = String::new(); + + for c in value.chars() { + match c { + '"' => { + if inside_quotes { + inside_quotes = false; + } else { + inside_quotes = true; + } + temp_string.push(c); + } + ',' => { + if !inside_quotes { + result.push(temp_string); + temp_string = String::new(); + } else { + temp_string.push(c); + } + } + _ => { + temp_string.push(c); + } + } + } + result.push(temp_string); + + result +} + #[cfg(test)] mod test { use super::*; #[test] - fn it_works() { - let mut pairs = AttributePairs::parse("FOO=BAR,BAR=\"baz,qux\",ABC=12.3"); - assert_eq!(pairs.next().map(|x| x.ok()), Some(Some(("FOO", "BAR")))); - assert_eq!( - pairs.next().map(|x| x.ok()), - Some(Some(("BAR", "\"baz,qux\""))) - ); - assert_eq!(pairs.next().map(|x| x.ok()), Some(Some(("ABC", "12.3")))); - assert_eq!(pairs.next().map(|x| x.ok()), None) + fn test_parser() { + let pairs = ("FOO=BAR,BAR=\"baz,qux\",ABC=12.3") + .parse::() + .unwrap(); + + let mut iterator = pairs.iter(); + assert!(iterator.any(|(k, v)| k == "FOO" && "BAR" == v)); + + let mut iterator = pairs.iter(); + assert!(iterator.any(|(k, v)| k == "BAR" && v == "\"baz,qux\"")); + + let mut iterator = pairs.iter(); + assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3")); + } + + #[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")); } } diff --git a/src/error.rs b/src/error.rs index ff1fddf..d4666c7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,9 @@ pub enum ErrorKind { #[fail(display = "Unknown Protocol version: {:?}", _0)] UnknownProtocolVersion(String), + #[fail(display = "IoError: {}", _0)] + Io(String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -164,6 +167,10 @@ impl Error { pub(crate) fn unknown_protocol_version(value: T) -> Self { Self::from(ErrorKind::UnknownProtocolVersion(value.to_string())) } + + pub(crate) fn io(value: T) -> Self { + Self::from(ErrorKind::Io(value.to_string())) + } } impl From for Error { @@ -177,3 +184,9 @@ impl From for Error { Error::parse_float_error(value) } } + +impl From for Error { + fn from(value: ::std::io::Error) -> Self { + Error::io(value) + } +} diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 7f9abd3..93e5425 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -94,10 +94,8 @@ impl FromStr for ExtXIFrameStreamInf { let mut hdcp_level = None; let mut video = None; - let attrs = AttributePairs::parse(input); - for attr in attrs { - let (key, value) = attr?; - match key { + for (key, value) in input.parse::()? { + match key.as_str() { "URI" => uri = Some(unquote(value)), "BANDWIDTH" => bandwidth = Some(parse_u64(value)?), "AVERAGE-BANDWIDTH" => average_bandwidth = Some(parse_u64(value)?), diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 40d1e41..f63f1f3 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -331,10 +331,9 @@ impl FromStr for ExtXMedia { let input = tag(input, Self::PREFIX)?; let mut builder = ExtXMediaBuilder::new(); - let attrs = AttributePairs::parse(input); - for attr in attrs { - let (key, value) = attr?; - match key { + + for (key, value) in input.parse::()? { + match key.as_str() { "TYPE" => { builder.media_type(value.parse()?); } diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index f246862..12d138d 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -77,10 +77,8 @@ impl FromStr for ExtXSessionData { let mut uri = None; let mut language = None; - let attrs = AttributePairs::parse(input); - for attr in attrs { - let (key, value) = attr?; - match key { + for (key, value) in input.parse::()? { + match key.as_str() { "DATA-ID" => data_id = Some(unquote(value)), "VALUE" => session_value = Some(unquote(value)), "URI" => uri = Some(unquote(value)), @@ -92,7 +90,7 @@ impl FromStr for ExtXSessionData { } } - let data_id = data_id.ok_or(Error::missing_value("DATA-ID"))?; + let data_id = data_id.ok_or(Error::missing_value("EXT-X-DATA-ID"))?; let data = { if let Some(value) = session_value { if uri.is_some() { diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index e69df8b..a172da9 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -149,10 +149,10 @@ impl FromStr for ExtXStreamInf { fn from_str(input: &str) -> Result { let mut lines = input.lines(); - let first_line = lines.next().ok_or(Error::invalid_input())?; // TODO! - let second_line = lines.next().ok_or(Error::invalid_input())?; // TODO! + let first_line = lines.next().ok_or(Error::missing_value("first_line"))?; + let second_line = lines.next().ok_or(Error::missing_value("second_line"))?; - tag(first_line, Self::PREFIX)?; + let first_line = tag(first_line, Self::PREFIX)?; let uri = SingleLineString::new(second_line)?; @@ -167,10 +167,8 @@ impl FromStr for ExtXStreamInf { let mut subtitles = None; let mut closed_captions = None; - let attrs = AttributePairs::parse(first_line.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = (attr)?; - match key { + for (key, value) in first_line.parse::()? { + match key.as_str() { "BANDWIDTH" => bandwidth = Some((parse_u64(value))?), "AVERAGE-BANDWIDTH" => average_bandwidth = Some((parse_u64(value))?), "CODECS" => codecs = Some(unquote(value)), @@ -188,7 +186,7 @@ impl FromStr for ExtXStreamInf { } } - let bandwidth = bandwidth.ok_or(Error::missing_value("BANDWIDTH"))?; + let bandwidth = bandwidth.ok_or(Error::missing_value("EXT-X-BANDWIDTH"))?; Ok(ExtXStreamInf { uri, diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 8fa6f11..841b513 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -96,11 +96,9 @@ impl FromStr for ExtXDateRange { let mut end_on_next = false; let mut client_attributes = BTreeMap::new(); - let attrs = AttributePairs::parse(input); - for attr in attrs { - let (key, value) = attr?; - match key { + for (key, value) in input.parse::()? { + match key.as_str() { "ID" => id = Some(unquote(value)), "CLASS" => class = Some(unquote(value)), "START-DATE" => start_date = Some(unquote(value)), diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index fb47c83..8e59522 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -10,33 +10,31 @@ use crate::Error; /// /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXKey { - key: Option, -} +pub struct ExtXKey(Option); impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; /// Makes a new `ExtXKey` tag. pub const fn new(key: DecryptionKey) -> Self { - ExtXKey { key: Some(key) } + Self(Some(key)) } /// Makes a new `ExtXKey` tag without a decryption key. /// /// This tag has the `METHDO=NONE` attribute. pub const fn new_without_key() -> Self { - ExtXKey { key: None } + Self(None) } /// Returns the decryption key for the following media segments and media initialization sections. pub fn key(&self) -> Option<&DecryptionKey> { - self.key.as_ref() + self.0.as_ref() } /// Returns the protocol compatibility version that this tag requires. pub fn requires_version(&self) -> ProtocolVersion { - self.key + self.0 .as_ref() .map_or(ProtocolVersion::V1, |k| k.requires_version()) } @@ -45,7 +43,7 @@ impl ExtXKey { impl fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - if let Some(ref key) = self.key { + if let Some(ref key) = self.0 { write!(f, "{}", key)?; } else { write!(f, "METHOD=NONE")?; @@ -60,17 +58,18 @@ impl FromStr for ExtXKey { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - if AttributePairs::parse(input).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) { - for attr in AttributePairs::parse(input) { - let (key, _) = attr?; + let pairs = input.parse::()?; + + if pairs.iter().any(|(k, v)| k == "METHOD" && v == "NONE") { + for (key, _) in pairs { if key == "URI" || key == "IV" || key == "KEYFORMAT" || key == "KEYFORMATVERSIONS" { return Err(Error::invalid_input()); } } - Ok(ExtXKey { key: None }) + + Ok(Self(None)) } else { - let key = input.parse()?; - Ok(ExtXKey { key: Some(key) }) + Ok(Self(Some(input.parse()?))) } } } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 728afa4..97ca7fc 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -69,10 +69,9 @@ impl FromStr for ExtXMap { let mut uri = None; let mut range = None; - let attrs = AttributePairs::parse(input); - for attr in attrs { - let (key, value) = (attr)?; - match key { + + for (key, value) in input.parse::()? { + match key.as_str() { "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 f946992..06b6d90 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -71,11 +71,8 @@ impl FromStr for ExtXStart { let mut time_offset = None; let mut precise = false; - let attrs = AttributePairs::parse(input); - - for attr in attrs { - let (key, value) = (attr)?; - match key { + for (key, value) in input.parse::()? { + match key.as_str() { "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 87c7116..014a61f 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -60,10 +60,8 @@ impl FromStr for DecryptionKey { let mut key_format = None; let mut key_format_versions = None; - let attrs = AttributePairs::parse(input); - for attr in attrs { - let (key, value) = (attr)?; - match key { + for (key, value) in input.parse::()? { + match key.as_str() { "METHOD" => method = Some((value.parse())?), "URI" => uri = Some(unquote(value)), "IV" => iv = Some((value.parse())?), From 273c0990dce8e86399100235503f4509c662e875 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 11:57:56 +0200 Subject: [PATCH 07/25] Rewrote `Lines` --- src/line.rs | 117 ++++++++++++++++++----------------------- src/master_playlist.rs | 28 ++++++---- src/media_playlist.rs | 5 +- 3 files changed, 72 insertions(+), 78 deletions(-) diff --git a/src/line.rs b/src/line.rs index 0641bc5..4cd76bf 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,87 +1,74 @@ use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::tags; use crate::types::SingleLineString; use crate::Error; -#[derive(Debug)] -pub struct Lines<'a> { - input: &'a str, -} -impl<'a> Lines<'a> { - pub const fn new(input: &'a str) -> Self { - Lines { input } +#[derive(Debug, Default)] +pub struct Lines(Vec); + +impl Lines { + pub fn new() -> Self { + Self::default() } +} - fn read_line(&mut self) -> crate::Result> { - let mut end = self.input.len(); - let mut next_start = self.input.len(); - let mut adjust = 0; - let mut next_line_of_ext_x_stream_inf = false; +impl FromStr for Lines { + type Err = Error; - for (i, c) in self.input.char_indices() { - match c { - '\n' => { - if !next_line_of_ext_x_stream_inf - && self.input.starts_with(tags::ExtXStreamInf::PREFIX) - { - next_line_of_ext_x_stream_inf = true; - adjust = 0; - continue; - } - next_start = i + 1; - end = i - adjust; - break; - } - '\r' => { - adjust = 1; - } - _ => { - if c.is_control() { - return Err(Error::invalid_input()); - } - adjust = 0; - } + fn from_str(input: &str) -> Result { + let mut result = Lines::new(); + + for line in input.lines() { + // ignore empty lines + if line.trim().len() == 0 { + continue; } - } - let raw_line = &self.input[..end]; - let line = if raw_line.is_empty() { - Line::Blank - } else if raw_line.starts_with("#EXT") { - Line::Tag((raw_line.parse())?) - } else if raw_line.starts_with('#') { - Line::Comment(raw_line) - } else { - let uri = SingleLineString::new(raw_line)?; - Line::Uri(uri) - }; + let pline = { + if line.starts_with("#EXT") { + Line::Tag(line.parse()?) + } else if line.starts_with("#") { + continue; // ignore comments + } else { + Line::Uri(SingleLineString::new(line)?) + } + }; - self.input = &self.input[next_start..]; - Ok(line) - } -} -impl<'a> Iterator for Lines<'a> { - type Item = crate::Result>; - - fn next(&mut self) -> Option { - if self.input.is_empty() { - return None; + result.push(pline); } - match self.read_line() { - Err(e) => Some(Err(e)), - Ok(line) => Some(Ok(line)), - } + Ok(result) + } +} + +impl IntoIterator for Lines { + type Item = Line; + type IntoIter = ::std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Deref for Lines { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Lines { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } -#[allow(clippy::large_enum_variant)] #[derive(Debug, PartialEq, Eq)] -pub enum Line<'a> { - Blank, - Comment(&'a str), +pub enum Line { Tag(Tag), Uri(SingleLineString), } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 5ce9a38..85ba8fe 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -9,7 +9,7 @@ use crate::tags::{ ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; -use crate::{Error, Result}; +use crate::Error; /// Master playlist builder. #[derive(Debug, Clone)] @@ -69,7 +69,7 @@ impl MasterPlaylistBuilder { } /// Builds a `MasterPlaylist` instance. - pub fn finish(self) -> Result { + pub fn finish(self) -> crate::Result { let required_version = self.required_version(); let specified_version = self.version.unwrap_or(required_version); @@ -116,7 +116,7 @@ impl MasterPlaylistBuilder { .expect("Never fails") } - fn validate_stream_inf_tags(&self) -> Result<()> { + fn validate_stream_inf_tags(&self) -> crate::Result<()> { let mut has_none_closed_captions = false; for t in &self.stream_inf_tags { if let Some(group_id) = t.audio() { @@ -159,7 +159,7 @@ impl MasterPlaylistBuilder { Ok(()) } - fn validate_i_frame_stream_inf_tags(&self) -> Result<()> { + fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> { for t in &self.i_frame_stream_inf_tags { if let Some(group_id) = t.video() { if !self.check_media_group(MediaType::Video, group_id) { @@ -171,7 +171,7 @@ impl MasterPlaylistBuilder { Ok(()) } - fn validate_session_data_tags(&self) -> Result<()> { + fn validate_session_data_tags(&self) -> crate::Result<()> { let mut set = HashSet::new(); for t in &self.session_data_tags { if !set.insert((t.data_id(), t.language())) { @@ -182,7 +182,7 @@ impl MasterPlaylistBuilder { Ok(()) } - fn validate_session_key_tags(&self) -> Result<()> { + fn validate_session_key_tags(&self) -> crate::Result<()> { let mut set = HashSet::new(); for t in &self.session_key_tags { if !set.insert(t.key()) { @@ -294,11 +294,11 @@ impl fmt::Display for MasterPlaylist { impl FromStr for MasterPlaylist { type Err = Error; - fn from_str(s: &str) -> Result { + + fn from_str(input: &str) -> Result { let mut builder = MasterPlaylistBuilder::new(); - for (i, line) in Lines::new(s).enumerate() { - match (line)? { - Line::Blank | Line::Comment(_) => {} + for (i, line) in input.parse::()?.into_iter().enumerate() { + match line { Line::Tag(tag) => { if i == 0 { if tag != Tag::ExtM3u(ExtM3u) { @@ -373,3 +373,11 @@ impl FromStr for MasterPlaylist { builder.finish() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parser() {} +} diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 0b09225..a4074b6 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -337,9 +337,8 @@ impl MediaPlaylistOptions { let mut segment = MediaSegmentBuilder::new(); let mut has_partial_segment = false; let mut has_discontinuity_tag = false; - for (i, line) in Lines::new(m3u8).enumerate() { - match (line)? { - Line::Blank | Line::Comment(_) => {} + for (i, line) in m3u8.parse::()?.into_iter().enumerate() { + match line { Line::Tag(tag) => { if i == 0 { if tag != Tag::ExtM3u(ExtM3u) { From 7483f49fe985f78ce2d36c96898f2cd2f08a48f2 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 12:29:54 +0200 Subject: [PATCH 08/25] fix bug --- src/line.rs | 73 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/line.rs b/src/line.rs index 4cd76bf..70328dd 100644 --- a/src/line.rs +++ b/src/line.rs @@ -21,19 +21,40 @@ impl FromStr for Lines { fn from_str(input: &str) -> Result { let mut result = Lines::new(); - for line in input.lines() { + let mut stream_inf = false; + let mut stream_inf_line = None; + + for l in input.lines() { + let line = l.trim(); + // ignore empty lines - if line.trim().len() == 0 { + if line.len() == 0 { continue; } let pline = { - if line.starts_with("#EXT") { + if line.starts_with(tags::ExtXStreamInf::PREFIX) { + stream_inf = true; + stream_inf_line = Some(line); + + continue; + } else if line.starts_with("#EXT") { Line::Tag(line.parse()?) } else if line.starts_with("#") { continue; // ignore comments } else { - Line::Uri(SingleLineString::new(line)?) + if stream_inf { + stream_inf = false; + if let Some(first_line) = stream_inf_line { + let res = Line::Tag(format!("{}\n{}", first_line, line).parse()?); + stream_inf_line = None; + res + } else { + continue; + } + } else { + Line::Uri(SingleLineString::new(line)?) + } } }; @@ -136,49 +157,49 @@ impl FromStr for Tag { fn from_str(s: &str) -> Result { if s.starts_with(tags::ExtM3u::PREFIX) { - (s.parse().map(Tag::ExtM3u)) + s.parse().map(Tag::ExtM3u) } else if s.starts_with(tags::ExtXVersion::PREFIX) { - (s.parse().map(Tag::ExtXVersion)) + s.parse().map(Tag::ExtXVersion) } else if s.starts_with(tags::ExtInf::PREFIX) { - (s.parse().map(Tag::ExtInf)) + s.parse().map(Tag::ExtInf) } else if s.starts_with(tags::ExtXByteRange::PREFIX) { - (s.parse().map(Tag::ExtXByteRange)) + s.parse().map(Tag::ExtXByteRange) } else if s.starts_with(tags::ExtXDiscontinuity::PREFIX) { - (s.parse().map(Tag::ExtXDiscontinuity)) + s.parse().map(Tag::ExtXDiscontinuity) } else if s.starts_with(tags::ExtXKey::PREFIX) { - (s.parse().map(Tag::ExtXKey)) + s.parse().map(Tag::ExtXKey) } else if s.starts_with(tags::ExtXMap::PREFIX) { - (s.parse().map(Tag::ExtXMap)) + s.parse().map(Tag::ExtXMap) } else if s.starts_with(tags::ExtXProgramDateTime::PREFIX) { - (s.parse().map(Tag::ExtXProgramDateTime)) + s.parse().map(Tag::ExtXProgramDateTime) } else if s.starts_with(tags::ExtXTargetDuration::PREFIX) { - (s.parse().map(Tag::ExtXTargetDuration)) + s.parse().map(Tag::ExtXTargetDuration) } else if s.starts_with(tags::ExtXDateRange::PREFIX) { - (s.parse().map(Tag::ExtXDateRange)) + s.parse().map(Tag::ExtXDateRange) } else if s.starts_with(tags::ExtXMediaSequence::PREFIX) { - (s.parse().map(Tag::ExtXMediaSequence)) + s.parse().map(Tag::ExtXMediaSequence) } else if s.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) { - (s.parse().map(Tag::ExtXDiscontinuitySequence)) + s.parse().map(Tag::ExtXDiscontinuitySequence) } else if s.starts_with(tags::ExtXEndList::PREFIX) { - (s.parse().map(Tag::ExtXEndList)) + s.parse().map(Tag::ExtXEndList) } else if s.starts_with(tags::ExtXPlaylistType::PREFIX) { - (s.parse().map(Tag::ExtXPlaylistType)) + s.parse().map(Tag::ExtXPlaylistType) } else if s.starts_with(tags::ExtXIFramesOnly::PREFIX) { - (s.parse().map(Tag::ExtXIFramesOnly)) + s.parse().map(Tag::ExtXIFramesOnly) } else if s.starts_with(tags::ExtXMedia::PREFIX) { - (s.parse().map(Tag::ExtXMedia)) + s.parse().map(Tag::ExtXMedia) } else if s.starts_with(tags::ExtXStreamInf::PREFIX) { - (s.parse().map(Tag::ExtXStreamInf)) + s.parse().map(Tag::ExtXStreamInf) } else if s.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { - (s.parse().map(Tag::ExtXIFrameStreamInf)) + s.parse().map(Tag::ExtXIFrameStreamInf) } else if s.starts_with(tags::ExtXSessionData::PREFIX) { - (s.parse().map(Tag::ExtXSessionData)) + s.parse().map(Tag::ExtXSessionData) } else if s.starts_with(tags::ExtXSessionKey::PREFIX) { - (s.parse().map(Tag::ExtXSessionKey)) + s.parse().map(Tag::ExtXSessionKey) } else if s.starts_with(tags::ExtXIndependentSegments::PREFIX) { - (s.parse().map(Tag::ExtXIndependentSegments)) + s.parse().map(Tag::ExtXIndependentSegments) } else if s.starts_with(tags::ExtXStart::PREFIX) { - (s.parse().map(Tag::ExtXStart)) + s.parse().map(Tag::ExtXStart) } else { SingleLineString::new(s).map(Tag::Unknown) } From a2614b5acaddefbbab5df95f36e4c8271b64a70e Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 12:34:34 +0200 Subject: [PATCH 09/25] added test --- src/error.rs | 17 +++++++++++++++++ src/master_playlist.rs | 23 +++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index d4666c7..5d089a9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -49,6 +49,12 @@ pub enum ErrorKind { #[fail(display = "IoError: {}", _0)] Io(String), + #[fail( + display = "VersionError: required_version: {:?}, specified_version: {:?}", + _0, _1 + )] + VersionError(String, String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -171,6 +177,17 @@ impl Error { pub(crate) fn io(value: T) -> Self { Self::from(ErrorKind::Io(value.to_string())) } + + pub(crate) fn required_version(required_version: T, specified_version: U) -> Self + where + T: ToString, + U: ToString, + { + Self::from(ErrorKind::VersionError( + required_version.to_string(), + specified_version.to_string(), + )) + } } impl From for Error { diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 85ba8fe..683640d 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -73,9 +73,8 @@ impl MasterPlaylistBuilder { let required_version = self.required_version(); let specified_version = self.version.unwrap_or(required_version); - if required_version <= specified_version { - // "required_version:{}, specified_version:{}" - return Err(Error::invalid_input()); + if required_version < specified_version { + return Err(Error::required_version(required_version, specified_version)); } (self.validate_stream_inf_tags())?; @@ -379,5 +378,21 @@ mod tests { use super::*; #[test] - fn test_parser() {} + fn test_parser() { + let playlist = r#" + #EXTM3U + #EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" + http://example.com/low/index.m3u8 + #EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" + http://example.com/lo_mid/index.m3u8 + #EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" + http://example.com/hi_mid/index.m3u8 + #EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" + http://example.com/high/index.m3u8 + #EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" + http://example.com/audio/index.m3u8 + "# + .parse::() + .unwrap(); + } } From b1aa51267981dc070b0fefdb28a52c64a28ebbfe Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 13:26:16 +0200 Subject: [PATCH 10/25] added master_playlist builder --- Cargo.toml | 1 + src/error.rs | 7 + src/master_playlist.rs | 383 ++++++++++--------------- src/tags/basic/version.rs | 12 + src/tags/master_playlist/stream_inf.rs | 6 +- 5 files changed, 177 insertions(+), 232 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65c0cf4..35c5a24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ codecov = {repository = "sile/hls_m3u8"} [dependencies] getset = "0.0.8" failure = "0.1.5" +derive_builder = "0.7.2" [dev-dependencies] clap = "2" diff --git a/src/error.rs b/src/error.rs index 5d089a9..92e77dd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,6 +55,9 @@ pub enum ErrorKind { )] VersionError(String, String), + #[fail(display = "BuilderError: {}", _0)] + BuilderError(String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -188,6 +191,10 @@ impl Error { specified_version.to_string(), )) } + + pub(crate) fn builder_error(value: T) -> Self { + Self::from(ErrorKind::BuilderError(value.to_string())) + } } impl From for Error { diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 683640d..ed8e880 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,212 +1,21 @@ use std::collections::HashSet; use std::fmt; -use std::iter; use std::str::FromStr; +use derive_builder::Builder; + use crate::line::{Line, Lines, Tag}; use crate::tags::{ ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, - ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag, + ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::Error; -/// Master playlist builder. -#[derive(Debug, Clone)] -pub struct MasterPlaylistBuilder { - version: Option, - independent_segments_tag: Option, - start_tag: Option, - media_tags: Vec, - stream_inf_tags: Vec, - i_frame_stream_inf_tags: Vec, - session_data_tags: Vec, - session_key_tags: Vec, -} - -impl MasterPlaylistBuilder { - /// Makes a new `MasterPlaylistBuilder` instance. - pub fn new() -> Self { - MasterPlaylistBuilder { - version: None, - independent_segments_tag: None, - start_tag: None, - media_tags: Vec::new(), - stream_inf_tags: Vec::new(), - i_frame_stream_inf_tags: Vec::new(), - session_data_tags: Vec::new(), - session_key_tags: Vec::new(), - } - } - - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility version greater than `version`, - /// `finish()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. - pub fn version(&mut self, version: ProtocolVersion) -> &mut Self { - self.version = Some(version); - self - } - - /// Adds the given tag to the resulting playlist. - /// - /// If it is forbidden to have multiple instance of the tag, the existing one will be overwritten. - pub fn tag>(&mut self, tag: T) -> &mut Self { - match tag.into() { - MasterPlaylistTag::ExtXIndependentSegments(t) => { - self.independent_segments_tag = Some(t); - } - MasterPlaylistTag::ExtXStart(t) => self.start_tag = Some(t), - MasterPlaylistTag::ExtXMedia(t) => self.media_tags.push(t), - MasterPlaylistTag::ExtXStreamInf(t) => self.stream_inf_tags.push(t), - MasterPlaylistTag::ExtXIFrameStreamInf(t) => self.i_frame_stream_inf_tags.push(t), - MasterPlaylistTag::ExtXSessionData(t) => self.session_data_tags.push(t), - MasterPlaylistTag::ExtXSessionKey(t) => self.session_key_tags.push(t), - } - self - } - - /// Builds a `MasterPlaylist` instance. - pub fn finish(self) -> crate::Result { - let required_version = self.required_version(); - let specified_version = self.version.unwrap_or(required_version); - - if required_version < specified_version { - return Err(Error::required_version(required_version, specified_version)); - } - - (self.validate_stream_inf_tags())?; - (self.validate_i_frame_stream_inf_tags())?; - (self.validate_session_data_tags())?; - (self.validate_session_key_tags())?; - - Ok(MasterPlaylist { - version_tag: ExtXVersion::new(specified_version), - independent_segments_tag: self.independent_segments_tag, - start_tag: self.start_tag, - media_tags: self.media_tags, - stream_inf_tags: self.stream_inf_tags, - i_frame_stream_inf_tags: self.i_frame_stream_inf_tags, - session_data_tags: self.session_data_tags, - session_key_tags: self.session_key_tags, - }) - } - - fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain( - self.independent_segments_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.start_tag.iter().map(|t| t.requires_version())) - .chain(self.media_tags.iter().map(|t| t.requires_version())) - .chain(self.stream_inf_tags.iter().map(|t| t.requires_version())) - .chain( - self.i_frame_stream_inf_tags - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.session_data_tags.iter().map(|t| t.requires_version())) - .chain(self.session_key_tags.iter().map(|t| t.requires_version())) - .max() - .expect("Never fails") - } - - fn validate_stream_inf_tags(&self) -> crate::Result<()> { - let mut has_none_closed_captions = false; - for t in &self.stream_inf_tags { - if let Some(group_id) = t.audio() { - if !self.check_media_group(MediaType::Audio, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - if let Some(group_id) = t.video() { - if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - if let Some(group_id) = t.subtitles() { - if !self.check_media_group(MediaType::Subtitles, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - match t.closed_captions() { - Some(&ClosedCaptions::GroupId(ref group_id)) => { - if !self.check_media_group(MediaType::ClosedCaptions, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - Some(&ClosedCaptions::None) => { - has_none_closed_captions = true; - } - None => {} - } - } - if has_none_closed_captions { - if !self - .stream_inf_tags - .iter() - .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) - { - return Err(Error::invalid_input()); - } - } - - Ok(()) - } - - fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> { - for t in &self.i_frame_stream_inf_tags { - if let Some(group_id) = t.video() { - if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id)); - } - } - } - - Ok(()) - } - - fn validate_session_data_tags(&self) -> crate::Result<()> { - let mut set = HashSet::new(); - for t in &self.session_data_tags { - if !set.insert((t.data_id(), t.language())) { - return Err(Error::custom(format!("Conflict: {}", t))); - } - } - - Ok(()) - } - - fn validate_session_key_tags(&self) -> crate::Result<()> { - let mut set = HashSet::new(); - for t in &self.session_key_tags { - if !set.insert(t.key()) { - return Err(Error::custom(format!("Conflict: {}", t))); - } - } - - Ok(()) - } - - fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { - self.media_tags - .iter() - .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) - } -} - -impl Default for MasterPlaylistBuilder { - fn default() -> Self { - Self::new() - } -} - /// Master playlist. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Builder, Default)] +#[builder(build_fn(validate = "Self::validate"))] +#[builder(setter(into, strip_option), default)] pub struct MasterPlaylist { version_tag: ExtXVersion, independent_segments_tag: Option, @@ -219,6 +28,11 @@ pub struct MasterPlaylist { } impl MasterPlaylist { + /// Returns a Builder for a MasterPlaylist. + pub fn builder() -> MasterPlaylistBuilder { + MasterPlaylistBuilder::default() + } + /// Returns the `EXT-X-VERSION` tag contained in the playlist. pub const fn version_tag(&self) -> ExtXVersion { self.version_tag @@ -260,6 +74,95 @@ impl MasterPlaylist { } } +impl MasterPlaylistBuilder { + pub(crate) fn validate(&self) -> Result<(), String> { + // validate stream inf tags + if let Some(stream_inf_tags) = &self.stream_inf_tags { + let mut has_none_closed_captions = false; + for t in stream_inf_tags { + if let Some(group_id) = t.audio() { + if !self.check_media_group(MediaType::Audio, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + if let Some(group_id) = t.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + if let Some(group_id) = t.subtitles() { + if !self.check_media_group(MediaType::Subtitles, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + match t.closed_captions() { + Some(&ClosedCaptions::GroupId(ref group_id)) => { + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + Some(&ClosedCaptions::None) => { + has_none_closed_captions = true; + } + None => {} + } + } + if has_none_closed_captions { + if !stream_inf_tags + .iter() + .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) + { + return Err(Error::invalid_input().to_string()); + } + } + } + + // validate i_frame_stream_inf_tags + if let Some(i_frame_stream_inf_tags) = &self.i_frame_stream_inf_tags { + for t in i_frame_stream_inf_tags { + if let Some(group_id) = t.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id).to_string()); + } + } + } + } + + // validate session_data_tags + if let Some(session_data_tags) = &self.session_data_tags { + let mut set = HashSet::new(); + + for t in session_data_tags { + if !set.insert((t.data_id(), t.language())) { + return Err(Error::custom(format!("Conflict: {}", t)).to_string()); + } + } + } + + // validate session_key_tags + if let Some(session_key_tags) = &self.session_key_tags { + let mut set = HashSet::new(); + for t in session_key_tags { + if !set.insert(t.key()) { + return Err(Error::custom(format!("Conflict: {}", t)).to_string()); + } + } + } + + Ok(()) + } + + fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { + if let Some(media_tags) = &self.media_tags { + media_tags + .iter() + .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) + } else { + false + } + } +} + impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; @@ -295,7 +198,14 @@ impl FromStr for MasterPlaylist { type Err = Error; fn from_str(input: &str) -> Result { - let mut builder = MasterPlaylistBuilder::new(); + let mut builder = MasterPlaylist::builder(); + + let mut media_tags = vec![]; + let mut stream_inf_tags = vec![]; + let mut i_frame_stream_inf_tags = vec![]; + let mut session_data_tags = vec![]; + let mut session_key_tags = vec![]; + for (i, line) in input.parse::()?.into_iter().enumerate() { match line { Line::Tag(tag) => { @@ -310,10 +220,7 @@ impl FromStr for MasterPlaylist { return Err(Error::invalid_input()); } Tag::ExtXVersion(t) => { - if builder.version.is_some() { - return Err(Error::invalid_input()); - } - builder.version(t.version()); + builder.version_tag(t.version()); } Tag::ExtInf(_) | Tag::ExtXByteRange(_) @@ -331,31 +238,25 @@ impl FromStr for MasterPlaylist { return Err(Error::invalid_input()); // TODO: why? } Tag::ExtXMedia(t) => { - builder.tag(t); + media_tags.push(t); } Tag::ExtXStreamInf(t) => { - builder.tag(t); + stream_inf_tags.push(t); } Tag::ExtXIFrameStreamInf(t) => { - builder.tag(t); + i_frame_stream_inf_tags.push(t); } Tag::ExtXSessionData(t) => { - builder.tag(t); + session_data_tags.push(t); } Tag::ExtXSessionKey(t) => { - builder.tag(t); + session_key_tags.push(t); } Tag::ExtXIndependentSegments(t) => { - if builder.independent_segments_tag.is_some() { - return Err(Error::invalid_input()); - } - builder.tag(t); + builder.independent_segments_tag(t); } Tag::ExtXStart(t) => { - if builder.start_tag.is_some() { - return Err(Error::invalid_input()); - } - builder.tag(t); + builder.start_tag(t); } Tag::Unknown(_) => { // [6.3.1. General Client Responsibilities] @@ -369,7 +270,14 @@ impl FromStr for MasterPlaylist { } } } - builder.finish() + + builder.media_tags(media_tags); + builder.stream_inf_tags(stream_inf_tags); + builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags); + builder.session_data_tags(session_data_tags); + builder.session_key_tags(session_key_tags); + + builder.build().map_err(Error::builder_error) } } @@ -379,20 +287,37 @@ mod tests { #[test] fn test_parser() { - let playlist = r#" - #EXTM3U - #EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" - http://example.com/low/index.m3u8 - #EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" - http://example.com/lo_mid/index.m3u8 - #EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" - http://example.com/hi_mid/index.m3u8 - #EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" - http://example.com/high/index.m3u8 - #EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" - http://example.com/audio/index.m3u8 - "# + r#"#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/low/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/lo_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/hi_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/high/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" +http://example.com/audio/index.m3u8 +"# .parse::() .unwrap(); } + + #[test] + fn test_display() { + let input = r#"#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/low/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/lo_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/hi_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/high/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" +http://example.com/audio/index.m3u8 +"#; + let playlist = input.parse::().unwrap(); + assert_eq!(playlist.to_string(), input); + } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index d81db3f..5f196db 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -36,6 +36,18 @@ impl fmt::Display for ExtXVersion { } } +impl Default for ExtXVersion { + fn default() -> Self { + Self(ProtocolVersion::V1) + } +} + +impl From for ExtXVersion { + fn from(value: ProtocolVersion) -> Self { + Self(value) + } +} + impl FromStr for ExtXVersion { type Err = Error; diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index a172da9..b6aed5f 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -115,12 +115,12 @@ impl fmt::Display for ExtXStreamInf { if let Some(ref x) = self.average_bandwidth { write!(f, ",AVERAGE-BANDWIDTH={}", x)?; } - if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", quote(x))?; - } if let Some(ref x) = self.resolution { write!(f, ",RESOLUTION={}", x)?; } + if let Some(ref x) = self.codecs { + write!(f, ",CODECS={}", quote(x))?; + } if let Some(ref x) = self.frame_rate { write!(f, ",FRAME-RATE={:.3}", x.as_f64())?; } From dd1a40abc98a301461c1ec3da0d8ad3ca8aea072 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 21:08:35 +0200 Subject: [PATCH 11/25] added media_playlist builder --- src/error.rs | 64 +-- src/lib.rs | 2 +- src/master_playlist.rs | 161 ++++-- src/media_playlist.rs | 590 ++++++++++----------- src/tags/master_playlist/stream_inf.rs | 32 +- src/tags/media_playlist/target_duration.rs | 2 +- 6 files changed, 462 insertions(+), 389 deletions(-) diff --git a/src/error.rs b/src/error.rs index 92e77dd..2a340d2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,56 +6,63 @@ use failure::{Backtrace, Context, Fail}; /// This crate specific `Result` type. pub type Result = std::result::Result; -#[derive(Debug, Fail, Clone)] -pub enum AttributeError { - #[fail(display = "The attribute has an invalid name; {:?}", _0)] - InvalidAttribute(String), - #[fail(display = "A value is missing for the attribute: {}", _0)] - MissingValue(String), -} - +/// The ErrorKind. #[derive(Debug, Fail, Clone)] pub enum ErrorKind { - #[fail(display = "AttributeError: {}", _0)] - AttributeError(AttributeError), - #[fail(display = "UnknownError: {}", _0)] + /// An unknown error occured. UnknownError(String), #[fail(display = "A value is missing for the attribute {}", _0)] + /// A required value is missing. MissingValue(String), #[fail(display = "Invalid Input")] + /// Error for anything. InvalidInput, #[fail(display = "ParseIntError: {}", _0)] + /// Failed to parse a String to int. ParseIntError(String), #[fail(display = "ParseFloatError: {}", _0)] + /// Failed to parse a String to float. ParseFloatError(String), #[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)] - MissingTag { tag: String, input: String }, + /// A tag is missing, that is required at the start of the input. + MissingTag { + /// The required tag. + tag: String, + /// The unparsed input data. + input: String, + }, #[fail(display = "CustomError: {}", _0)] + /// A custom error. Custom(String), #[fail(display = "Unmatched Group: {:?}", _0)] + /// Unmatched Group UnmatchedGroup(String), #[fail(display = "Unknown Protocol version: {:?}", _0)] + /// Unknown m3u8 version. This library supports up to ProtocolVersion 7. UnknownProtocolVersion(String), #[fail(display = "IoError: {}", _0)] + /// Some io error Io(String), #[fail( display = "VersionError: required_version: {:?}, specified_version: {:?}", _0, _1 )] + /// This error occurs, if there is a ProtocolVersion mismatch. VersionError(String, String), #[fail(display = "BuilderError: {}", _0)] + /// An Error from a Builder. BuilderError(String), /// Hints that destructuring should not be exhaustive. @@ -69,6 +76,7 @@ pub enum ErrorKind { } #[derive(Debug)] +/// The Error type of this library. pub struct Error { inner: Context, } @@ -101,33 +109,7 @@ impl From> for Error { } } -macro_rules! from_error { - ( $( $f:tt ),* ) => { - $( - impl From<$f> for ErrorKind { - fn from(value: $f) -> Self { - Self::$f(value) - } - } - )* - } -} - -from_error!(AttributeError); - impl Error { - pub(crate) fn invalid_attribute(value: T) -> Self { - Self::from(ErrorKind::from(AttributeError::InvalidAttribute( - value.to_string(), - ))) - } - - pub(crate) fn missing_attribute_value(value: T) -> Self { - Self::from(ErrorKind::from(AttributeError::MissingValue( - value.to_string(), - ))) - } - pub(crate) fn unknown(value: T) -> Self where T: error::Error, @@ -197,19 +179,19 @@ impl Error { } } -impl From for Error { +impl From<::std::num::ParseIntError> for Error { fn from(value: ::std::num::ParseIntError) -> Self { Error::parse_int_error(value) } } -impl From for Error { +impl From<::std::num::ParseFloatError> for Error { fn from(value: ::std::num::ParseFloatError) -> Self { Error::parse_float_error(value) } } -impl From for Error { +impl From<::std::io::Error> for Error { fn from(value: ::std::io::Error) -> Self { Error::io(value) } diff --git a/src/lib.rs b/src/lib.rs index 135c9ef..75e8bd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,7 @@ pub use error::{Error, ErrorKind}; pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder}; -pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder, MediaPlaylistOptions}; +pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder}; pub use media_segment::{MediaSegment, MediaSegmentBuilder}; pub mod tags; diff --git a/src/master_playlist.rs b/src/master_playlist.rs index ed8e880..0b6cd40 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; use std::fmt; +use std::iter; use std::str::FromStr; use derive_builder::Builder; @@ -13,17 +14,34 @@ use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; use crate::Error; /// Master playlist. -#[derive(Debug, Clone, Builder, Default)] +#[derive(Debug, Clone, Builder)] #[builder(build_fn(validate = "Self::validate"))] -#[builder(setter(into, strip_option), default)] +#[builder(setter(into, strip_option))] pub struct MasterPlaylist { + #[builder(default, setter(name = "version"))] + /// Sets the protocol compatibility version of the resulting playlist. + /// + /// If the resulting playlist has tags which requires a compatibility version greater than + /// `version`, + /// `build()` method will fail with an `ErrorKind::InvalidInput` error. + /// + /// The default is the maximum version among the tags in the playlist. version_tag: ExtXVersion, + #[builder(default)] + /// Sets the [ExtXIndependentSegments] tag. independent_segments_tag: Option, + #[builder(default)] + /// Sets the [ExtXStart] tag. start_tag: Option, + /// Sets the [ExtXMedia] tag. media_tags: Vec, + /// Sets all [ExtXStreamInf]s. stream_inf_tags: Vec, + /// Sets all [ExtXIFrameStreamInf]s. i_frame_stream_inf_tags: Vec, + /// Sets all [ExtXSessionData]s. session_data_tags: Vec, + /// Sets all [ExtXSessionKey]s. session_key_tags: Vec, } @@ -75,30 +93,100 @@ impl MasterPlaylist { } impl MasterPlaylistBuilder { - pub(crate) fn validate(&self) -> Result<(), String> { - // validate stream inf tags - if let Some(stream_inf_tags) = &self.stream_inf_tags { + fn validate(&self) -> Result<(), String> { + let required_version = self.required_version(); + let specified_version = self + .version_tag + .unwrap_or(required_version.into()) + .version(); + + if required_version > specified_version { + return Err(Error::required_version(required_version, specified_version).to_string()); + } + + self.validate_stream_inf_tags().map_err(|e| e.to_string())?; + self.validate_i_frame_stream_inf_tags() + .map_err(|e| e.to_string())?; + self.validate_session_data_tags() + .map_err(|e| e.to_string())?; + self.validate_session_key_tags() + .map_err(|e| e.to_string())?; + + Ok(()) + } + + fn required_version(&self) -> ProtocolVersion { + iter::empty() + .chain( + self.independent_segments_tag + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.start_tag + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.media_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.stream_inf_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.i_frame_stream_inf_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.session_data_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.session_key_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .max() + .unwrap_or(ProtocolVersion::V7) + } + + fn validate_stream_inf_tags(&self) -> crate::Result<()> { + if let Some(value) = &self.stream_inf_tags { let mut has_none_closed_captions = false; - for t in stream_inf_tags { + + for t in value { if let Some(group_id) = t.audio() { if !self.check_media_group(MediaType::Audio, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } if let Some(group_id) = t.video() { if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } if let Some(group_id) = t.subtitles() { if !self.check_media_group(MediaType::Subtitles, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } match t.closed_captions() { Some(&ClosedCaptions::GroupId(ref group_id)) => { if !self.check_media_group(MediaType::ClosedCaptions, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } Some(&ClosedCaptions::None) => { @@ -108,53 +196,57 @@ impl MasterPlaylistBuilder { } } if has_none_closed_captions { - if !stream_inf_tags + if !value .iter() .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) { - return Err(Error::invalid_input().to_string()); + return Err(Error::invalid_input()); } } } + Ok(()) + } - // validate i_frame_stream_inf_tags - if let Some(i_frame_stream_inf_tags) = &self.i_frame_stream_inf_tags { - for t in i_frame_stream_inf_tags { + fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> { + if let Some(value) = &self.i_frame_stream_inf_tags { + for t in value { if let Some(group_id) = t.video() { if !self.check_media_group(MediaType::Video, group_id) { - return Err(Error::unmatched_group(group_id).to_string()); + return Err(Error::unmatched_group(group_id)); } } } } + Ok(()) + } - // validate session_data_tags - if let Some(session_data_tags) = &self.session_data_tags { - let mut set = HashSet::new(); - - for t in session_data_tags { + fn validate_session_data_tags(&self) -> crate::Result<()> { + let mut set = HashSet::new(); + if let Some(value) = &self.session_data_tags { + for t in value { if !set.insert((t.data_id(), t.language())) { - return Err(Error::custom(format!("Conflict: {}", t)).to_string()); + return Err(Error::custom(format!("Conflict: {}", t))); } } } + Ok(()) + } - // validate session_key_tags - if let Some(session_key_tags) = &self.session_key_tags { - let mut set = HashSet::new(); - for t in session_key_tags { + fn validate_session_key_tags(&self) -> crate::Result<()> { + let mut set = HashSet::new(); + if let Some(value) = &self.session_key_tags { + for t in value { if !set.insert(t.key()) { - return Err(Error::custom(format!("Conflict: {}", t)).to_string()); + return Err(Error::custom(format!("Conflict: {}", t))); } } } - Ok(()) } fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { - if let Some(media_tags) = &self.media_tags { - media_tags + if let Some(value) = &self.media_tags { + value .iter() .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) } else { @@ -220,7 +312,7 @@ impl FromStr for MasterPlaylist { return Err(Error::invalid_input()); } Tag::ExtXVersion(t) => { - builder.version_tag(t.version()); + builder.version(t.version()); } Tag::ExtInf(_) | Tag::ExtXByteRange(_) @@ -235,7 +327,10 @@ impl FromStr for MasterPlaylist { | Tag::ExtXEndList(_) | Tag::ExtXPlaylistType(_) | Tag::ExtXIFramesOnly(_) => { - return Err(Error::invalid_input()); // TODO: why? + return Err(Error::custom(format!( + "This tag isn't allowed in a master playlist: {}", + tag + ))); } Tag::ExtXMedia(t) => { media_tags.push(t); @@ -258,7 +353,7 @@ impl FromStr for MasterPlaylist { Tag::ExtXStart(t) => { builder.start_tag(t); } - Tag::Unknown(_) => { + _ => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. // TODO: collect custom tags diff --git a/src/media_playlist.rs b/src/media_playlist.rs index a4074b6..6a860fe 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -3,154 +3,135 @@ use std::iter; use std::str::FromStr; use std::time::Duration; +use derive_builder::Builder; + use crate::line::{Line, Lines, Tag}; use crate::media_segment::{MediaSegment, MediaSegmentBuilder}; use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, - MediaPlaylistTag, }; use crate::types::ProtocolVersion; use crate::Error; -/// Media playlist builder. -#[derive(Debug, Clone)] -pub struct MediaPlaylistBuilder { - version: Option, - target_duration_tag: Option, +/// Media playlist. +#[derive(Debug, Clone, Builder)] +#[builder(build_fn(validate = "Self::validate"))] +#[builder(setter(into, strip_option))] +pub struct MediaPlaylist { + /// Sets the protocol compatibility version of the resulting playlist. + /// + /// If the resulting playlist has tags which requires a compatibility + /// version greater than `version`, + /// `build()` method will fail with an `ErrorKind::InvalidInput` error. + /// + /// The default is the maximum version among the tags in the playlist. + #[builder(setter(name = "version"))] + version_tag: ExtXVersion, + /// Sets the [ExtXTargetDuration] tag. + target_duration_tag: ExtXTargetDuration, + #[builder(default)] + /// Sets the [ExtXMediaSequence] tag. media_sequence_tag: Option, + #[builder(default)] + /// Sets the [ExtXDiscontinuitySequence] tag. discontinuity_sequence_tag: Option, + #[builder(default)] + /// Sets the [ExtXPlaylistType] tag. playlist_type_tag: Option, + #[builder(default)] + /// Sets the [ExtXIFramesOnly] tag. i_frames_only_tag: Option, + #[builder(default)] + /// Sets the [ExtXIndependentSegments] tag. independent_segments_tag: Option, + #[builder(default)] + /// Sets the [ExtXStart] tag. start_tag: Option, + #[builder(default)] + /// Sets the [ExtXEndList] tag. end_list_tag: Option, + /// Sets all [MediaSegment]s. segments: Vec, - options: MediaPlaylistOptions, + /// Sets the allowable excess duration of each media segment in the associated playlist. + /// + /// # Error + /// If there is a media segment of which duration exceeds + /// `#EXT-X-TARGETDURATION + allowable_excess_duration`, + /// the invocation of `MediaPlaylistBuilder::build()` method will fail. + /// + /// The default value is `Duration::from_secs(0)`. + #[builder(default = "Duration::from_secs(0)")] + allowable_excess_duration: Duration, } impl MediaPlaylistBuilder { - /// Makes a new `MediaPlaylistBuilder` instance. - pub fn new() -> Self { - MediaPlaylistBuilder { - version: None, - target_duration_tag: None, - media_sequence_tag: None, - discontinuity_sequence_tag: None, - playlist_type_tag: None, - i_frames_only_tag: None, - independent_segments_tag: None, - start_tag: None, - end_list_tag: None, - segments: Vec::new(), - options: MediaPlaylistOptions::new(), - } - } - - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility version greater than `version`, - /// `finish()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. - pub fn version(&mut self, version: ProtocolVersion) -> &mut Self { - self.version = Some(version); - self - } - - /// Sets the given tag to the resulting playlist. - pub fn tag>(&mut self, tag: T) -> &mut Self { - match tag.into() { - MediaPlaylistTag::ExtXTargetDuration(t) => self.target_duration_tag = Some(t), - MediaPlaylistTag::ExtXMediaSequence(t) => self.media_sequence_tag = Some(t), - MediaPlaylistTag::ExtXDiscontinuitySequence(t) => { - self.discontinuity_sequence_tag = Some(t) - } - MediaPlaylistTag::ExtXPlaylistType(t) => self.playlist_type_tag = Some(t), - MediaPlaylistTag::ExtXIFramesOnly(t) => self.i_frames_only_tag = Some(t), - MediaPlaylistTag::ExtXIndependentSegments(t) => self.independent_segments_tag = Some(t), - MediaPlaylistTag::ExtXStart(t) => self.start_tag = Some(t), - MediaPlaylistTag::ExtXEndList(t) => self.end_list_tag = Some(t), - } - self - } - - /// Adds a media segment to the resulting playlist. - pub fn segment(&mut self, segment: MediaSegment) -> &mut Self { - self.segments.push(segment); - self - } - - /// Sets the options that will be associated to the resulting playlist. - /// - /// The default value is `MediaPlaylistOptions::default()`. - pub fn options(&mut self, options: MediaPlaylistOptions) -> &mut Self { - self.options = options; - self - } - - /// Builds a `MediaPlaylist` instance. - pub fn finish(self) -> crate::Result { + fn validate(&self) -> Result<(), String> { let required_version = self.required_version(); - let specified_version = self.version.unwrap_or(required_version); - if !(required_version <= specified_version) { + let specified_version = self + .version_tag + .unwrap_or(required_version.into()) + .version(); + + if required_version > specified_version { return Err(Error::custom(format!( - "required_version:{}, specified_version:{}", + "required_version: {}, specified_version: {}", required_version, specified_version - ))); + )) + .to_string()); } - let target_duration_tag = self.target_duration_tag.ok_or(Error::invalid_input())?; - self.validate_media_segments(target_duration_tag.duration())?; + if let Some(target_duration) = &self.target_duration_tag { + self.validate_media_segments(target_duration.duration()) + .map_err(|e| e.to_string())?; + } - Ok(MediaPlaylist { - version_tag: ExtXVersion::new(specified_version), - target_duration_tag, - media_sequence_tag: self.media_sequence_tag, - discontinuity_sequence_tag: self.discontinuity_sequence_tag, - playlist_type_tag: self.playlist_type_tag, - i_frames_only_tag: self.i_frames_only_tag, - independent_segments_tag: self.independent_segments_tag, - start_tag: self.start_tag, - end_list_tag: self.end_list_tag, - segments: self.segments, - }) + Ok(()) } fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> { let mut last_range_uri = None; - for s in &self.segments { - // CHECK: `#EXT-X-TARGETDURATION` - let segment_duration = s.inf_tag().duration(); - let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { - Duration::from_secs(segment_duration.as_secs()) - } else { - Duration::from_secs(segment_duration.as_secs() + 1) - }; - let max_segment_duration = target_duration + self.options.allowable_excess_duration; + if let Some(segments) = &self.segments { + for s in segments { + // CHECK: `#EXT-X-TARGETDURATION` + let segment_duration = s.inf_tag().duration(); + let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { + Duration::from_secs(segment_duration.as_secs()) + } else { + Duration::from_secs(segment_duration.as_secs() + 1) + }; - if !(rounded_segment_duration <= max_segment_duration) { - return Err(Error::custom(format!( - "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", - segment_duration, - max_segment_duration, - target_duration, - s.uri() - ))); - } + let max_segment_duration = { + if let Some(value) = &self.allowable_excess_duration { + target_duration + *value + } else { + target_duration + } + }; - // CHECK: `#EXT-X-BYTE-RANGE` - if let Some(tag) = s.byte_range_tag() { - if tag.to_range().start().is_none() { - let last_uri = last_range_uri.ok_or(Error::invalid_input())?; - if last_uri != s.uri() { - return Err(Error::invalid_input()); + if !(rounded_segment_duration <= max_segment_duration) { + return Err(Error::custom(format!( + "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", + segment_duration, + max_segment_duration, + target_duration, + s.uri() + ))); + } + + // CHECK: `#EXT-X-BYTE-RANGE` + if let Some(tag) = s.byte_range_tag() { + if tag.to_range().start().is_none() { + let last_uri = last_range_uri.ok_or(Error::invalid_input())?; + if last_uri != s.uri() { + return Err(Error::invalid_input()); + } + } else { + last_range_uri = Some(s.uri()); } } else { - last_range_uri = Some(s.uri()); + last_range_uri = None; } - } else { - last_range_uri = None; } } Ok(()) @@ -163,49 +144,86 @@ impl MediaPlaylistBuilder { .iter() .map(|t| t.requires_version()), ) - .chain(self.media_sequence_tag.iter().map(|t| t.requires_version())) - .chain( - self.discontinuity_sequence_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.playlist_type_tag.iter().map(|t| t.requires_version())) - .chain(self.i_frames_only_tag.iter().map(|t| t.requires_version())) - .chain( - self.independent_segments_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.start_tag.iter().map(|t| t.requires_version())) - .chain(self.end_list_tag.iter().map(|t| t.requires_version())) - .chain(self.segments.iter().map(|s| s.requires_version())) + .chain(self.media_sequence_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.discontinuity_sequence_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.playlist_type_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.i_frames_only_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.independent_segments_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.start_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.end_list_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.segments.iter().map(|t| { + t.iter() + .map(|s| s.requires_version()) + .max() + .unwrap_or(ProtocolVersion::V1) + })) .max() .unwrap_or(ProtocolVersion::V1) } -} -impl Default for MediaPlaylistBuilder { - fn default() -> Self { - Self::new() + /// Adds a media segment to the resulting playlist. + pub fn push_segment>(&mut self, value: VALUE) -> &mut Self { + if let Some(segments) = &mut self.segments { + segments.push(value.into()); + } else { + self.segments = Some(vec![value.into()]); + } + self + } + + /// Parse the rest of the [MediaPlaylist] from an m3u8 file. + pub fn parse(&mut self, input: &str) -> crate::Result { + parse_media_playlist(input, self) } } -/// Media playlist. -#[derive(Debug, Clone)] -pub struct MediaPlaylist { - version_tag: ExtXVersion, - target_duration_tag: ExtXTargetDuration, - media_sequence_tag: Option, - discontinuity_sequence_tag: Option, - playlist_type_tag: Option, - i_frames_only_tag: Option, - independent_segments_tag: Option, - start_tag: Option, - end_list_tag: Option, - segments: Vec, -} - impl MediaPlaylist { + /// Creates a [MediaPlaylistBuilder]. + pub fn builder() -> MediaPlaylistBuilder { + MediaPlaylistBuilder::default() + } /// Returns the `EXT-X-VERSION` tag contained in the playlist. pub const fn version_tag(&self) -> ExtXVersion { self.version_tag @@ -292,161 +310,123 @@ impl fmt::Display for MediaPlaylist { } } +fn parse_media_playlist( + input: &str, + builder: &mut MediaPlaylistBuilder, +) -> crate::Result { + let mut segment = MediaSegmentBuilder::new(); + let mut segments = vec![]; + + let mut has_partial_segment = false; + let mut has_discontinuity_tag = false; + + for (i, line) in input.parse::()?.into_iter().enumerate() { + match line { + Line::Tag(tag) => { + if i == 0 { + if tag != Tag::ExtM3u(ExtM3u) { + return Err(Error::custom("m3u8 doesn't start with #EXTM3U")); + } + continue; + } + match tag { + Tag::ExtM3u(_) => return Err(Error::invalid_input()), + Tag::ExtXVersion(t) => { + builder.version(t.version()); + } + Tag::ExtInf(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXByteRange(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXDiscontinuity(t) => { + has_discontinuity_tag = true; + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXKey(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXMap(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXProgramDateTime(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXDateRange(t) => { + has_partial_segment = true; + segment.tag(t); + } + Tag::ExtXTargetDuration(t) => { + builder.target_duration_tag(t); + } + Tag::ExtXMediaSequence(t) => { + builder.media_sequence_tag(t); + } + Tag::ExtXDiscontinuitySequence(t) => { + if segments.is_empty() { + return Err(Error::invalid_input()); + } + if has_discontinuity_tag { + return Err(Error::invalid_input()); + } + builder.discontinuity_sequence_tag(t); + } + Tag::ExtXEndList(t) => { + builder.end_list_tag(t); + } + Tag::ExtXPlaylistType(t) => { + builder.playlist_type_tag(t); + } + Tag::ExtXIFramesOnly(t) => { + builder.i_frames_only_tag(t); + } + Tag::ExtXMedia(_) + | Tag::ExtXStreamInf(_) + | Tag::ExtXIFrameStreamInf(_) + | Tag::ExtXSessionData(_) + | Tag::ExtXSessionKey(_) => { + return Err(Error::custom(tag)); + } + Tag::ExtXIndependentSegments(t) => { + builder.independent_segments_tag(t); + } + Tag::ExtXStart(t) => { + builder.start_tag(t); + } + Tag::Unknown(_) => { + // [6.3.1. General Client Responsibilities] + // > ignore any unrecognized tags. + } + } + } + Line::Uri(uri) => { + segment.uri(uri); + segments.push(segment.finish()?); + segment = MediaSegmentBuilder::new(); + has_partial_segment = false; + } + } + } + if has_partial_segment { + return Err(Error::invalid_input()); + } + + builder.segments(segments); + builder.build().map_err(Error::builder_error) +} + impl FromStr for MediaPlaylist { type Err = Error; fn from_str(input: &str) -> Result { - MediaPlaylistOptions::new().parse(input) - } -} - -/// Media playlist options. -#[derive(Debug, Clone)] -pub struct MediaPlaylistOptions { - allowable_excess_duration: Duration, -} - -impl MediaPlaylistOptions { - /// Makes a new `MediaPlaylistOptions` with the default settings. - pub const fn new() -> Self { - MediaPlaylistOptions { - allowable_excess_duration: Duration::from_secs(0), - } - } - - /// Sets the allowable excess duration of each media segment in the associated playlist. - /// - /// If there is a media segment of which duration exceeds - /// `#EXT-X-TARGETDURATION + allowable_excess_duration`, - /// the invocation of `MediaPlaylistBuilder::finish()` method will fail. - /// - /// The default value is `Duration::from_secs(0)`. - pub fn allowable_excess_segment_duration( - &mut self, - allowable_excess_duration: Duration, - ) -> &mut Self { - self.allowable_excess_duration = allowable_excess_duration; - self - } - - /// Parses the given M3U8 text with the specified settings. - pub fn parse(&self, m3u8: &str) -> crate::Result { - let mut builder = MediaPlaylistBuilder::new(); - builder.options(self.clone()); - - let mut segment = MediaSegmentBuilder::new(); - let mut has_partial_segment = false; - let mut has_discontinuity_tag = false; - for (i, line) in m3u8.parse::()?.into_iter().enumerate() { - match line { - Line::Tag(tag) => { - if i == 0 { - if tag != Tag::ExtM3u(ExtM3u) { - return Err(Error::invalid_input()); - } - continue; - } - match tag { - Tag::ExtM3u(_) => return Err(Error::invalid_input()), - Tag::ExtXVersion(t) => { - if builder.version.is_some() { - return Err(Error::invalid_input()); - } - builder.version(t.version()); - } - Tag::ExtInf(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXByteRange(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXDiscontinuity(t) => { - has_discontinuity_tag = true; - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXKey(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXMap(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXProgramDateTime(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXDateRange(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXTargetDuration(t) => { - builder.tag(t); - } - Tag::ExtXMediaSequence(t) => { - if builder.segments.is_empty() { - return Err(Error::invalid_input()); - } - builder.tag(t); - } - Tag::ExtXDiscontinuitySequence(t) => { - if builder.segments.is_empty() { - return Err(Error::invalid_input()); - } - if has_discontinuity_tag { - return Err(Error::invalid_input()); - } - builder.tag(t); - } - Tag::ExtXEndList(t) => { - builder.tag(t); - } - Tag::ExtXPlaylistType(t) => { - builder.tag(t); - } - Tag::ExtXIFramesOnly(t) => { - builder.tag(t); - } - Tag::ExtXMedia(_) - | Tag::ExtXStreamInf(_) - | Tag::ExtXIFrameStreamInf(_) - | Tag::ExtXSessionData(_) - | Tag::ExtXSessionKey(_) => { - return Err(Error::custom(tag)); - } - Tag::ExtXIndependentSegments(t) => { - builder.tag(t); - } - Tag::ExtXStart(t) => { - builder.tag(t); - } - Tag::Unknown(_) => { - // [6.3.1. General Client Responsibilities] - // > ignore any unrecognized tags. - } - } - } - Line::Uri(uri) => { - segment.uri(uri); - builder.segment((segment.finish())?); - segment = MediaSegmentBuilder::new(); - has_partial_segment = false; - } - } - } - if has_partial_segment { - return Err(Error::invalid_input()); - } - builder.finish() - } -} - -impl Default for MediaPlaylistOptions { - fn default() -> Self { - Self::new() + parse_media_playlist(input, &mut Self::builder()) } } @@ -471,20 +451,20 @@ mod tests { assert!(m3u8.parse::().is_err()); // Error (allowable segment duration = 9) - assert!(MediaPlaylistOptions::new() - .allowable_excess_segment_duration(Duration::from_secs(1)) + assert!(MediaPlaylist::builder() + .allowable_excess_duration(Duration::from_secs(1)) .parse(m3u8) .is_err()); // Ok (allowable segment duration = 10) - assert!(MediaPlaylistOptions::new() - .allowable_excess_segment_duration(Duration::from_secs(2)) + MediaPlaylist::builder() + .allowable_excess_duration(Duration::from_secs(2)) .parse(m3u8) - .is_ok()); + .unwrap(); } #[test] - fn empty_m3u8_parse_test() { + fn test_parser() { let m3u8 = ""; assert!(m3u8.parse::().is_err()); } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index b6aed5f..671179d 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -31,9 +31,9 @@ impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; /// Makes a new `ExtXStreamInf` tag. - pub const fn new(uri: SingleLineString, bandwidth: u64) -> Self { + pub fn new(uri: T, bandwidth: u64) -> Self { ExtXStreamInf { - uri, + uri: SingleLineString::new(uri.to_string()).unwrap(), bandwidth, average_bandwidth: None, codecs: None, @@ -209,11 +209,27 @@ mod test { use super::*; #[test] - fn ext_x_stream_inf() { - let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 1000); - let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo"; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_parser() { + let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo" + .parse::() + .unwrap(); + + assert_eq!(stream_inf, ExtXStreamInf::new("foo", 1000)); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ProtocolVersion::V1, + ExtXStreamInf::new("foo", 1000).requires_version() + ); + } + + #[test] + fn test_display() { + assert_eq!( + ExtXStreamInf::new("foo", 1000).to_string(), + "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo".to_string() + ); } } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 32ca2a9..71b0c66 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -9,7 +9,7 @@ use crate::Error; /// [4.3.3.1. EXT-X-TARGETDURATION] /// /// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct ExtXTargetDuration { duration: Duration, } From 51b66d2adf08f562ce1798618f5715d78335d312 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 21:21:44 +0200 Subject: [PATCH 12/25] added media_segment builder --- src/media_playlist.rs | 22 ++++----- src/media_segment.rs | 107 +++++++++++++----------------------------- 2 files changed, 44 insertions(+), 85 deletions(-) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 6a860fe..238aaf3 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -6,7 +6,7 @@ use std::time::Duration; use derive_builder::Builder; use crate::line::{Line, Lines, Tag}; -use crate::media_segment::{MediaSegment, MediaSegmentBuilder}; +use crate::media_segment::MediaSegment; use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, @@ -314,7 +314,7 @@ fn parse_media_playlist( input: &str, builder: &mut MediaPlaylistBuilder, ) -> crate::Result { - let mut segment = MediaSegmentBuilder::new(); + let mut segment = MediaSegment::builder(); let mut segments = vec![]; let mut has_partial_segment = false; @@ -336,32 +336,32 @@ fn parse_media_playlist( } Tag::ExtInf(t) => { has_partial_segment = true; - segment.tag(t); + segment.inf_tag(t); } Tag::ExtXByteRange(t) => { has_partial_segment = true; - segment.tag(t); + segment.byte_range_tag(t); } Tag::ExtXDiscontinuity(t) => { has_discontinuity_tag = true; has_partial_segment = true; - segment.tag(t); + segment.discontinuity_tag(t); } Tag::ExtXKey(t) => { has_partial_segment = true; - segment.tag(t); + segment.push_key_tag(t); } Tag::ExtXMap(t) => { has_partial_segment = true; - segment.tag(t); + segment.map_tag(t); } Tag::ExtXProgramDateTime(t) => { has_partial_segment = true; - segment.tag(t); + segment.program_date_time_tag(t); } Tag::ExtXDateRange(t) => { has_partial_segment = true; - segment.tag(t); + segment.date_range_tag(t); } Tag::ExtXTargetDuration(t) => { builder.target_duration_tag(t); @@ -408,8 +408,8 @@ fn parse_media_playlist( } Line::Uri(uri) => { segment.uri(uri); - segments.push(segment.finish()?); - segment = MediaSegmentBuilder::new(); + segments.push(segment.build().map_err(Error::builder_error)?); + segment = MediaSegment::builder(); has_partial_segment = false; } } diff --git a/src/media_segment.rs b/src/media_segment.rs index 3f45f5b..45520b5 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -1,96 +1,51 @@ use std::fmt; use std::iter; +use derive_builder::Builder; + use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, - MediaSegmentTag, }; use crate::types::{ProtocolVersion, SingleLineString}; -use crate::Error; -/// Media segment builder. -#[derive(Debug, Clone)] -pub struct MediaSegmentBuilder { +/// Media segment. +#[derive(Debug, Clone, Builder)] +#[builder(setter(into, strip_option))] +pub struct MediaSegment { + #[builder(default)] + /// Sets all [ExtXKey] tags. key_tags: Vec, + #[builder(default)] + /// Sets an [ExtXMap] tag. map_tag: Option, + #[builder(default)] + /// Sets an [ExtXByteRange] tag. byte_range_tag: Option, + #[builder(default)] + /// Sets an [ExtXDateRange] tag. date_range_tag: Option, + #[builder(default)] + /// Sets an [ExtXDiscontinuity] tag. discontinuity_tag: Option, + #[builder(default)] + /// Sets an [ExtXProgramDateTime] tag. program_date_time_tag: Option, - inf_tag: Option, - uri: Option, + /// Sets an [ExtInf] tag. + inf_tag: ExtInf, + /// Sets an Uri. + uri: SingleLineString, } impl MediaSegmentBuilder { - /// Makes a new `MediaSegmentBuilder` instance. - pub fn new() -> Self { - MediaSegmentBuilder { - key_tags: Vec::new(), - map_tag: None, - byte_range_tag: None, - date_range_tag: None, - discontinuity_tag: None, - program_date_time_tag: None, - inf_tag: None, - uri: None, - } - } - - /// Sets the URI of the resulting media segment. - pub fn uri(&mut self, uri: SingleLineString) -> &mut Self { - self.uri = Some(uri); - self - } - - /// Sets the given tag to the resulting media segment. - pub fn tag>(&mut self, tag: T) -> &mut Self { - match tag.into() { - MediaSegmentTag::ExtInf(t) => self.inf_tag = Some(t), - MediaSegmentTag::ExtXByteRange(t) => self.byte_range_tag = Some(t), - MediaSegmentTag::ExtXDateRange(t) => self.date_range_tag = Some(t), - MediaSegmentTag::ExtXDiscontinuity(t) => self.discontinuity_tag = Some(t), - MediaSegmentTag::ExtXKey(t) => self.key_tags.push(t), - MediaSegmentTag::ExtXMap(t) => self.map_tag = Some(t), - MediaSegmentTag::ExtXProgramDateTime(t) => self.program_date_time_tag = Some(t), + /// Pushes an [ExtXKey] tag. + pub fn push_key_tag>(&mut self, value: VALUE) -> &mut Self { + if let Some(key_tags) = &mut self.key_tags { + key_tags.push(value.into()); + } else { + self.key_tags = Some(vec![value.into()]); } self } - - /// Builds a `MediaSegment` instance. - pub fn finish(self) -> crate::Result { - let uri = self.uri.ok_or(Error::missing_value("self.uri"))?; - let inf_tag = self.inf_tag.ok_or(Error::missing_value("self.inf_tag"))?; - - Ok(MediaSegment { - key_tags: self.key_tags, - map_tag: self.map_tag, - byte_range_tag: self.byte_range_tag, - date_range_tag: self.date_range_tag, - discontinuity_tag: self.discontinuity_tag, - program_date_time_tag: self.program_date_time_tag, - inf_tag, - uri, - }) - } -} - -impl Default for MediaSegmentBuilder { - fn default() -> Self { - Self::new() - } -} - -/// Media segment. -#[derive(Debug, Clone)] -pub struct MediaSegment { - key_tags: Vec, - map_tag: Option, - byte_range_tag: Option, - date_range_tag: Option, - discontinuity_tag: Option, - program_date_time_tag: Option, - inf_tag: ExtInf, - uri: SingleLineString, } impl fmt::Display for MediaSegment { @@ -120,6 +75,10 @@ impl fmt::Display for MediaSegment { } impl MediaSegment { + /// Creates a [MediaSegmentBuilder]. + pub fn builder() -> MediaSegmentBuilder { + MediaSegmentBuilder::default() + } /// Returns the URI of the media segment. pub const fn uri(&self) -> &SingleLineString { &self.uri @@ -175,6 +134,6 @@ impl MediaSegment { ) .chain(iter::once(self.inf_tag.requires_version())) .max() - .expect("Never fails") + .unwrap_or(ProtocolVersion::V7) } } From b954ae11340422da630fe65d1c5436733ec16041 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sat, 14 Sep 2019 21:42:06 +0200 Subject: [PATCH 13/25] remove all occurences of `ref` --- src/line.rs | 48 +++++++++---------- src/master_playlist.rs | 8 ++-- src/media_playlist.rs | 28 +++++------ src/media_segment.rs | 24 +++++----- .../master_playlist/i_frame_stream_inf.rs | 20 ++++---- src/tags/master_playlist/media.rs | 24 +++++----- src/tags/master_playlist/session_data.rs | 10 ++-- src/tags/master_playlist/stream_inf.rs | 36 +++++++------- src/tags/media_segment/date_range.rs | 20 ++++---- src/tags/media_segment/inf.rs | 4 +- src/tags/media_segment/key.rs | 4 +- src/tags/media_segment/map.rs | 4 +- src/types/closed_captions.rs | 4 +- src/types/decryption_key.rs | 12 ++--- src/types/encryption_method.rs | 2 +- src/types/hdcp_level.rs | 2 +- src/types/media_type.rs | 2 +- 17 files changed, 126 insertions(+), 126 deletions(-) diff --git a/src/line.rs b/src/line.rs index 70328dd..67a7193 100644 --- a/src/line.rs +++ b/src/line.rs @@ -124,30 +124,30 @@ pub enum Tag { impl fmt::Display for Tag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Tag::ExtM3u(ref t) => t.fmt(f), - Tag::ExtXVersion(ref t) => t.fmt(f), - Tag::ExtInf(ref t) => t.fmt(f), - Tag::ExtXByteRange(ref t) => t.fmt(f), - Tag::ExtXDiscontinuity(ref t) => t.fmt(f), - Tag::ExtXKey(ref t) => t.fmt(f), - Tag::ExtXMap(ref t) => t.fmt(f), - Tag::ExtXProgramDateTime(ref t) => t.fmt(f), - Tag::ExtXDateRange(ref t) => t.fmt(f), - Tag::ExtXTargetDuration(ref t) => t.fmt(f), - Tag::ExtXMediaSequence(ref t) => t.fmt(f), - Tag::ExtXDiscontinuitySequence(ref t) => t.fmt(f), - Tag::ExtXEndList(ref t) => t.fmt(f), - Tag::ExtXPlaylistType(ref t) => t.fmt(f), - Tag::ExtXIFramesOnly(ref t) => t.fmt(f), - Tag::ExtXMedia(ref t) => t.fmt(f), - Tag::ExtXStreamInf(ref t) => t.fmt(f), - Tag::ExtXIFrameStreamInf(ref t) => t.fmt(f), - Tag::ExtXSessionData(ref t) => t.fmt(f), - Tag::ExtXSessionKey(ref t) => t.fmt(f), - Tag::ExtXIndependentSegments(ref t) => t.fmt(f), - Tag::ExtXStart(ref t) => t.fmt(f), - Tag::Unknown(ref t) => t.fmt(f), + match &self { + Tag::ExtM3u(value) => value.fmt(f), + Tag::ExtXVersion(value) => value.fmt(f), + Tag::ExtInf(value) => value.fmt(f), + Tag::ExtXByteRange(value) => value.fmt(f), + Tag::ExtXDiscontinuity(value) => value.fmt(f), + Tag::ExtXKey(value) => value.fmt(f), + Tag::ExtXMap(value) => value.fmt(f), + Tag::ExtXProgramDateTime(value) => value.fmt(f), + Tag::ExtXDateRange(value) => value.fmt(f), + Tag::ExtXTargetDuration(value) => value.fmt(f), + Tag::ExtXMediaSequence(value) => value.fmt(f), + Tag::ExtXDiscontinuitySequence(value) => value.fmt(f), + Tag::ExtXEndList(value) => value.fmt(f), + Tag::ExtXPlaylistType(value) => value.fmt(f), + Tag::ExtXIFramesOnly(value) => value.fmt(f), + Tag::ExtXMedia(value) => value.fmt(f), + Tag::ExtXStreamInf(value) => value.fmt(f), + Tag::ExtXIFrameStreamInf(value) => value.fmt(f), + Tag::ExtXSessionData(value) => value.fmt(f), + Tag::ExtXSessionKey(value) => value.fmt(f), + Tag::ExtXIndependentSegments(value) => value.fmt(f), + Tag::ExtXStart(value) => value.fmt(f), + Tag::Unknown(value) => value.fmt(f), } } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 0b6cd40..4d1d2a5 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -276,11 +276,11 @@ impl fmt::Display for MasterPlaylist { for t in &self.session_key_tags { writeln!(f, "{}", t)?; } - if let Some(ref t) = self.independent_segments_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.independent_segments_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.start_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.start_tag { + writeln!(f, "{}", value)?; } Ok(()) } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 238aaf3..3ea878c 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -282,29 +282,29 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", self.version_tag)?; } writeln!(f, "{}", self.target_duration_tag)?; - if let Some(ref t) = self.media_sequence_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.media_sequence_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.discontinuity_sequence_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.discontinuity_sequence_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.playlist_type_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.playlist_type_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.i_frames_only_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.i_frames_only_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.independent_segments_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.independent_segments_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.start_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.start_tag { + writeln!(f, "{}", value)?; } for segment in &self.segments { write!(f, "{}", segment)?; } - if let Some(ref t) = self.end_list_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.end_list_tag { + writeln!(f, "{}", value)?; } Ok(()) } diff --git a/src/media_segment.rs b/src/media_segment.rs index 45520b5..f157ad2 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -50,23 +50,23 @@ impl MediaSegmentBuilder { impl fmt::Display for MediaSegment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for t in &self.key_tags { - writeln!(f, "{}", t)?; + for value in &self.key_tags { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.map_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.map_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.byte_range_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.byte_range_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.date_range_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.date_range_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.discontinuity_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.discontinuity_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.program_date_time_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.program_date_time_tag { + writeln!(f, "{}", value)?; } writeln!(f, "{},", self.inf_tag)?; writeln!(f, "{}", self.uri)?; diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 93e5425..50e75bd 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -61,20 +61,20 @@ impl fmt::Display for ExtXIFrameStreamInf { write!(f, "URI={}", quote(&self.uri))?; write!(f, ",BANDWIDTH={}", self.bandwidth)?; - if let Some(ref x) = self.average_bandwidth { - write!(f, ",AVERAGE-BANDWIDTH={}", x)?; + if let Some(value) = &self.average_bandwidth { + write!(f, ",AVERAGE-BANDWIDTH={}", value)?; } - if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", quote(x))?; + if let Some(value) = &self.codecs { + write!(f, ",CODECS={}", quote(value))?; } - if let Some(ref x) = self.resolution { - write!(f, ",RESOLUTION={}", x)?; + if let Some(value) = &self.resolution { + write!(f, ",RESOLUTION={}", value)?; } - if let Some(ref x) = self.hdcp_level { - write!(f, ",HDCP-LEVEL={}", x)?; + if let Some(value) = &self.hdcp_level { + write!(f, ",HDCP-LEVEL={}", value)?; } - if let Some(ref x) = self.video { - write!(f, ",VIDEO={}", quote(x))?; + if let Some(value) = &self.video { + write!(f, ",VIDEO={}", quote(value))?; } Ok(()) } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index f63f1f3..819f62b 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -291,15 +291,15 @@ impl fmt::Display for ExtXMedia { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "TYPE={}", self.media_type)?; - if let Some(ref x) = self.uri { - write!(f, ",URI={}", quote(x))?; + if let Some(value) = &self.uri { + write!(f, ",URI={}", quote(value))?; } write!(f, ",GROUP-ID={}", quote(&self.group_id))?; - if let Some(ref x) = self.language { - write!(f, ",LANGUAGE={}", quote(x))?; + if let Some(value) = &self.language { + write!(f, ",LANGUAGE={}", quote(value))?; } - if let Some(ref x) = self.assoc_language { - write!(f, ",ASSOC-LANGUAGE={}", quote(x))?; + if let Some(value) = &self.assoc_language { + write!(f, ",ASSOC-LANGUAGE={}", quote(value))?; } write!(f, ",NAME={}", quote(&self.name))?; if self.default { @@ -311,14 +311,14 @@ impl fmt::Display for ExtXMedia { if self.forced { write!(f, ",FORCED=YES")?; } - if let Some(ref x) = self.instream_id { - write!(f, ",INSTREAM-ID={}", quote(x))?; + if let Some(value) = &self.instream_id { + write!(f, ",INSTREAM-ID={}", quote(value))?; } - if let Some(ref x) = self.characteristics { - write!(f, ",CHARACTERISTICS={}", quote(x))?; + if let Some(value) = &self.characteristics { + write!(f, ",CHARACTERISTICS={}", quote(value))?; } - if let Some(ref x) = self.channels { - write!(f, ",CHANNELS={}", quote(x))?; + if let Some(value) = &self.channels { + write!(f, ",CHANNELS={}", quote(value))?; } Ok(()) } diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 12d138d..183318a 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -55,12 +55,12 @@ impl fmt::Display for ExtXSessionData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "DATA-ID={}", quote(&self.data_id))?; - match self.data { - SessionData::Value(ref x) => write!(f, ",VALUE={}", quote(x))?, - SessionData::Uri(ref x) => write!(f, ",URI={}", quote(x))?, + match &self.data { + SessionData::Value(value) => write!(f, ",VALUE={}", quote(value))?, + SessionData::Uri(value) => write!(f, ",URI={}", quote(value))?, } - if let Some(ref x) = self.language { - write!(f, ",LANGUAGE={}", quote(x))?; + if let Some(value) = &self.language { + write!(f, ",LANGUAGE={}", quote(value))?; } Ok(()) } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 671179d..296fba0 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -112,32 +112,32 @@ impl fmt::Display for ExtXStreamInf { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "BANDWIDTH={}", self.bandwidth)?; - if let Some(ref x) = self.average_bandwidth { - write!(f, ",AVERAGE-BANDWIDTH={}", x)?; + if let Some(value) = &self.average_bandwidth { + write!(f, ",AVERAGE-BANDWIDTH={}", value)?; } - if let Some(ref x) = self.resolution { - write!(f, ",RESOLUTION={}", x)?; + if let Some(value) = &self.resolution { + write!(f, ",RESOLUTION={}", value)?; } - if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", quote(x))?; + if let Some(value) = &self.codecs { + write!(f, ",CODECS={}", quote(value))?; } - if let Some(ref x) = self.frame_rate { - write!(f, ",FRAME-RATE={:.3}", x.as_f64())?; + if let Some(value) = &self.frame_rate { + write!(f, ",FRAME-RATE={:.3}", value.as_f64())?; } - if let Some(ref x) = self.hdcp_level { - write!(f, ",HDCP-LEVEL={}", x)?; + if let Some(value) = &self.hdcp_level { + write!(f, ",HDCP-LEVEL={}", value)?; } - if let Some(ref x) = self.audio { - write!(f, ",AUDIO={}", quote(x))?; + if let Some(value) = &self.audio { + write!(f, ",AUDIO={}", quote(value))?; } - if let Some(ref x) = self.video { - write!(f, ",VIDEO={}", quote(x))?; + if let Some(value) = &self.video { + write!(f, ",VIDEO={}", quote(value))?; } - if let Some(ref x) = self.subtitles { - write!(f, ",SUBTITLES={}", quote(x))?; + if let Some(value) = &self.subtitles { + write!(f, ",SUBTITLES={}", quote(value))?; } - if let Some(ref x) = self.closed_captions { - write!(f, ",CLOSED-CAPTIONS={}", x)?; + if let Some(value) = &self.closed_captions { + write!(f, ",CLOSED-CAPTIONS={}", value)?; } write!(f, "\n{}", self.uri)?; Ok(()) diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 841b513..138002c 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -42,12 +42,12 @@ impl fmt::Display for ExtXDateRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "ID={}", quote(&self.id))?; - if let Some(ref x) = self.class { - write!(f, ",CLASS={}", quote(x))?; + if let Some(value) = &self.class { + write!(f, ",CLASS={}", quote(value))?; } write!(f, ",START-DATE={}", quote(&self.start_date))?; - if let Some(ref x) = self.end_date { - write!(f, ",END-DATE={}", quote(x))?; + if let Some(value) = &self.end_date { + write!(f, ",END-DATE={}", quote(value))?; } if let Some(x) = self.duration { write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?; @@ -59,14 +59,14 @@ impl fmt::Display for ExtXDateRange { DecimalFloatingPoint::from_duration(x) )?; } - if let Some(ref x) = self.scte35_cmd { - write!(f, ",SCTE35-CMD={}", quote(x))?; + if let Some(value) = &self.scte35_cmd { + write!(f, ",SCTE35-CMD={}", quote(value))?; } - if let Some(ref x) = self.scte35_out { - write!(f, ",SCTE35-OUT={}", quote(x))?; + if let Some(value) = &self.scte35_out { + write!(f, ",SCTE35-OUT={}", quote(value))?; } - if let Some(ref x) = self.scte35_in { - write!(f, ",SCTE35-IN={}", quote(x))?; + if let Some(value) = &self.scte35_in { + write!(f, ",SCTE35-IN={}", quote(value))?; } if self.end_on_next { write!(f, ",END-ON-NEXT=YES",)?; diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index ddf305c..f90a654 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -62,8 +62,8 @@ impl fmt::Display for ExtInf { + (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0); write!(f, "{}", duration)?; - if let Some(ref title) = self.title { - write!(f, ",{}", title)?; + if let Some(value) = &self.title { + write!(f, ",{}", value)?; } Ok(()) } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index 8e59522..cb7844e 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -43,8 +43,8 @@ impl ExtXKey { impl fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - if let Some(ref key) = self.0 { - write!(f, "{}", key)?; + if let Some(value) = &self.0 { + write!(f, "{}", value)?; } else { write!(f, "METHOD=NONE")?; } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 97ca7fc..df6659c 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -54,8 +54,8 @@ impl fmt::Display for ExtXMap { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "URI={}", quote(&self.uri))?; - if let Some(ref x) = self.range { - write!(f, ",BYTERANGE={}", quote(x))?; + if let Some(value) = &self.range { + write!(f, ",BYTERANGE={}", quote(value))?; } Ok(()) } diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index 7f51d9f..a391703 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -17,8 +17,8 @@ pub enum ClosedCaptions { impl fmt::Display for ClosedCaptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ClosedCaptions::GroupId(ref x) => write!(f, "{}", quote(x)), + match &self { + ClosedCaptions::GroupId(value) => write!(f, "{}", quote(value)), ClosedCaptions::None => "NONE".fmt(f), } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 014a61f..571a588 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -37,14 +37,14 @@ impl fmt::Display for DecryptionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "METHOD={}", self.method)?; write!(f, ",URI={}", quote(&self.uri))?; - if let Some(ref x) = self.iv { - write!(f, ",IV={}", x)?; + if let Some(value) = &self.iv { + write!(f, ",IV={}", value)?; } - if let Some(ref x) = self.key_format { - write!(f, ",KEYFORMAT={}", quote(x))?; + if let Some(value) = &self.key_format { + write!(f, ",KEYFORMAT={}", quote(value))?; } - if let Some(ref x) = self.key_format_versions { - write!(f, ",KEYFORMATVERSIONS={}", quote(x))?; + if let Some(value) = &self.key_format_versions { + write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; } Ok(()) } diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 379e251..8754ba1 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -17,7 +17,7 @@ pub enum EncryptionMethod { impl fmt::Display for EncryptionMethod { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { + match &self { EncryptionMethod::Aes128 => "AES-128".fmt(f), EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), } diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index fc67f9e..741959c 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -17,7 +17,7 @@ pub enum HdcpLevel { impl fmt::Display for HdcpLevel { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { + match &self { HdcpLevel::Type0 => "TYPE-0".fmt(f), HdcpLevel::None => "NONE".fmt(f), } diff --git a/src/types/media_type.rs b/src/types/media_type.rs index f47b7d3..323dd43 100644 --- a/src/types/media_type.rs +++ b/src/types/media_type.rs @@ -19,7 +19,7 @@ pub enum MediaType { impl fmt::Display for MediaType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { + match &self { MediaType::Audio => "AUDIO".fmt(f), MediaType::Video => "VIDEO".fmt(f), MediaType::Subtitles => "SUBTITLES".fmt(f), From 3acf67df6a4de4df04d3cbc6f3f83f7a569ab6d2 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 10:40:45 +0200 Subject: [PATCH 14/25] added more tests --- src/error.rs | 2 +- src/line.rs | 1 + src/media_playlist.rs | 38 ++--- src/tags/media_playlist/playlist_type.rs | 101 +++++++++---- src/tags/media_segment/inf.rs | 173 +++++++++++++++++++---- src/types/decimal_resolution.rs | 97 +++++++++---- src/types/mod.rs | 2 - src/types/playlist_type.rs | 36 ----- src/utils.rs | 6 +- tests/playlist.rs | 47 ++++++ 10 files changed, 365 insertions(+), 138 deletions(-) create mode 100644 tests/playlist.rs diff --git a/src/error.rs b/src/error.rs index 2a340d2..951bd18 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,7 +7,7 @@ use failure::{Backtrace, Context, Fail}; pub type Result = std::result::Result; /// The ErrorKind. -#[derive(Debug, Fail, Clone)] +#[derive(Debug, Fail, Clone, PartialEq, Eq)] pub enum ErrorKind { #[fail(display = "UnknownError: {}", _0)] /// An unknown error occured. diff --git a/src/line.rs b/src/line.rs index 67a7193..a5aa690 100644 --- a/src/line.rs +++ b/src/line.rs @@ -43,6 +43,7 @@ impl FromStr for Lines { } else if line.starts_with("#") { continue; // ignore comments } else { + // stream inf line needs special treatment if stream_inf { stream_inf = false; if let Some(first_line) = stream_inf_line { diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 3ea878c..9d9778e 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -319,6 +319,7 @@ fn parse_media_playlist( let mut has_partial_segment = false; let mut has_discontinuity_tag = false; + let mut has_version = false; // m3u8 files without ExtXVersion tags are ProtocolVersion::V1 for (i, line) in input.parse::()?.into_iter().enumerate() { match line { @@ -333,6 +334,7 @@ fn parse_media_playlist( Tag::ExtM3u(_) => return Err(Error::invalid_input()), Tag::ExtXVersion(t) => { builder.version(t.version()); + has_version = true; } Tag::ExtInf(t) => { has_partial_segment = true; @@ -417,6 +419,9 @@ fn parse_media_playlist( if has_partial_segment { return Err(Error::invalid_input()); } + if !has_version { + builder.version(ProtocolVersion::V1); + } builder.segments(segments); builder.build().map_err(Error::builder_error) @@ -436,36 +441,37 @@ mod tests { #[test] fn too_large_segment_duration_test() { - let m3u8 = "#EXTM3U\n\ - #EXT-X-TARGETDURATION:8\n\ - #EXT-X-VERSION:3\n\ - #EXTINF:9.009,\n\ - http://media.example.com/first.ts\n\ - #EXTINF:9.509,\n\ - http://media.example.com/second.ts\n\ - #EXTINF:3.003,\n\ - http://media.example.com/third.ts\n\ - #EXT-X-ENDLIST"; + let playlist = r#" + #EXTM3U + #EXT-X-TARGETDURATION:8 + #EXT-X-VERSION:3 + #EXTINF:9.009, + http://media.example.com/first.ts + #EXTINF:9.509, + http://media.example.com/second.ts + #EXTINF:3.003, + http://media.example.com/third.ts + #EXT-X-ENDLIST"#; // Error (allowable segment duration = target duration = 8) - assert!(m3u8.parse::().is_err()); + assert!(playlist.parse::().is_err()); // Error (allowable segment duration = 9) assert!(MediaPlaylist::builder() .allowable_excess_duration(Duration::from_secs(1)) - .parse(m3u8) + .parse(playlist) .is_err()); // Ok (allowable segment duration = 10) MediaPlaylist::builder() .allowable_excess_duration(Duration::from_secs(2)) - .parse(m3u8) + .parse(playlist) .unwrap(); } #[test] - fn test_parser() { - let m3u8 = ""; - assert!(m3u8.parse::().is_err()); + fn test_empty_playlist() { + let playlist = ""; + assert!(playlist.parse::().is_err()); } } diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index f9911b7..221914f 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -1,40 +1,49 @@ use std::fmt; use std::str::FromStr; -use crate::types::{PlaylistType, ProtocolVersion}; +use crate::types::ProtocolVersion; use crate::utils::tag; use crate::Error; -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE] +/// [4.3.3.5. EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/rfc8216#section-4.3.3.5) /// -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 +/// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the +/// Media Playlist. It applies to the entire Media Playlist. +/// It is OPTIONAL. Its format is: +/// +/// ```text +/// #EXT-X-PLAYLIST-TYPE: +/// ``` +/// +/// # Note +/// If the EXT-X-PLAYLIST-TYPE tag is omitted from a Media Playlist, the +/// Playlist can be updated according to the rules in Section 6.2.1 with +/// no additional restrictions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXPlaylistType { - playlist_type: PlaylistType, +pub enum ExtXPlaylistType { + /// If the ExtXPlaylistType is Event, Media Segments can only be added to + /// the end of the Media Playlist. + Event, + /// If the ExtXPlaylistType is Video On Demand (Vod), + /// the Media Playlist cannot change. + Vod, } impl ExtXPlaylistType { pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; - /// Makes a new `ExtXPlaylistType` tag. - pub const fn new(playlist_type: PlaylistType) -> Self { - ExtXPlaylistType { playlist_type } - } - - /// Returns the type of the associated media playlist. - pub const fn playlist_type(self) -> PlaylistType { - self.playlist_type - } - /// Returns the protocol compatibility version that this tag requires. - pub const fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXPlaylistType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.playlist_type) + match &self { + Self::Event => write!(f, "{}EVENT", Self::PREFIX), + Self::Vod => write!(f, "{}VOD", Self::PREFIX), + } } } @@ -42,9 +51,12 @@ impl FromStr for ExtXPlaylistType { type Err = Error; fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?.parse()?; - - Ok(ExtXPlaylistType::new(input)) + let input = tag(input, Self::PREFIX)?; + match input { + "EVENT" => Ok(Self::Event), + "VOD" => Ok(Self::Vod), + _ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))), + } } } @@ -53,11 +65,48 @@ mod test { use super::*; #[test] - fn ext_x_playlist_type() { - let tag = ExtXPlaylistType::new(PlaylistType::Vod); - let text = "#EXT-X-PLAYLIST-TYPE:VOD"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_parser() { + assert_eq!( + "#EXT-X-PLAYLIST-TYPE:VOD" + .parse::() + .unwrap(), + ExtXPlaylistType::Vod, + ); + + assert_eq!( + "#EXT-X-PLAYLIST-TYPE:EVENT" + .parse::() + .unwrap(), + ExtXPlaylistType::Event, + ); + + assert!("#EXT-X-PLAYLIST-TYPE:H" + .parse::() + .is_err()); + } + + #[test] + fn test_display() { + assert_eq!( + "#EXT-X-PLAYLIST-TYPE:VOD".to_string(), + ExtXPlaylistType::Vod.to_string(), + ); + + assert_eq!( + "#EXT-X-PLAYLIST-TYPE:EVENT".to_string(), + ExtXPlaylistType::Event.to_string(), + ); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ExtXPlaylistType::Vod.requires_version(), + ProtocolVersion::V1 + ); + assert_eq!( + ExtXPlaylistType::Event.requires_version(), + ProtocolVersion::V1 + ); } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index f90a654..9d2c5b7 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -6,10 +6,46 @@ use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString}; use crate::utils::tag; use crate::Error; -/// [4.3.2.1. EXTINF] +/// [4.3.2.1. EXTINF](https://tools.ietf.org/html/rfc8216#section-4.3.2.1) /// -/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies +/// only to the next [Media Segment]. This tag is REQUIRED for each [Media Segment]. +/// +/// Its format is: +/// ```text +/// #EXTINF:,[] +/// ``` +/// The title is an optional informative title about the [Media Segment]. +/// +/// [Media Segment]: crate::media_segment::MediaSegment +/// +/// # Examples +/// Parsing from a String: +/// ``` +/// use std::time::Duration; +/// use hls_m3u8::tags::ExtInf; +/// +/// let ext_inf = "#EXTINF:8,".parse::<ExtInf>().expect("Failed to parse tag!"); +/// +/// assert_eq!(ext_inf.duration(), Duration::from_secs(8)); +/// assert_eq!(ext_inf.title(), None); +/// ``` +/// +/// Converting to a String: +/// ``` +/// use std::time::Duration; +/// use hls_m3u8::tags::ExtInf; +/// use hls_m3u8::types::SingleLineString; +/// +/// let ext_inf = ExtInf::with_title( +/// Duration::from_millis(88), +/// SingleLineString::new("title").unwrap() +/// ); +/// +/// assert_eq!(ext_inf.duration(), Duration::from_millis(88)); +/// assert_eq!(ext_inf.to_string(), "#EXTINF:0.088,title".to_string()); +/// ``` +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtInf { duration: Duration, title: Option<SingleLineString>, @@ -60,10 +96,10 @@ impl fmt::Display for ExtInf { let duration = (self.duration.as_secs() as f64) + (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0); - write!(f, "{}", duration)?; + write!(f, "{},", duration)?; if let Some(value) = &self.title { - write!(f, ",{}", value)?; + write!(f, "{}", value)?; } Ok(()) } @@ -74,44 +110,129 @@ impl FromStr for ExtInf { fn from_str(input: &str) -> Result<Self, Self::Err> { let input = tag(input, Self::PREFIX)?; - let mut tokens = input.splitn(2, ','); + dbg!(&input); + let tokens = input.splitn(2, ',').collect::<Vec<_>>(); - let seconds: DecimalFloatingPoint = tokens.next().expect("Never fails").parse()?; - let duration = seconds.to_duration(); + if tokens.len() == 0 { + return Err(Error::custom(format!( + "failed to parse #EXTINF tag, couldn't split input: {:?}", + input + ))); + } + + let duration = tokens[0].parse::<DecimalFloatingPoint>()?.to_duration(); let title = { - if let Some(title) = tokens.next() { - Some((SingleLineString::new(title))?) + if tokens.len() >= 2 { + if tokens[1].trim().is_empty() { + None + } else { + Some(SingleLineString::new(tokens[1])?) + } } else { None } }; + Ok(ExtInf { duration, title }) } } +impl From<Duration> for ExtInf { + fn from(value: Duration) -> Self { + Self::new(value) + } +} + #[cfg(test)] mod test { use super::*; #[test] - fn extinf() { - let tag = ExtInf::new(Duration::from_secs(5)); - assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), "#EXTINF:5"); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - - let tag = ExtInf::with_title( - Duration::from_secs(5), - SingleLineString::new("foo").unwrap(), + fn test_display() { + assert_eq!( + "#EXTINF:5,".to_string(), + ExtInf::new(Duration::from_secs(5)).to_string() ); - assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), "#EXTINF:5,foo"); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + assert_eq!( + "#EXTINF:5.5,".to_string(), + ExtInf::new(Duration::from_millis(5500)).to_string() + ); + assert_eq!( + "#EXTINF:5.5,title".to_string(), + ExtInf::with_title( + Duration::from_millis(5500), + SingleLineString::new("title").unwrap() + ) + .to_string() + ); + assert_eq!( + "#EXTINF:5,title".to_string(), + ExtInf::with_title( + Duration::from_secs(5), + SingleLineString::new("title").unwrap() + ) + .to_string() + ); + } - let tag = ExtInf::new(Duration::from_millis(1234)); - assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), "#EXTINF:1.234"); - assert_eq!(tag.requires_version(), ProtocolVersion::V3); + #[test] + fn test_parser() { + // #EXTINF:<duration>,[<title>] + assert_eq!( + "#EXTINF:5".parse::<ExtInf>().unwrap(), + ExtInf::new(Duration::from_secs(5)) + ); + assert_eq!( + "#EXTINF:5,".parse::<ExtInf>().unwrap(), + ExtInf::new(Duration::from_secs(5)) + ); + assert_eq!( + "#EXTINF:5.5".parse::<ExtInf>().unwrap(), + ExtInf::new(Duration::from_millis(5500)) + ); + assert_eq!( + "#EXTINF:5.5,".parse::<ExtInf>().unwrap(), + ExtInf::new(Duration::from_millis(5500)) + ); + assert_eq!( + "#EXTINF:5.5,title".parse::<ExtInf>().unwrap(), + ExtInf::with_title( + Duration::from_millis(5500), + SingleLineString::new("title").unwrap() + ) + ); + assert_eq!( + "#EXTINF:5,title".parse::<ExtInf>().unwrap(), + ExtInf::with_title( + Duration::from_secs(5), + SingleLineString::new("title").unwrap() + ) + ); + } + + #[test] + fn test_title() { + assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), None); + assert_eq!( + ExtInf::with_title( + Duration::from_secs(5), + SingleLineString::new("title").unwrap() + ) + .title(), + Some(&SingleLineString::new("title").unwrap()) + ); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ExtInf::new(Duration::from_secs(4)).requires_version(), + ProtocolVersion::V1 + ); + assert_eq!( + ExtInf::new(Duration::from_millis(4400)).requires_version(), + ProtocolVersion::V3 + ); } } diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index 67128ff..4df01a2 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -1,5 +1,5 @@ use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; use crate::Error; @@ -10,11 +10,37 @@ use crate::Error; /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DecimalResolution { + width: usize, + height: usize, +} + +impl DecimalResolution { + /// Creates a new DecimalResolution. + pub const fn new(width: usize, height: usize) -> Self { + Self { width, height } + } + /// Horizontal pixel dimension. - pub width: usize, + pub const fn width(&self) -> usize { + self.width + } + + /// Sets Horizontal pixel dimension. + pub fn set_width(&mut self, value: usize) -> &mut Self { + self.width = value; + self + } /// Vertical pixel dimension. - pub height: usize, + pub const fn height(&self) -> usize { + self.height + } + + /// Sets Vertical pixel dimension. + pub fn set_height(&mut self, value: usize) -> &mut Self { + self.height = value; + self + } } impl fmt::Display for DecimalResolution { @@ -27,13 +53,21 @@ impl FromStr for DecimalResolution { type Err = Error; fn from_str(input: &str) -> Result<Self, Self::Err> { - let mut tokens = input.splitn(2, 'x'); - let width = tokens.next().ok_or(Error::missing_value("width"))?; - let height = tokens.next().ok_or(Error::missing_value("height"))?; + let tokens = input.splitn(2, 'x').collect::<Vec<_>>(); + + if tokens.len() != 2 { + return Err(Error::custom(format!( + "InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}", + input, + ))); + } + + let width = tokens[0]; + let height = tokens[1]; Ok(DecimalResolution { - width: width.parse().map_err(|e| Error::custom(e))?, - height: height.parse().map_err(|e| Error::custom(e))?, + width: width.parse()?, + height: height.parse()?, }) } } @@ -44,37 +78,44 @@ mod tests { #[test] fn test_display() { - let decimal_resolution = DecimalResolution { - width: 1920, - height: 1080, - }; - assert_eq!(decimal_resolution.to_string(), "1920x1080".to_string()); + assert_eq!( + DecimalResolution::new(1920, 1080).to_string(), + "1920x1080".to_string() + ); - let decimal_resolution = DecimalResolution { - width: 1280, - height: 720, - }; - assert_eq!(decimal_resolution.to_string(), "1280x720".to_string()); + assert_eq!( + DecimalResolution::new(1280, 720).to_string(), + "1280x720".to_string() + ); } #[test] fn test_parse() { - let decimal_resolution = DecimalResolution { - width: 1920, - height: 1080, - }; assert_eq!( - decimal_resolution, + DecimalResolution::new(1920, 1080), "1920x1080".parse::<DecimalResolution>().unwrap() ); - let decimal_resolution = DecimalResolution { - width: 1280, - height: 720, - }; assert_eq!( - decimal_resolution, + DecimalResolution::new(1280, 720), "1280x720".parse::<DecimalResolution>().unwrap() ); + + assert!("1280".parse::<DecimalResolution>().is_err()); + } + + #[test] + fn test_width() { + assert_eq!(DecimalResolution::new(1920, 1080).width(), 1920); + assert_eq!(DecimalResolution::new(1920, 1080).set_width(12).width(), 12); + } + + #[test] + fn test_height() { + assert_eq!(DecimalResolution::new(1920, 1080).height(), 1080); + assert_eq!( + DecimalResolution::new(1920, 1080).set_height(12).height(), + 12 + ); } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 1b1830d..90ff14c 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -10,7 +10,6 @@ mod hexadecimal_sequence; mod in_stream_id; mod initialization_vector; mod media_type; -mod playlist_type; mod protocol_version; mod session_data; mod signed_decimal_floating_point; @@ -27,7 +26,6 @@ pub use hexadecimal_sequence::*; pub use in_stream_id::*; pub use initialization_vector::*; pub use media_type::*; -pub use playlist_type::*; pub use protocol_version::*; pub use session_data::*; pub use signed_decimal_floating_point::*; diff --git a/src/types/playlist_type.rs b/src/types/playlist_type.rs index 29efa54..8b13789 100644 --- a/src/types/playlist_type.rs +++ b/src/types/playlist_type.rs @@ -1,37 +1 @@ -use std::fmt; -use std::str::FromStr; -use crate::Error; - -/// Playlist type. -/// -/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE] -/// -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum PlaylistType { - Event, - Vod, -} - -impl fmt::Display for PlaylistType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - PlaylistType::Event => write!(f, "EVENT"), - PlaylistType::Vod => write!(f, "VOD"), - } - } -} - -impl FromStr for PlaylistType { - type Err = Error; - - fn from_str(input: &str) -> Result<Self, Self::Err> { - match input { - "EVENT" => Ok(PlaylistType::Event), - "VOD" => Ok(PlaylistType::Vod), - _ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))), - } - } -} diff --git a/src/utils.rs b/src/utils.rs index 7f557ec..eeda232 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ -use crate::{Error, Result}; +use crate::Error; -pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> Result<bool> { +pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> { match s.as_ref() { "YES" => Ok(true), "NO" => Ok(false), @@ -8,7 +8,7 @@ pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> Result<bool> { } } -pub(crate) fn parse_u64<T: AsRef<str>>(s: T) -> Result<u64> { +pub(crate) fn parse_u64<T: AsRef<str>>(s: T) -> crate::Result<u64> { let n = s.as_ref().parse().map_err(Error::unknown)?; // TODO: Error::number Ok(n) } diff --git a/tests/playlist.rs b/tests/playlist.rs new file mode 100644 index 0000000..9b92ae5 --- /dev/null +++ b/tests/playlist.rs @@ -0,0 +1,47 @@ +//! Credits go to +//! - https://github.com/globocom/m3u8/blob/master/tests/playlists.py +use hls_m3u8::tags::*; +use hls_m3u8::types::*; +use hls_m3u8::MediaPlaylist; + +use std::time::Duration; + +#[test] +fn test_simple_playlist() { + let playlist = r#" + #EXTM3U + #EXT-X-TARGETDURATION:5220 + #EXTINF:0, + http://media.example.com/entire1.ts + #EXTINF:5220, + http://media.example.com/entire2.ts + #EXT-X-ENDLIST"#; + + let media_playlist = playlist.parse::<MediaPlaylist>().unwrap(); + assert_eq!( + media_playlist.target_duration_tag(), + ExtXTargetDuration::new(Duration::from_secs(5220)) + ); + + assert_eq!(media_playlist.segments().len(), 2); + + assert_eq!( + media_playlist.segments()[0].inf_tag(), + &ExtInf::new(Duration::from_secs(0)) + ); + + assert_eq!( + media_playlist.segments()[1].inf_tag(), + &ExtInf::new(Duration::from_secs(5220)) + ); + + assert_eq!( + media_playlist.segments()[0].uri(), + &SingleLineString::new("http://media.example.com/entire1.ts").unwrap() + ); + + assert_eq!( + media_playlist.segments()[1].uri(), + &SingleLineString::new("http://media.example.com/entire2.ts").unwrap() + ); +} From 6ffbe503225b2926c013f4830b90636a7f0e51f1 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 10:51:04 +0200 Subject: [PATCH 15/25] internalized DecimalResolution #9 --- .../master_playlist/i_frame_stream_inf.rs | 40 +++++++++++++++++-- src/tags/master_playlist/stream_inf.rs | 11 +++-- src/types/decimal_resolution.rs | 2 +- src/types/mod.rs | 2 +- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 50e75bd..1567c07 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -13,22 +13,37 @@ use crate::Error; /// /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3 #[derive(Getters, Setters, MutGetters, Debug, Clone, PartialEq, Eq, Hash)] -#[get = "pub"] -#[set = "pub"] -#[get_mut = "pub"] pub struct ExtXIFrameStreamInf { + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] /// The URI, that identifies the associated media playlist. uri: String, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] /// The peak segment bit rate of the variant stream. bandwidth: u64, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] /// The average segment bit rate of the variant stream. average_bandwidth: Option<u64>, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] /// A string that represents the list of codec types contained the variant stream. codecs: Option<String>, /// The optimal pixel resolution at which to display all the video in the variant stream. resolution: Option<DecimalResolution>, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] /// The HDCP level of the variant stream. hdcp_level: Option<HdcpLevel>, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] /// The group identifier for the video in the variant stream. video: Option<String>, } @@ -49,6 +64,25 @@ impl ExtXIFrameStreamInf { } } + /// The optimal pixel resolution at which to display all the video in the variant stream. + pub fn resolution(&self) -> Option<(usize, usize)> { + if let Some(res) = &self.resolution { + Some((res.width(), res.height())) + } else { + None + } + } + + /// Sets the optimal pixel resolution at which to display all the video in the variant stream. + pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self { + if let Some(res) = &mut self.resolution { + res.set_width(width); + res.set_height(height); + } else { + self.resolution = Some(DecimalResolution::new(width, height)); + } + self + } /// Returns the protocol compatibility version that this tag requires. pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 296fba0..d613d6a 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -67,9 +67,14 @@ impl ExtXStreamInf { self.codecs.as_ref() } - /// Returns the optimal pixel resolution at which to display all the video in the variant stream. - pub const fn resolution(&self) -> Option<DecimalResolution> { - self.resolution + /// Returns the optimal pixel resolution at which to display all the video in the variant + /// stream. + pub fn resolution(&self) -> Option<(usize, usize)> { + if let Some(res) = &self.resolution { + Some((res.width(), res.height())) + } else { + None + } } /// Returns the maximum frame rate for all the video in the variant stream. diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index 4df01a2..7d225c6 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -9,7 +9,7 @@ use crate::Error; /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct DecimalResolution { +pub(crate) struct DecimalResolution { width: usize, height: usize, } diff --git a/src/types/mod.rs b/src/types/mod.rs index 90ff14c..4e2e6a1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -18,7 +18,7 @@ mod single_line_string; pub use byte_range::*; pub use closed_captions::*; pub use decimal_floating_point::*; -pub use decimal_resolution::*; +pub(crate) use decimal_resolution::*; pub use decryption_key::*; pub use encryption_method::*; pub use hdcp_level::*; From c28d6963a6682ccc306100751a1dc62d0b8fa5b4 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 11:05:22 +0200 Subject: [PATCH 16/25] removed SingleLineString #9 --- src/line.rs | 9 ++-- src/media_segment.rs | 6 +-- src/tags/master_playlist/stream_inf.rs | 13 ++--- src/tags/media_segment/inf.rs | 45 +++++----------- src/tags/media_segment/program_date_time.rs | 20 +++---- src/types/mod.rs | 2 - src/types/single_line_string.rs | 58 --------------------- tests/playlist.rs | 4 +- 8 files changed, 35 insertions(+), 122 deletions(-) delete mode 100644 src/types/single_line_string.rs diff --git a/src/line.rs b/src/line.rs index a5aa690..6d8ca95 100644 --- a/src/line.rs +++ b/src/line.rs @@ -3,7 +3,6 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::tags; -use crate::types::SingleLineString; use crate::Error; #[derive(Debug, Default)] @@ -54,7 +53,7 @@ impl FromStr for Lines { continue; } } else { - Line::Uri(SingleLineString::new(line)?) + Line::Uri(line.trim().to_string()) } } }; @@ -92,7 +91,7 @@ impl DerefMut for Lines { #[derive(Debug, PartialEq, Eq)] pub enum Line { Tag(Tag), - Uri(SingleLineString), + Uri(String), } #[allow(clippy::large_enum_variant)] @@ -120,7 +119,7 @@ pub enum Tag { ExtXSessionKey(tags::ExtXSessionKey), ExtXIndependentSegments(tags::ExtXIndependentSegments), ExtXStart(tags::ExtXStart), - Unknown(SingleLineString), + Unknown(String), } impl fmt::Display for Tag { @@ -202,7 +201,7 @@ impl FromStr for Tag { } else if s.starts_with(tags::ExtXStart::PREFIX) { s.parse().map(Tag::ExtXStart) } else { - SingleLineString::new(s).map(Tag::Unknown) + Ok(Tag::Unknown(s.to_string())) } } } diff --git a/src/media_segment.rs b/src/media_segment.rs index f157ad2..8460547 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -6,7 +6,7 @@ use derive_builder::Builder; use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, }; -use crate::types::{ProtocolVersion, SingleLineString}; +use crate::types::ProtocolVersion; /// Media segment. #[derive(Debug, Clone, Builder)] @@ -33,7 +33,7 @@ pub struct MediaSegment { /// Sets an [ExtInf] tag. inf_tag: ExtInf, /// Sets an Uri. - uri: SingleLineString, + uri: String, } impl MediaSegmentBuilder { @@ -80,7 +80,7 @@ impl MediaSegment { MediaSegmentBuilder::default() } /// Returns the URI of the media segment. - pub const fn uri(&self) -> &SingleLineString { + pub const fn uri(&self) -> &String { &self.uri } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index d613d6a..e494e02 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -4,7 +4,6 @@ use std::str::FromStr; use crate::attribute::AttributePairs; use crate::types::{ ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion, - SingleLineString, }; use crate::utils::{parse_u64, quote, tag, unquote}; use crate::Error; @@ -14,7 +13,7 @@ use crate::Error; /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExtXStreamInf { - uri: SingleLineString, + uri: String, bandwidth: u64, average_bandwidth: Option<u64>, codecs: Option<String>, @@ -33,7 +32,7 @@ impl ExtXStreamInf { /// Makes a new `ExtXStreamInf` tag. pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self { ExtXStreamInf { - uri: SingleLineString::new(uri.to_string()).unwrap(), + uri: uri.to_string(), bandwidth, average_bandwidth: None, codecs: None, @@ -48,7 +47,7 @@ impl ExtXStreamInf { } /// Returns the URI that identifies the associated media playlist. - pub const fn uri(&self) -> &SingleLineString { + pub const fn uri(&self) -> &String { &self.uri } @@ -155,12 +154,10 @@ impl FromStr for ExtXStreamInf { fn from_str(input: &str) -> Result<Self, Self::Err> { let mut lines = input.lines(); let first_line = lines.next().ok_or(Error::missing_value("first_line"))?; - let second_line = lines.next().ok_or(Error::missing_value("second_line"))?; + let uri = lines.next().ok_or(Error::missing_value("second_line"))?; let first_line = tag(first_line, Self::PREFIX)?; - let uri = SingleLineString::new(second_line)?; - let mut bandwidth = None; let mut average_bandwidth = None; let mut codecs = None; @@ -194,7 +191,7 @@ impl FromStr for ExtXStreamInf { let bandwidth = bandwidth.ok_or(Error::missing_value("EXT-X-BANDWIDTH"))?; Ok(ExtXStreamInf { - uri, + uri: uri.to_string(), bandwidth, average_bandwidth, codecs, diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 9d2c5b7..287864b 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -2,7 +2,7 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; -use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString}; +use crate::types::{DecimalFloatingPoint, ProtocolVersion}; use crate::utils::tag; use crate::Error; @@ -35,11 +35,10 @@ use crate::Error; /// ``` /// use std::time::Duration; /// use hls_m3u8::tags::ExtInf; -/// use hls_m3u8::types::SingleLineString; /// /// let ext_inf = ExtInf::with_title( /// Duration::from_millis(88), -/// SingleLineString::new("title").unwrap() +/// "title" /// ); /// /// assert_eq!(ext_inf.duration(), Duration::from_millis(88)); @@ -48,7 +47,7 @@ use crate::Error; #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtInf { duration: Duration, - title: Option<SingleLineString>, + title: Option<String>, } impl ExtInf { @@ -63,10 +62,10 @@ impl ExtInf { } /// Makes a new `ExtInf` tag with the given title. - pub const fn with_title(duration: Duration, title: SingleLineString) -> Self { + pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self { ExtInf { duration, - title: Some(title), + title: Some(title.to_string()), } } @@ -76,7 +75,7 @@ impl ExtInf { } /// Returns the title of the associated media segment. - pub fn title(&self) -> Option<&SingleLineString> { + pub fn title(&self) -> Option<&String> { self.title.as_ref() } @@ -127,7 +126,7 @@ impl FromStr for ExtInf { if tokens[1].trim().is_empty() { None } else { - Some(SingleLineString::new(tokens[1])?) + Some(tokens[1].to_string()) } } else { None @@ -160,19 +159,11 @@ mod test { ); assert_eq!( "#EXTINF:5.5,title".to_string(), - ExtInf::with_title( - Duration::from_millis(5500), - SingleLineString::new("title").unwrap() - ) - .to_string() + ExtInf::with_title(Duration::from_millis(5500), "title").to_string() ); assert_eq!( "#EXTINF:5,title".to_string(), - ExtInf::with_title( - Duration::from_secs(5), - SingleLineString::new("title").unwrap() - ) - .to_string() + ExtInf::with_title(Duration::from_secs(5), "title").to_string() ); } @@ -197,17 +188,11 @@ mod test { ); assert_eq!( "#EXTINF:5.5,title".parse::<ExtInf>().unwrap(), - ExtInf::with_title( - Duration::from_millis(5500), - SingleLineString::new("title").unwrap() - ) + ExtInf::with_title(Duration::from_millis(5500), "title") ); assert_eq!( "#EXTINF:5,title".parse::<ExtInf>().unwrap(), - ExtInf::with_title( - Duration::from_secs(5), - SingleLineString::new("title").unwrap() - ) + ExtInf::with_title(Duration::from_secs(5), "title") ); } @@ -215,12 +200,8 @@ mod test { fn test_title() { assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), None); assert_eq!( - ExtInf::with_title( - Duration::from_secs(5), - SingleLineString::new("title").unwrap() - ) - .title(), - Some(&SingleLineString::new("title").unwrap()) + ExtInf::with_title(Duration::from_secs(5), "title").title(), + Some(&"title".to_string()) ); } diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index a4be50d..46a2874 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,7 +1,7 @@ use std::fmt; use std::str::FromStr; -use crate::types::{ProtocolVersion, SingleLineString}; +use crate::types::ProtocolVersion; use crate::utils::tag; use crate::Error; @@ -9,21 +9,19 @@ use crate::Error; /// /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXProgramDateTime { - date_time: SingleLineString, -} +pub struct ExtXProgramDateTime(String); impl ExtXProgramDateTime { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; /// Makes a new `ExtXProgramDateTime` tag. - pub const fn new(date_time: SingleLineString) -> Self { - ExtXProgramDateTime { date_time } + pub fn new<T: ToString>(date_time: T) -> Self { + Self(date_time.to_string()) } /// Returns the date-time of the first sample of the associated media segment. - pub const fn date_time(&self) -> &SingleLineString { - &self.date_time + pub const fn date_time(&self) -> &String { + &self.0 } /// Returns the protocol compatibility version that this tag requires. @@ -34,7 +32,7 @@ impl ExtXProgramDateTime { impl fmt::Display for ExtXProgramDateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.date_time) + write!(f, "{}{}", Self::PREFIX, self.0) } } @@ -46,9 +44,7 @@ impl FromStr for ExtXProgramDateTime { // TODO: parse with chrono - Ok(ExtXProgramDateTime { - date_time: (SingleLineString::new(input))?, - }) + Ok(Self::new(input)) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 4e2e6a1..5a78297 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -13,7 +13,6 @@ mod media_type; mod protocol_version; mod session_data; mod signed_decimal_floating_point; -mod single_line_string; pub use byte_range::*; pub use closed_captions::*; @@ -29,4 +28,3 @@ pub use media_type::*; pub use protocol_version::*; pub use session_data::*; pub use signed_decimal_floating_point::*; -pub use single_line_string::*; diff --git a/src/types/single_line_string.rs b/src/types/single_line_string.rs deleted file mode 100644 index fea3dbb..0000000 --- a/src/types/single_line_string.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::Error; -use std::fmt; -use std::ops::Deref; - -/// String that represents a single line in a playlist file. -/// -/// See: [4.1. Definition of a Playlist] -/// -/// [4.1. Definition of a Playlist]: https://tools.ietf.org/html/rfc8216#section-4.1 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SingleLineString(String); - -impl SingleLineString { - /// Makes a new `SingleLineString` instance. - /// - /// # Errors - /// - /// If the given string contains any control characters, - /// this function will return an error which has the kind `ErrorKind::InvalidInput`. - pub fn new<T: Into<String>>(s: T) -> crate::Result<Self> { - let s = s.into(); - if s.chars().any(|c| c.is_control()) { - Err(Error::invalid_input()) - } else { - Ok(SingleLineString(s)) - } - } -} - -impl Deref for SingleLineString { - type Target = str; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef<str> for SingleLineString { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for SingleLineString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn single_line_string() { - assert!(SingleLineString::new("foo").is_ok()); - assert!(SingleLineString::new("b\rar").is_err()); - } -} diff --git a/tests/playlist.rs b/tests/playlist.rs index 9b92ae5..466a6c7 100644 --- a/tests/playlist.rs +++ b/tests/playlist.rs @@ -37,11 +37,11 @@ fn test_simple_playlist() { assert_eq!( media_playlist.segments()[0].uri(), - &SingleLineString::new("http://media.example.com/entire1.ts").unwrap() + &"http://media.example.com/entire1.ts".to_string() ); assert_eq!( media_playlist.segments()[1].uri(), - &SingleLineString::new("http://media.example.com/entire2.ts").unwrap() + &"http://media.example.com/entire2.ts".to_string() ); } From fa96a76ca9c9044f3694a1d7ae5f305aa3a14802 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 11:25:41 +0200 Subject: [PATCH 17/25] parse Urls #9 --- Cargo.toml | 1 + src/error.rs | 13 +++++++++++++ src/line.rs | 6 ++++-- src/media_segment.rs | 5 +++-- src/tags/master_playlist/stream_inf.rs | 25 +++++++++++++++---------- tests/playlist.rs | 5 ++--- 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 35c5a24..4ee2342 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ codecov = {repository = "sile/hls_m3u8"} getset = "0.0.8" failure = "0.1.5" derive_builder = "0.7.2" +url = "2.1.0" [dev-dependencies] clap = "2" diff --git a/src/error.rs b/src/error.rs index 951bd18..c6eee27 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,9 @@ pub type Result<T> = std::result::Result<T, Error>; /// The ErrorKind. #[derive(Debug, Fail, Clone, PartialEq, Eq)] pub enum ErrorKind { + #[fail(display = "UrlParseError: {}", _0)] + /// An error from the [Url](url) crate. + UrlParseError(String), #[fail(display = "UnknownError: {}", _0)] /// An unknown error occured. UnknownError(String), @@ -177,6 +180,10 @@ impl Error { pub(crate) fn builder_error<T: ToString>(value: T) -> Self { Self::from(ErrorKind::BuilderError(value.to_string())) } + + pub(crate) fn url<T: ToString>(value: T) -> Self { + Self::from(ErrorKind::UrlParseError(value.to_string())) + } } impl From<::std::num::ParseIntError> for Error { @@ -196,3 +203,9 @@ impl From<::std::io::Error> for Error { Error::io(value) } } + +impl From<::url::ParseError> for Error { + fn from(value: ::url::ParseError) -> Self { + Error::url(value) + } +} diff --git a/src/line.rs b/src/line.rs index 6d8ca95..28b3a32 100644 --- a/src/line.rs +++ b/src/line.rs @@ -2,6 +2,8 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use url::Url; + use crate::tags; use crate::Error; @@ -53,7 +55,7 @@ impl FromStr for Lines { continue; } } else { - Line::Uri(line.trim().to_string()) + Line::Uri(line.trim().parse()?) } } }; @@ -91,7 +93,7 @@ impl DerefMut for Lines { #[derive(Debug, PartialEq, Eq)] pub enum Line { Tag(Tag), - Uri(String), + Uri(Url), } #[allow(clippy::large_enum_variant)] diff --git a/src/media_segment.rs b/src/media_segment.rs index 8460547..d637131 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -2,6 +2,7 @@ use std::fmt; use std::iter; use derive_builder::Builder; +use url::Url; use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, @@ -33,7 +34,7 @@ pub struct MediaSegment { /// Sets an [ExtInf] tag. inf_tag: ExtInf, /// Sets an Uri. - uri: String, + uri: Url, } impl MediaSegmentBuilder { @@ -80,7 +81,7 @@ impl MediaSegment { MediaSegmentBuilder::default() } /// Returns the URI of the media segment. - pub const fn uri(&self) -> &String { + pub const fn uri(&self) -> &Url { &self.uri } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index e494e02..78dd7ee 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -1,6 +1,8 @@ use std::fmt; use std::str::FromStr; +use url::Url; + use crate::attribute::AttributePairs; use crate::types::{ ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion, @@ -13,7 +15,7 @@ use crate::Error; /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExtXStreamInf { - uri: String, + uri: Url, bandwidth: u64, average_bandwidth: Option<u64>, codecs: Option<String>, @@ -30,9 +32,9 @@ impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; /// Makes a new `ExtXStreamInf` tag. - pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self { + pub const fn new(uri: Url, bandwidth: u64) -> Self { ExtXStreamInf { - uri: uri.to_string(), + uri, bandwidth, average_bandwidth: None, codecs: None, @@ -47,7 +49,7 @@ impl ExtXStreamInf { } /// Returns the URI that identifies the associated media playlist. - pub const fn uri(&self) -> &String { + pub const fn uri(&self) -> &Url { &self.uri } @@ -191,7 +193,7 @@ impl FromStr for ExtXStreamInf { let bandwidth = bandwidth.ok_or(Error::missing_value("EXT-X-BANDWIDTH"))?; Ok(ExtXStreamInf { - uri: uri.to_string(), + uri: uri.parse()?, bandwidth, average_bandwidth, codecs, @@ -212,26 +214,29 @@ mod test { #[test] fn test_parser() { - let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo" + let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com" .parse::<ExtXStreamInf>() .unwrap(); - assert_eq!(stream_inf, ExtXStreamInf::new("foo", 1000)); + assert_eq!( + stream_inf, + ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000) + ); } #[test] fn test_requires_version() { assert_eq!( ProtocolVersion::V1, - ExtXStreamInf::new("foo", 1000).requires_version() + ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000).requires_version() ); } #[test] fn test_display() { assert_eq!( - ExtXStreamInf::new("foo", 1000).to_string(), - "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo".to_string() + ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000).to_string(), + "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string() ); } } diff --git a/tests/playlist.rs b/tests/playlist.rs index 466a6c7..b61068a 100644 --- a/tests/playlist.rs +++ b/tests/playlist.rs @@ -1,7 +1,6 @@ //! Credits go to //! - https://github.com/globocom/m3u8/blob/master/tests/playlists.py use hls_m3u8::tags::*; -use hls_m3u8::types::*; use hls_m3u8::MediaPlaylist; use std::time::Duration; @@ -37,11 +36,11 @@ fn test_simple_playlist() { assert_eq!( media_playlist.segments()[0].uri(), - &"http://media.example.com/entire1.ts".to_string() + &"http://media.example.com/entire1.ts".parse().unwrap() ); assert_eq!( media_playlist.segments()[1].uri(), - &"http://media.example.com/entire2.ts".to_string() + &"http://media.example.com/entire2.ts".parse().unwrap() ); } From db6961d19f540eb0ba9f4490f0dd0ab71e5c39f1 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 12:51:51 +0200 Subject: [PATCH 18/25] parse dates with chrono #9 --- Cargo.toml | 1 + src/error.rs | 13 ++++ src/tags/media_segment/date_range.rs | 67 ++++++++++++++++----- src/tags/media_segment/program_date_time.rs | 53 +++++++++++----- 4 files changed, 106 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4ee2342..d37b563 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ getset = "0.0.8" failure = "0.1.5" derive_builder = "0.7.2" url = "2.1.0" +chrono = "0.4.9" [dev-dependencies] clap = "2" diff --git a/src/error.rs b/src/error.rs index c6eee27..7acb888 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,9 @@ pub type Result<T> = std::result::Result<T, Error>; /// The ErrorKind. #[derive(Debug, Fail, Clone, PartialEq, Eq)] pub enum ErrorKind { + #[fail(display = "ChronoParseError: {}", _0)] + /// An error from the [Chrono](chrono) crate. + ChronoParseError(String), #[fail(display = "UrlParseError: {}", _0)] /// An error from the [Url](url) crate. UrlParseError(String), @@ -184,6 +187,10 @@ impl Error { pub(crate) fn url<T: ToString>(value: T) -> Self { Self::from(ErrorKind::UrlParseError(value.to_string())) } + + pub(crate) fn chrono<T: ToString>(value: T) -> Self { + Self::from(ErrorKind::ChronoParseError(value.to_string())) + } } impl From<::std::num::ParseIntError> for Error { @@ -209,3 +216,9 @@ impl From<::url::ParseError> for Error { Error::url(value) } } + +impl From<::chrono::ParseError> for Error { + fn from(value: ::chrono::ParseError) -> Self { + Error::chrono(value) + } +} diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 138002c..d1c4fbd 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -3,6 +3,9 @@ use std::fmt; use std::str::FromStr; use std::time::Duration; +use chrono::{DateTime, FixedOffset}; +use getset::{Getters, MutGetters, Setters}; + use crate::attribute::AttributePairs; use crate::types::{DecimalFloatingPoint, ProtocolVersion}; use crate::utils::{quote, tag, unquote}; @@ -14,19 +17,52 @@ use crate::Error; /// /// TODO: Implement properly #[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters, MutGetters, Setters)] +#[get = "pub"] +#[set = "pub"] +#[get_mut = "pub"] pub struct ExtXDateRange { - pub id: String, - pub class: Option<String>, - pub start_date: String, - pub end_date: Option<String>, - pub duration: Option<Duration>, - pub planned_duration: Option<Duration>, - pub scte35_cmd: Option<String>, - pub scte35_out: Option<String>, - pub scte35_in: Option<String>, - pub end_on_next: bool, - pub client_attributes: BTreeMap<String, String>, + /// A string that uniquely identifies a Date Range in the Playlist. + /// This attribute is REQUIRED. + id: String, + /// A client-defined string that specifies some set of attributes and their associated value + /// semantics. All Date Ranges with the same CLASS attribute value MUST adhere to these + /// semantics. This attribute is OPTIONAL. + class: Option<String>, + /// The date at which the Date Range begins. This attribute is REQUIRED. + start_date: DateTime<FixedOffset>, + /// The date at which the Date Range ends. It MUST be equal to or later than the value of the + /// START-DATE attribute. This attribute is OPTIONAL. + end_date: Option<DateTime<FixedOffset>>, + /// The duration of the Date Range. It MUST NOT be negative. A single + /// instant in time (e.g., crossing a finish line) SHOULD be + /// represented with a duration of 0. This attribute is OPTIONAL. + duration: Option<Duration>, + /// The expected duration of the Date Range. It MUST NOT be negative. This + /// attribute SHOULD be used to indicate the expected duration of a + /// Date Range whose actual duration is not yet known. + /// It is OPTIONAL. + planned_duration: Option<Duration>, + /// + scte35_cmd: Option<String>, + /// + scte35_out: Option<String>, + /// + scte35_in: Option<String>, + /// This attribute indicates that the end of the range containing it is equal to the + /// START-DATE of its Following Range. The Following Range is the + /// Date Range of the same CLASS, that has the earliest START-DATE + /// after the START-DATE of the range in question. This attribute is + /// OPTIONAL. + end_on_next: bool, + /// The "X-" prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute MUST be a legal AttributeName. + /// Clients SHOULD use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. The attribute value MUST be + /// a quoted-string, a hexadecimal-sequence, or a decimal-floating- + /// point. An example of a client-defined attribute is X-COM-EXAMPLE- + /// AD-ID="XYZ123". These attributes are OPTIONAL. + client_attributes: BTreeMap<String, String>, } impl ExtXDateRange { @@ -102,7 +138,7 @@ impl FromStr for ExtXDateRange { "ID" => id = Some(unquote(value)), "CLASS" => class = Some(unquote(value)), "START-DATE" => start_date = Some(unquote(value)), - "END-DATE" => end_date = Some(unquote(value)), + "END-DATE" => end_date = Some(unquote(value).parse()?), "DURATION" => { let seconds: DecimalFloatingPoint = (value.parse())?; duration = Some(seconds.to_duration()); @@ -132,7 +168,10 @@ impl FromStr for ExtXDateRange { } let id = id.ok_or(Error::missing_value("EXT-X-ID"))?; - let start_date = start_date.ok_or(Error::missing_value("EXT-X-START-DATE"))?; + let start_date = start_date + .ok_or(Error::missing_value("EXT-X-START-DATE"))? + .parse()?; + if end_on_next { if class.is_none() { return Err(Error::invalid_input()); diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 46a2874..643ec4b 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,6 +1,8 @@ use std::fmt; use std::str::FromStr; +use chrono::{DateTime, FixedOffset}; + use crate::types::ProtocolVersion; use crate::utils::tag; use crate::Error; @@ -8,19 +10,19 @@ use crate::Error; /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] /// /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXProgramDateTime(String); +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ExtXProgramDateTime(DateTime<FixedOffset>); impl ExtXProgramDateTime { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; /// Makes a new `ExtXProgramDateTime` tag. - pub fn new<T: ToString>(date_time: T) -> Self { - Self(date_time.to_string()) + pub fn new<T: Into<DateTime<FixedOffset>>>(date_time: T) -> Self { + Self(date_time.into()) } /// Returns the date-time of the first sample of the associated media segment. - pub const fn date_time(&self) -> &String { + pub const fn date_time(&self) -> &DateTime<FixedOffset> { &self.0 } @@ -32,7 +34,8 @@ impl ExtXProgramDateTime { impl fmt::Display for ExtXProgramDateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.0) + let date_time = self.0.to_rfc3339(); + write!(f, "{}{}", Self::PREFIX, date_time) } } @@ -43,8 +46,8 @@ impl FromStr for ExtXProgramDateTime { let input = tag(input, Self::PREFIX)?; // TODO: parse with chrono - - Ok(Self::new(input)) + let date_time = DateTime::parse_from_rfc3339(input)?; + Ok(Self::new(date_time)) } } @@ -53,12 +56,34 @@ mod test { use super::*; #[test] - fn ext_x_program_date_time() { - let text = "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"; - assert!(text.parse::<ExtXProgramDateTime>().is_ok()); + fn test_display() { + let date_time = "2010-02-19T14:54:23.031+08:00" + .parse::<DateTime<FixedOffset>>() + .unwrap(); - let tag = text.parse::<ExtXProgramDateTime>().unwrap(); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + let program_date_time = ExtXProgramDateTime::new(date_time); + + assert_eq!( + program_date_time.to_string(), + "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string() + ); + } + + #[test] + fn test_parser() { + "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00" + .parse::<ExtXProgramDateTime>() + .unwrap(); + } + + #[test] + fn test_requires_version() { + let date_time = "2010-02-19T14:54:23.031+08:00" + .parse::<DateTime<FixedOffset>>() + .unwrap(); + + let program_date_time = ExtXProgramDateTime::new(date_time); + + assert_eq!(program_date_time.requires_version(), ProtocolVersion::V1); } } From 1d614d580aac6674fa086d49ba5ed4a984e79c1e Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 16:45:43 +0200 Subject: [PATCH 19/25] updated ExtXKey + ExtXSessionKey #9 --- src/attribute.rs | 10 +- src/master_playlist.rs | 14 - src/tags/master_playlist/session_data.rs | 14 +- src/tags/master_playlist/session_key.rs | 433 ++++++++++++++++-- src/tags/media_segment/key.rs | 536 ++++++++++++++++++++--- src/types/decryption_key.rs | 87 ---- src/types/encryption_method.rs | 57 ++- src/types/initialization_vector.rs | 14 +- src/types/mod.rs | 6 +- src/types/session_data.rs | 11 - 10 files changed, 947 insertions(+), 235 deletions(-) delete mode 100644 src/types/session_data.rs diff --git a/src/attribute.rs b/src/attribute.rs index 338fed5..f2645e0 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -51,8 +51,8 @@ impl FromStr for AttributePairs { fn from_str(input: &str) -> Result<Self, Self::Err> { let mut result = AttributePairs::new(); - for line in split(input) { - let pair = line.trim().split("=").collect::<Vec<_>>(); + for line in split(input, ',') { + let pair = split(line.trim(), '='); if pair.len() < 2 { return Err(Error::invalid_input()); @@ -63,11 +63,13 @@ impl FromStr for AttributePairs { result.insert(key.to_string(), value.to_string()); } + + dbg!(&result); Ok(result) } } -fn split(value: &str) -> Vec<String> { +fn split(value: &str, terminator: char) -> Vec<String> { let mut result = vec![]; let mut inside_quotes = false; @@ -83,7 +85,7 @@ fn split(value: &str) -> Vec<String> { } temp_string.push(c); } - ',' => { + k if (k == terminator) => { if !inside_quotes { result.push(temp_string); temp_string = String::new(); diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 4d1d2a5..ec11a86 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -109,8 +109,6 @@ impl MasterPlaylistBuilder { .map_err(|e| e.to_string())?; self.validate_session_data_tags() .map_err(|e| e.to_string())?; - self.validate_session_key_tags() - .map_err(|e| e.to_string())?; Ok(()) } @@ -232,18 +230,6 @@ impl MasterPlaylistBuilder { Ok(()) } - fn validate_session_key_tags(&self) -> crate::Result<()> { - let mut set = HashSet::new(); - if let Some(value) = &self.session_key_tags { - for t in value { - if !set.insert(t.key()) { - return Err(Error::custom(format!("Conflict: {}", t))); - } - } - } - Ok(()) - } - fn check_media_group<T: ToString>(&self, media_type: MediaType, group_id: T) -> bool { if let Some(value) = &self.media_tags { value diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 183318a..0c7d1aa 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -4,10 +4,22 @@ use std::str::FromStr; use getset::{Getters, MutGetters, Setters}; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, SessionData}; +use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; use crate::Error; +/// Session data. +/// +/// See: [4.3.4.4. EXT-X-SESSION-DATA] +/// +/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SessionData { + Value(String), + Uri(String), +} + /// [4.3.4.4. EXT-X-SESSION-DATA] /// /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index e2d29ba..ac19110 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,71 +1,444 @@ use std::fmt; use std::str::FromStr; -use crate::types::{DecryptionKey, ProtocolVersion}; -use crate::utils::tag; +use url::Url; + +use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; /// [4.3.4.5. EXT-X-SESSION-KEY] /// /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXSessionKey { - key: DecryptionKey, + method: EncryptionMethod, + uri: Option<Url>, + iv: Option<InitializationVector>, + key_format: Option<String>, + key_format_versions: Option<String>, } impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; - /// Makes a new `ExtXSessionKey` tag. - pub const fn new(key: DecryptionKey) -> Self { - ExtXSessionKey { key } + /// Makes a new [ExtXSessionKey] tag. + /// # Panic + /// This method will panic, if the [EncryptionMethod] is None. + pub fn new(method: EncryptionMethod, uri: Url) -> Self { + if method == EncryptionMethod::None { + panic!("The EncryptionMethod is not allowed to be None"); + } + + Self { + method, + uri: Some(uri), + iv: None, + key_format: None, + key_format_versions: None, + } } - /// Returns a decryption key for the playlist. - pub const fn key(&self) -> &DecryptionKey { - &self.key + /// Returns the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.method(), + /// EncryptionMethod::Aes128 + /// ); + /// ``` + pub const fn method(&self) -> EncryptionMethod { + self.method + } + + /// Sets the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_method(EncryptionMethod::SampleAes); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() + /// ); + /// ``` + pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self { + self.method = value; + self + } + + /// Returns an `URI` that specifies how to obtain the key. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.uri(), + /// &Some("https://www.example.com".parse().unwrap()) + /// ); + /// ``` + pub const fn uri(&self) -> &Option<Url> { + &self.uri + } + + /// Sets the `URI` attribute. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_uri("http://www.google.com".parse().unwrap()); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() + /// ); + /// ``` + pub fn set_uri(&mut self, value: Url) -> &mut Self { + self.uri = Some(value); + self + } + + /// Returns the IV (Initialization Vector) attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 + /// ]); + /// + /// assert_eq!( + /// key.iv(), + /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + /// ); + /// ``` + pub fn iv(&self) -> Option<[u8; 16]> { + if let Some(iv) = &self.iv { + Some(iv.to_slice()) + } else { + None + } + } + + /// Sets the `IV` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 + /// ]); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() + /// ); + /// ``` + pub fn set_iv<T>(&mut self, value: T) -> &mut Self + where + T: Into<[u8; 16]>, + { + self.iv = Some(InitializationVector(value.into())); + self + } + + /// Returns a string that specifies how the key is + /// represented in the resource identified by the URI. + /// + //// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.key_format(), + /// &Some("key_format_attribute".to_string()) + /// ); + /// ``` + pub const fn key_format(&self) -> &Option<String> { + &self.key_format + } + + /// Sets the `KEYFORMAT` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() + /// ); + /// ``` + pub fn set_key_format<T: ToString>(&mut self, value: T) -> &mut Self { + self.key_format = Some(value.to_string()); + self + } + + /// Returns a string containing one or more positive + /// integers separated by the "/" character (for example, "1", "1/2", + /// or "1/2/5"). If more than one version of a particular `KEYFORMAT` + /// is defined, this attribute can be used to indicate which + /// version(s) this instance complies with. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.key_format_versions(), + /// &Some("1/2/3/4/5".to_string()) + /// ); + /// ``` + pub const fn key_format_versions(&self) -> &Option<String> { + &self.key_format_versions + } + + /// Sets the `KEYFORMATVERSIONS` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() + /// ); + /// ``` + pub fn set_key_format_versions<T: ToString>(&mut self, value: T) -> &mut Self { + self.key_format_versions = Some(value.to_string()); + self } /// Returns the protocol compatibility version that this tag requires. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion}; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.requires_version(), + /// ProtocolVersion::V1 + /// ); + /// ``` pub fn requires_version(&self) -> ProtocolVersion { - self.key.requires_version() + if self.key_format.is_some() | self.key_format_versions.is_some() { + ProtocolVersion::V5 + } else if self.iv.is_some() { + ProtocolVersion::V2 + } else { + ProtocolVersion::V1 + } } } impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.key) + write!(f, "{}METHOD={}", Self::PREFIX, self.method)?; + if let Some(uri) = &self.uri { + write!(f, ",URI={}", quote(uri))?; + } + if let Some(value) = &self.iv { + write!(f, ",IV={}", value)?; + } + if let Some(value) = &self.key_format { + write!(f, ",KEYFORMAT={}", quote(value))?; + } + if let Some(value) = &self.key_format_versions { + write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; + } + Ok(()) } } impl FromStr for ExtXSessionKey { - type Err = crate::Error; + type Err = Error; fn from_str(input: &str) -> Result<Self, Self::Err> { - let key = tag(input, Self::PREFIX)?.parse()?; - Ok(Self::new(key)) + let input = tag(input, Self::PREFIX)?; + let mut method = None; + let mut uri = None; + let mut iv = None; + let mut key_format = None; + let mut key_format_versions = None; + + for (key, value) in input.parse::<AttributePairs>()? { + match key.as_str() { + "METHOD" => method = Some((value.parse())?), + "URI" => uri = Some(unquote(value).parse()?), + "IV" => iv = Some((value.parse())?), + "KEYFORMAT" => key_format = Some(unquote(value)), + "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + + let method = method.ok_or(Error::missing_value("EXT-X-METHOD"))?; + if method == EncryptionMethod::None { + return Err(Error::custom( + "EXT-X-SESSION-KEY: Encryption Method must not be NONE", + )); + } + Ok(ExtXSessionKey { + method, + uri, + iv, + key_format, + key_format_versions, + }) } } #[cfg(test)] mod test { use super::*; - use crate::types::{EncryptionMethod, InitializationVector}; + use crate::types::EncryptionMethod; #[test] - fn ext_x_session_key() { - let tag = ExtXSessionKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: "foo".to_string(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: None, - key_format_versions: None, - }); - let text = - r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V2); + fn test_display() { + let mut key = ExtXSessionKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); + + assert_eq!( + key.to_string(), + "#EXT-X-SESSION-KEY:METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52" + .to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!( + r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + .parse::<ExtXSessionKey>() + .unwrap(), + ExtXSessionKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52".parse().unwrap() + ) + ); + + let mut key = ExtXSessionKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); + + assert_eq!( + "#EXT-X-SESSION-KEY:METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0X10ef8f758ca555115584bb5b3c687f52" + .parse::<ExtXSessionKey>() + .unwrap(), + key + ); + + key.set_key_format("baz"); + + assert_eq!( + r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://www.example.com/hls-key/key.bin",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# + .parse::<ExtXSessionKey>().unwrap(), + key + ) } } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index cb7844e..bfa7df9 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,52 +1,399 @@ use std::fmt; use std::str::FromStr; +use url::Url; + use crate::attribute::AttributePairs; -use crate::types::{DecryptionKey, ProtocolVersion}; -use crate::utils::tag; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; +use crate::utils::{quote, tag, unquote}; use crate::Error; /// [4.3.2.4. EXT-X-KEY] /// /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 +/// # Note +/// In case of an empty key (`EncryptionMethod::None`), all attributes will be ignored. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXKey(Option<DecryptionKey>); +pub struct ExtXKey { + method: EncryptionMethod, + uri: Option<Url>, + iv: Option<InitializationVector>, + key_format: Option<String>, + key_format_versions: Option<String>, +} impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; /// Makes a new `ExtXKey` tag. - pub const fn new(key: DecryptionKey) -> Self { - Self(Some(key)) + /// # Example + /// ``` + /// use url::Url; + /// + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\"" + /// ); + /// ``` + pub const fn new(method: EncryptionMethod, uri: Url) -> Self { + Self { + method, + uri: Some(uri), + iv: None, + key_format: None, + key_format_versions: None, + } } /// Makes a new `ExtXKey` tag without a decryption key. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; /// - /// This tag has the `METHDO=NONE` attribute. - pub const fn new_without_key() -> Self { - Self(None) + /// let key = ExtXKey::empty(); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=NONE" + /// ); + /// ``` + pub const fn empty() -> Self { + Self { + method: EncryptionMethod::None, + uri: None, + iv: None, + key_format: None, + key_format_versions: None, + } } - /// Returns the decryption key for the following media segments and media initialization sections. - pub fn key(&self) -> Option<&DecryptionKey> { - self.0.as_ref() + /// Returns whether the EncryptionMethod is None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXKey::empty(); + /// + /// assert_eq!( + /// key.method() == EncryptionMethod::None, + /// key.is_empty() + /// ); + /// ``` + pub fn is_empty(&self) -> bool { + if self.method == EncryptionMethod::None { + true + } else { + false + } + } + + /// Returns the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.method(), + /// EncryptionMethod::Aes128 + /// ); + /// ``` + pub const fn method(&self) -> EncryptionMethod { + self.method + } + + /// Sets the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_method(EncryptionMethod::SampleAes); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() + /// ); + /// ``` + pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self { + self.method = value; + self + } + + /// Returns an `URI` that specifies how to obtain the key. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.uri(), + /// &Some("https://www.example.com".parse().unwrap()) + /// ); + /// ``` + pub const fn uri(&self) -> &Option<Url> { + &self.uri + } + + /// Sets the `URI` attribute. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_uri("http://www.google.com".parse().unwrap()); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() + /// ); + /// ``` + pub fn set_uri(&mut self, value: Url) -> &mut Self { + self.uri = Some(value); + self + } + + /// Returns the IV (Initialization Vector) attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 + /// ]); + /// + /// assert_eq!( + /// key.iv(), + /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + /// ); + /// ``` + pub fn iv(&self) -> Option<[u8; 16]> { + if let Some(iv) = &self.iv { + Some(iv.to_slice()) + } else { + None + } + } + + /// Sets the `IV` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 + /// ]); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() + /// ); + /// ``` + pub fn set_iv<T>(&mut self, value: T) -> &mut Self + where + T: Into<[u8; 16]>, + { + self.iv = Some(InitializationVector(value.into())); + self + } + + /// Returns a string that specifies how the key is + /// represented in the resource identified by the URI. + /// + //// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.key_format(), + /// &Some("key_format_attribute".to_string()) + /// ); + /// ``` + pub const fn key_format(&self) -> &Option<String> { + &self.key_format + } + + /// Sets the `KEYFORMAT` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() + /// ); + /// ``` + pub fn set_key_format<T: ToString>(&mut self, value: T) -> &mut Self { + self.key_format = Some(value.to_string()); + self + } + + /// Returns a string containing one or more positive + /// integers separated by the "/" character (for example, "1", "1/2", + /// or "1/2/5"). If more than one version of a particular `KEYFORMAT` + /// is defined, this attribute can be used to indicate which + /// version(s) this instance complies with. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.key_format_versions(), + /// &Some("1/2/3/4/5".to_string()) + /// ); + /// ``` + pub const fn key_format_versions(&self) -> &Option<String> { + &self.key_format_versions + } + + /// Sets the `KEYFORMATVERSIONS` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() + /// ); + /// ``` + pub fn set_key_format_versions<T: ToString>(&mut self, value: T) -> &mut Self { + self.key_format_versions = Some(value.to_string()); + self } /// Returns the protocol compatibility version that this tag requires. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion}; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.requires_version(), + /// ProtocolVersion::V1 + /// ); + /// ``` pub fn requires_version(&self) -> ProtocolVersion { - self.0 - .as_ref() - .map_or(ProtocolVersion::V1, |k| k.requires_version()) + if self.key_format.is_some() | self.key_format_versions.is_some() { + ProtocolVersion::V5 + } else if self.iv.is_some() { + ProtocolVersion::V2 + } else { + ProtocolVersion::V1 + } } } impl fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - if let Some(value) = &self.0 { - write!(f, "{}", value)?; - } else { - write!(f, "METHOD=NONE")?; + write!(f, "{}METHOD={}", Self::PREFIX, self.method)?; + + if self.method == EncryptionMethod::None { + return Ok(()); + } + if let Some(uri) = &self.uri { + write!(f, ",URI={}", quote(uri))?; + } + if let Some(value) = &self.iv { + write!(f, ",IV={}", value)?; + } + if let Some(value) = &self.key_format { + write!(f, ",KEYFORMAT={}", quote(value))?; + } + if let Some(value) = &self.key_format_versions { + write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; } Ok(()) } @@ -57,74 +404,117 @@ impl FromStr for ExtXKey { fn from_str(input: &str) -> Result<Self, Self::Err> { let input = tag(input, Self::PREFIX)?; + let mut method = None; + let mut uri = None; + let mut iv = None; + let mut key_format = None; + let mut key_format_versions = None; - let pairs = input.parse::<AttributePairs>()?; - - if pairs.iter().any(|(k, v)| k == "METHOD" && v == "NONE") { - for (key, _) in pairs { - if key == "URI" || key == "IV" || key == "KEYFORMAT" || key == "KEYFORMATVERSIONS" { - return Err(Error::invalid_input()); + for (key, value) in input.parse::<AttributePairs>()? { + match key.as_str() { + "METHOD" => method = Some((value.parse())?), + "URI" => uri = Some(unquote(value).parse()?), + "IV" => iv = Some((value.parse())?), + "KEYFORMAT" => key_format = Some(unquote(value)), + "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. } } - - Ok(Self(None)) - } else { - Ok(Self(Some(input.parse()?))) } + + let method = method.ok_or(Error::missing_value("EXT-X-KEY:METHOD"))?; + if method != EncryptionMethod::None && uri.is_none() { + return Err(Error::missing_value("EXT-X-KEY:URI")); + } + + Ok(ExtXKey { + method, + uri, + iv, + key_format, + key_format_versions, + }) } } #[cfg(test)] mod test { use super::*; - use crate::types::{EncryptionMethod, InitializationVector}; + use crate::types::EncryptionMethod; #[test] - fn ext_x_key() { - let tag = ExtXKey::new_without_key(); - let text = "#EXT-X-KEY:METHOD=NONE"; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_display() { + assert_eq!( + ExtXKey::empty().to_string(), + "#EXT-X-KEY:METHOD=NONE".to_string() + ); - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: "foo".to_string(), - iv: None, - key_format: None, - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + assert_eq!( + ExtXKey::empty().set_key_format("hi").to_string(), + "#EXT-X-KEY:METHOD=NONE".to_string() + ); - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: "foo".to_string(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: None, - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V2); + let mut key = ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: "foo".to_string(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: Some("baz".to_string()), - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V5); + assert_eq!( + key.to_string(), + "#EXT-X-KEY:METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52" + .to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!( + r#"#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + .parse::<ExtXKey>() + .unwrap(), + ExtXKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52".parse().unwrap() + ) + ); + + let mut key = ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); + + assert_eq!( + "#EXT-X-KEY:METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0X10ef8f758ca555115584bb5b3c687f52" + .parse::<ExtXKey>() + .unwrap(), + key + ); + + let mut key = ExtXKey::new( + EncryptionMethod::Aes128, + "http://www.example.com".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]) + .set_key_format("baz"); + + assert_eq!( + r#"#EXT-X-KEY:METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# + .parse::<ExtXKey>().unwrap(), + key + ) } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 571a588..8b13789 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -1,88 +1 @@ -use std::fmt; -use std::str::{self, FromStr}; -use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; -use crate::utils::{quote, unquote}; -use crate::Error; - -/// Decryption key. -/// -/// See: [4.3.2.4. EXT-X-KEY] -/// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct DecryptionKey { - pub method: EncryptionMethod, - pub uri: String, - pub iv: Option<InitializationVector>, - pub key_format: Option<String>, - pub key_format_versions: Option<String>, -} - -impl DecryptionKey { - pub(crate) fn requires_version(&self) -> ProtocolVersion { - if self.key_format.is_some() | self.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } - } -} - -impl fmt::Display for DecryptionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "METHOD={}", self.method)?; - write!(f, ",URI={}", quote(&self.uri))?; - if let Some(value) = &self.iv { - write!(f, ",IV={}", value)?; - } - if let Some(value) = &self.key_format { - write!(f, ",KEYFORMAT={}", quote(value))?; - } - if let Some(value) = &self.key_format_versions { - write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; - } - Ok(()) - } -} - -impl FromStr for DecryptionKey { - type Err = Error; - - fn from_str(input: &str) -> Result<Self, Self::Err> { - let mut method = None; - let mut uri = None; - let mut iv = None; - let mut key_format = None; - let mut key_format_versions = None; - - for (key, value) in input.parse::<AttributePairs>()? { - match key.as_str() { - "METHOD" => method = Some((value.parse())?), - "URI" => uri = Some(unquote(value)), - "IV" => iv = Some((value.parse())?), - "KEYFORMAT" => key_format = Some(unquote(value)), - "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - - let method = method.ok_or(Error::missing_value("EXT-X-METHOD"))?; - let uri = uri.ok_or(Error::missing_value("EXT-X-URI"))?; - - Ok(DecryptionKey { - method, - uri, - iv, - key_format, - key_format_versions, - }) - } -} diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 8754ba1..a5389f8 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -11,7 +11,40 @@ use crate::Error; #[allow(missing_docs)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum EncryptionMethod { + /// `None` means that [MediaSegment]s are not encrypted. + /// + /// [MediaSegment]: crate::MediaSegment + None, + /// `Aes128` signals that the [MediaSegment]s are completely encrypted + /// using the Advanced Encryption Standard ([AES_128]) with a 128-bit + /// key, Cipher Block Chaining (CBC), and + /// [Public-Key Cryptography Standards #7 (PKCS7)] padding. + /// CBC is restarted on each segment boundary, using either the + /// Initialization Vector (IV) attribute value or the Media Sequence + /// Number as the IV. + /// + /// [MediaSegment]: crate::MediaSegment + /// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf + /// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652 Aes128, + /// `SampleAes` means that the [MediaSegment]s + /// contain media samples, such as audio or video, that are encrypted + /// using the Advanced Encryption Standard ([AES_128]). How these media + /// streams are encrypted and encapsulated in a segment depends on the + /// media encoding and the media format of the segment. fMP4 Media + /// Segments are encrypted using the 'cbcs' scheme of + /// [Common Encryption]. Encryption of other Media Segment + /// formats containing [H.264], [AAC], [AC-3], + /// and Enhanced [AC-3] media streams is described in the HTTP + /// Live Streaming (HLS) [SampleEncryption specification]. + /// + /// [MediaSegment]: crate::MediaSegment + /// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf + /// [Common Encryption]: https://tools.ietf.org/html/rfc8216#ref-COMMON_ENC + /// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264 + /// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496 + /// [AC-3]: https://tools.ietf.org/html/rfc8216#ref-AC_3 + /// [SampleEncryption specification]: https://tools.ietf.org/html/rfc8216#ref-SampleEnc SampleAes, } @@ -20,6 +53,7 @@ impl fmt::Display for EncryptionMethod { match &self { EncryptionMethod::Aes128 => "AES-128".fmt(f), EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), + EncryptionMethod::None => "NONE".fmt(f), } } } @@ -31,6 +65,7 @@ impl FromStr for EncryptionMethod { match input { "AES-128" => Ok(EncryptionMethod::Aes128), "SAMPLE-AES" => Ok(EncryptionMethod::SampleAes), + "NONE" => Ok(EncryptionMethod::None), _ => Err(Error::custom(format!( "Unknown encryption method: {:?}", input @@ -45,25 +80,29 @@ mod tests { #[test] fn test_display() { - let encryption_method = EncryptionMethod::Aes128; - assert_eq!(encryption_method.to_string(), "AES-128".to_string()); - - let encryption_method = EncryptionMethod::SampleAes; - assert_eq!(encryption_method.to_string(), "SAMPLE-AES".to_string()); + assert_eq!(EncryptionMethod::Aes128.to_string(), "AES-128".to_string()); + assert_eq!( + EncryptionMethod::SampleAes.to_string(), + "SAMPLE-AES".to_string() + ); + assert_eq!(EncryptionMethod::None.to_string(), "NONE".to_string()); } #[test] fn test_parse() { - let encryption_method = EncryptionMethod::Aes128; assert_eq!( - encryption_method, + EncryptionMethod::Aes128, "AES-128".parse::<EncryptionMethod>().unwrap() ); - let encryption_method = EncryptionMethod::SampleAes; assert_eq!( - encryption_method, + EncryptionMethod::SampleAes, "SAMPLE-AES".parse::<EncryptionMethod>().unwrap() ); + + assert_eq!( + EncryptionMethod::None, + "NONE".parse::<EncryptionMethod>().unwrap() + ); } } diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 9743f34..2b66bd2 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -10,7 +10,19 @@ use crate::Error; /// /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InitializationVector(pub [u8; 16]); +pub(crate) struct InitializationVector(pub [u8; 16]); + +impl InitializationVector { + pub const fn to_slice(&self) -> [u8; 16] { + self.0 + } +} + +impl From<[u8; 16]> for InitializationVector { + fn from(value: [u8; 16]) -> Self { + Self(value) + } +} impl Deref for InitializationVector { type Target = [u8]; diff --git a/src/types/mod.rs b/src/types/mod.rs index 5a78297..83390b7 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,7 +3,6 @@ mod byte_range; mod closed_captions; mod decimal_floating_point; mod decimal_resolution; -mod decryption_key; mod encryption_method; mod hdcp_level; mod hexadecimal_sequence; @@ -11,20 +10,17 @@ mod in_stream_id; mod initialization_vector; mod media_type; mod protocol_version; -mod session_data; mod signed_decimal_floating_point; pub use byte_range::*; pub use closed_captions::*; pub use decimal_floating_point::*; pub(crate) use decimal_resolution::*; -pub use decryption_key::*; pub use encryption_method::*; pub use hdcp_level::*; pub use hexadecimal_sequence::*; pub use in_stream_id::*; -pub use initialization_vector::*; +pub(crate) use initialization_vector::*; pub use media_type::*; pub use protocol_version::*; -pub use session_data::*; pub use signed_decimal_floating_point::*; diff --git a/src/types/session_data.rs b/src/types/session_data.rs deleted file mode 100644 index 4875b35..0000000 --- a/src/types/session_data.rs +++ /dev/null @@ -1,11 +0,0 @@ -/// Session data. -/// -/// See: [4.3.4.4. EXT-X-SESSION-DATA] -/// -/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum SessionData { - Value(String), - Uri(String), -} From fd66f8b4efc929feeac5883a4bb7dd62f1612356 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 16:47:35 +0200 Subject: [PATCH 20/25] remove decryption_key #9 --- src/types/decryption_key.rs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/types/decryption_key.rs diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/types/decryption_key.rs +++ /dev/null @@ -1 +0,0 @@ - From 42469275d34b5d32355f86bd087270f125451620 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 18:54:25 +0200 Subject: [PATCH 21/25] internalize decimal_floating_point #9 --- src/tags/master_playlist/stream_inf.rs | 4 ++-- src/types/decimal_floating_point.rs | 4 ++-- src/types/mod.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 78dd7ee..808d4fc 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -79,8 +79,8 @@ impl ExtXStreamInf { } /// Returns the maximum frame rate for all the video in the variant stream. - pub const fn frame_rate(&self) -> Option<DecimalFloatingPoint> { - self.frame_rate + pub fn frame_rate(&self) -> Option<f64> { + self.frame_rate.map_or(None, |v| Some(v.as_f64())) } /// Returns the HDCP level of the variant stream. diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index b044cc4..a829129 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -10,7 +10,7 @@ use crate::Error; /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct DecimalFloatingPoint(f64); +pub(crate) struct DecimalFloatingPoint(f64); impl DecimalFloatingPoint { /// Makes a new `DecimalFloatingPoint` instance. @@ -66,7 +66,7 @@ impl FromStr for DecimalFloatingPoint { return Err(Error::invalid_input()); } let n = input.parse()?; - Ok(DecimalFloatingPoint(n)) + DecimalFloatingPoint::new(n) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 83390b7..10641d3 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -14,7 +14,7 @@ mod signed_decimal_floating_point; pub use byte_range::*; pub use closed_captions::*; -pub use decimal_floating_point::*; +pub(crate) use decimal_floating_point::*; pub(crate) use decimal_resolution::*; pub use encryption_method::*; pub use hdcp_level::*; From b932cef71a0adb84589d3ed0b5a50a02824f4054 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 19:01:56 +0200 Subject: [PATCH 22/25] internalize signed_decimal_floating_point #9 --- src/tags/shared/start.rs | 28 ++++++--- src/types/hexadecimal_sequence.rs | 72 ---------------------- src/types/mod.rs | 4 +- src/types/playlist_type.rs | 1 - src/types/protocol_version.rs | 38 +++++++----- src/types/signed_decimal_floating_point.rs | 2 +- 6 files changed, 43 insertions(+), 102 deletions(-) delete mode 100644 src/types/hexadecimal_sequence.rs delete mode 100644 src/types/playlist_type.rs diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 06b6d90..a4c594a 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -19,24 +19,36 @@ impl ExtXStart { pub(crate) const PREFIX: &'static str = "#EXT-X-START:"; /// Makes a new `ExtXStart` tag. - pub const fn new(time_offset: SignedDecimalFloatingPoint) -> Self { + /// # Panic + /// Panics if the time_offset value is infinite. + pub fn new(time_offset: f64) -> Self { + if time_offset.is_infinite() { + panic!("EXT-X-START: Floating point value must be finite!"); + } + ExtXStart { - time_offset, + time_offset: SignedDecimalFloatingPoint::new(time_offset).unwrap(), precise: false, } } /// Makes a new `ExtXStart` tag with the given `precise` flag. - pub const fn with_precise(time_offset: SignedDecimalFloatingPoint, precise: bool) -> Self { + /// # Panic + /// Panics if the time_offset value is infinite. + pub fn with_precise(time_offset: f64, precise: bool) -> Self { + if time_offset.is_infinite() { + panic!("EXT-X-START: Floating point value must be finite!"); + } + ExtXStart { - time_offset, + time_offset: SignedDecimalFloatingPoint::new(time_offset).unwrap(), precise, } } /// Returns the time offset of the media segments in the playlist. - pub const fn time_offset(&self) -> SignedDecimalFloatingPoint { - self.time_offset + pub const fn time_offset(&self) -> f64 { + self.time_offset.as_f64() } /// Returns whether clients should not render media stream whose presentation times are @@ -97,13 +109,13 @@ mod test { #[test] fn ext_x_start() { - let tag = ExtXStart::new(SignedDecimalFloatingPoint::new(-1.23).unwrap()); + let tag = ExtXStart::new(-1.23); let text = "#EXT-X-START:TIME-OFFSET=-1.23"; assert_eq!(text.parse().ok(), Some(tag)); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V1); - let tag = ExtXStart::with_precise(SignedDecimalFloatingPoint::new(1.23).unwrap(), true); + let tag = ExtXStart::with_precise(1.23, true); let text = "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES"; assert_eq!(text.parse().ok(), Some(tag)); assert_eq!(tag.to_string(), text); diff --git a/src/types/hexadecimal_sequence.rs b/src/types/hexadecimal_sequence.rs deleted file mode 100644 index fd6d69e..0000000 --- a/src/types/hexadecimal_sequence.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::fmt; -use std::ops::Deref; -use std::str::FromStr; - -use crate::Error; - -/// Hexadecimal sequence. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct HexadecimalSequence(Vec<u8>); - -impl HexadecimalSequence { - /// Makes a new `HexadecimalSequence` instance. - pub fn new<T: Into<Vec<u8>>>(v: T) -> Self { - HexadecimalSequence(v.into()) - } - - /// Converts into the underlying byte sequence. - pub fn into_bytes(self) -> Vec<u8> { - self.0 - } -} - -impl Deref for HexadecimalSequence { - type Target = [u8]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef<[u8]> for HexadecimalSequence { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl fmt::Display for HexadecimalSequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "0x")?; - for b in &self.0 { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} - -impl FromStr for HexadecimalSequence { - type Err = Error; - - fn from_str(input: &str) -> Result<Self, Self::Err> { - if !(input.starts_with("0x") || input.starts_with("0X")) { - return Err(Error::invalid_input()); - } - - if input.len() % 2 != 0 { - return Err(Error::invalid_input()); - } - - let mut result = Vec::with_capacity(input.len() / 2 - 1); - - for c in input.as_bytes().chunks(2).skip(1) { - let d = String::from_utf8(c.to_vec()).map_err(|e| Error::custom(e))?; - let b = u8::from_str_radix(d.as_str(), 16)?; - result.push(b); - } - - Ok(HexadecimalSequence(result)) - } -} diff --git a/src/types/mod.rs b/src/types/mod.rs index 10641d3..ea47eaa 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -5,7 +5,6 @@ mod decimal_floating_point; mod decimal_resolution; mod encryption_method; mod hdcp_level; -mod hexadecimal_sequence; mod in_stream_id; mod initialization_vector; mod media_type; @@ -18,9 +17,8 @@ pub(crate) use decimal_floating_point::*; pub(crate) use decimal_resolution::*; pub use encryption_method::*; pub use hdcp_level::*; -pub use hexadecimal_sequence::*; pub use in_stream_id::*; pub(crate) use initialization_vector::*; pub use media_type::*; pub use protocol_version::*; -pub use signed_decimal_floating_point::*; +pub(crate) use signed_decimal_floating_point::*; diff --git a/src/types/playlist_type.rs b/src/types/playlist_type.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/types/playlist_type.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 626e13e..4211a0a 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -19,14 +19,16 @@ pub enum ProtocolVersion { } impl fmt::Display for ProtocolVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let n = match *self { - ProtocolVersion::V1 => 1, - ProtocolVersion::V2 => 2, - ProtocolVersion::V3 => 3, - ProtocolVersion::V4 => 4, - ProtocolVersion::V5 => 5, - ProtocolVersion::V6 => 6, - ProtocolVersion::V7 => 7, + let n = { + match &self { + ProtocolVersion::V1 => 1, + ProtocolVersion::V2 => 2, + ProtocolVersion::V3 => 3, + ProtocolVersion::V4 => 4, + ProtocolVersion::V5 => 5, + ProtocolVersion::V6 => 6, + ProtocolVersion::V7 => 7, + } }; write!(f, "{}", n) } @@ -35,15 +37,17 @@ impl FromStr for ProtocolVersion { type Err = Error; fn from_str(input: &str) -> Result<Self, Self::Err> { - Ok(match input { - "1" => ProtocolVersion::V1, - "2" => ProtocolVersion::V2, - "3" => ProtocolVersion::V3, - "4" => ProtocolVersion::V4, - "5" => ProtocolVersion::V5, - "6" => ProtocolVersion::V6, - "7" => ProtocolVersion::V7, - _ => return Err(Error::unknown_protocol_version(input)), + Ok({ + match input { + "1" => ProtocolVersion::V1, + "2" => ProtocolVersion::V2, + "3" => ProtocolVersion::V3, + "4" => ProtocolVersion::V4, + "5" => ProtocolVersion::V5, + "6" => ProtocolVersion::V6, + "7" => ProtocolVersion::V7, + _ => return Err(Error::unknown_protocol_version(input)), + } }) } } diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 0c3691c..4495c93 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -9,7 +9,7 @@ use crate::Error; /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct SignedDecimalFloatingPoint(f64); +pub(crate) struct SignedDecimalFloatingPoint(f64); impl SignedDecimalFloatingPoint { /// Makes a new `SignedDecimalFloatingPoint` instance. From 5486c5e830bdcf7363ab8123349884997274f41d Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 19:09:48 +0200 Subject: [PATCH 23/25] cleanup imports --- src/types/closed_captions.rs | 5 +++-- src/types/initialization_vector.rs | 4 ++-- src/types/media_type.rs | 2 +- src/types/protocol_version.rs | 2 +- src/types/signed_decimal_floating_point.rs | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index a391703..ad016f1 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -1,7 +1,8 @@ +use std::fmt; +use std::str::FromStr; + use crate::utils::{quote, unquote}; use crate::{Error, Result}; -use std::fmt; -use std::str::{self, FromStr}; /// The identifier of a closed captions group or its absence. /// diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 2b66bd2..447e8c8 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -1,6 +1,6 @@ use std::fmt; use std::ops::Deref; -use std::str::{self, FromStr}; +use std::str::FromStr; use crate::Error; @@ -60,7 +60,7 @@ impl FromStr for InitializationVector { let mut v = [0; 16]; for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() { - let d = str::from_utf8(c).map_err(|e| Error::custom(e))?; + let d = std::str::from_utf8(c).map_err(|e| Error::custom(e))?; let b = u8::from_str_radix(d, 16).map_err(|e| Error::custom(e))?; v[i] = b; } diff --git a/src/types/media_type.rs b/src/types/media_type.rs index 323dd43..5bc10d4 100644 --- a/src/types/media_type.rs +++ b/src/types/media_type.rs @@ -1,5 +1,5 @@ use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; use crate::Error; diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 4211a0a..40092ed 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -1,5 +1,5 @@ use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; use crate::Error; diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 4495c93..a10d893 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -1,5 +1,5 @@ use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; use crate::Error; From e55113e75226d47548c21bd0cd89c77d0caab454 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Sep 2019 14:45:10 +0200 Subject: [PATCH 24/25] readded decryption_key --- src/tags/master_playlist/session_key.rs | 338 ++--------------- src/tags/media_segment/key.rs | 429 ++-------------------- src/types/decryption_key.rs | 465 ++++++++++++++++++++++++ src/types/initialization_vector.rs | 2 +- src/types/mod.rs | 4 +- 5 files changed, 523 insertions(+), 715 deletions(-) create mode 100644 src/types/decryption_key.rs diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index ac19110..0d7d275 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,24 +1,18 @@ use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; use url::Url; -use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; -use crate::utils::{quote, tag, unquote}; +use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; +use crate::utils::tag; use crate::Error; /// [4.3.4.5. EXT-X-SESSION-KEY] /// /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXSessionKey { - method: EncryptionMethod, - uri: Option<Url>, - iv: Option<InitializationVector>, - key_format: Option<String>, - key_format_versions: Option<String>, -} +pub struct ExtXSessionKey(DecryptionKey); impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; @@ -31,266 +25,7 @@ impl ExtXSessionKey { panic!("The EncryptionMethod is not allowed to be None"); } - Self { - method, - uri: Some(uri), - iv: None, - key_format: None, - key_format_versions: None, - } - } - - /// Returns the [EncryptionMethod]. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// assert_eq!( - /// key.method(), - /// EncryptionMethod::Aes128 - /// ); - /// ``` - pub const fn method(&self) -> EncryptionMethod { - self.method - } - - /// Sets the [EncryptionMethod]. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_method(EncryptionMethod::SampleAes); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() - /// ); - /// ``` - pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self { - self.method = value; - self - } - - /// Returns an `URI` that specifies how to obtain the key. - /// - /// This attribute is required, if the [EncryptionMethod] is not None. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// assert_eq!( - /// key.uri(), - /// &Some("https://www.example.com".parse().unwrap()) - /// ); - /// ``` - pub const fn uri(&self) -> &Option<Url> { - &self.uri - } - - /// Sets the `URI` attribute. - /// - /// This attribute is required, if the [EncryptionMethod] is not None. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_uri("http://www.google.com".parse().unwrap()); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() - /// ); - /// ``` - pub fn set_uri(&mut self, value: Url) -> &mut Self { - self.uri = Some(value); - self - } - - /// Returns the IV (Initialization Vector) attribute. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_iv([ - /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 - /// ]); - /// - /// assert_eq!( - /// key.iv(), - /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - /// ); - /// ``` - pub fn iv(&self) -> Option<[u8; 16]> { - if let Some(iv) = &self.iv { - Some(iv.to_slice()) - } else { - None - } - } - - /// Sets the `IV` attribute. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_iv([ - /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 - /// ]); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() - /// ); - /// ``` - pub fn set_iv<T>(&mut self, value: T) -> &mut Self - where - T: Into<[u8; 16]>, - { - self.iv = Some(InitializationVector(value.into())); - self - } - - /// Returns a string that specifies how the key is - /// represented in the resource identified by the URI. - /// - //// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_key_format("key_format_attribute"); - /// - /// assert_eq!( - /// key.key_format(), - /// &Some("key_format_attribute".to_string()) - /// ); - /// ``` - pub const fn key_format(&self) -> &Option<String> { - &self.key_format - } - - /// Sets the `KEYFORMAT` attribute. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_key_format("key_format_attribute"); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() - /// ); - /// ``` - pub fn set_key_format<T: ToString>(&mut self, value: T) -> &mut Self { - self.key_format = Some(value.to_string()); - self - } - - /// Returns a string containing one or more positive - /// integers separated by the "/" character (for example, "1", "1/2", - /// or "1/2/5"). If more than one version of a particular `KEYFORMAT` - /// is defined, this attribute can be used to indicate which - /// version(s) this instance complies with. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_key_format_versions("1/2/3/4/5"); - /// - /// assert_eq!( - /// key.key_format_versions(), - /// &Some("1/2/3/4/5".to_string()) - /// ); - /// ``` - pub const fn key_format_versions(&self) -> &Option<String> { - &self.key_format_versions - } - - /// Sets the `KEYFORMATVERSIONS` attribute. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXSessionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_key_format_versions("1/2/3/4/5"); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() - /// ); - /// ``` - pub fn set_key_format_versions<T: ToString>(&mut self, value: T) -> &mut Self { - self.key_format_versions = Some(value.to_string()); - self + Self(DecryptionKey::new(method, uri)) } /// Returns the protocol compatibility version that this tag requires. @@ -310,9 +45,9 @@ impl ExtXSessionKey { /// ); /// ``` pub fn requires_version(&self) -> ProtocolVersion { - if self.key_format.is_some() | self.key_format_versions.is_some() { + if self.0.key_format.is_some() | self.0.key_format_versions.is_some() { ProtocolVersion::V5 - } else if self.iv.is_some() { + } else if self.0.iv.is_some() { ProtocolVersion::V2 } else { ProtocolVersion::V1 @@ -322,20 +57,7 @@ impl ExtXSessionKey { impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}METHOD={}", Self::PREFIX, self.method)?; - if let Some(uri) = &self.uri { - write!(f, ",URI={}", quote(uri))?; - } - if let Some(value) = &self.iv { - write!(f, ",IV={}", value)?; - } - if let Some(value) = &self.key_format { - write!(f, ",KEYFORMAT={}", quote(value))?; - } - if let Some(value) = &self.key_format_versions { - write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; - } - Ok(()) + write!(f, "{}{}", Self::PREFIX, self.0) } } @@ -344,39 +66,21 @@ impl FromStr for ExtXSessionKey { fn from_str(input: &str) -> Result<Self, Self::Err> { let input = tag(input, Self::PREFIX)?; - let mut method = None; - let mut uri = None; - let mut iv = None; - let mut key_format = None; - let mut key_format_versions = None; + Ok(Self(input.parse()?)) + } +} - for (key, value) in input.parse::<AttributePairs>()? { - match key.as_str() { - "METHOD" => method = Some((value.parse())?), - "URI" => uri = Some(unquote(value).parse()?), - "IV" => iv = Some((value.parse())?), - "KEYFORMAT" => key_format = Some(unquote(value)), - "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } +impl Deref for ExtXSessionKey { + type Target = DecryptionKey; - let method = method.ok_or(Error::missing_value("EXT-X-METHOD"))?; - if method == EncryptionMethod::None { - return Err(Error::custom( - "EXT-X-SESSION-KEY: Encryption Method must not be NONE", - )); - } - Ok(ExtXSessionKey { - method, - uri, - iv, - key_format, - key_format_versions, - }) + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ExtXSessionKey { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index bfa7df9..64abf6d 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,11 +1,11 @@ use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; use url::Url; -use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; -use crate::utils::{quote, tag, unquote}; +use crate::types::{DecryptionKey, EncryptionMethod}; +use crate::utils::tag; use crate::Error; /// [4.3.2.4. EXT-X-KEY] @@ -14,13 +14,7 @@ use crate::Error; /// # Note /// In case of an empty key (`EncryptionMethod::None`), all attributes will be ignored. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXKey { - method: EncryptionMethod, - uri: Option<Url>, - iv: Option<InitializationVector>, - key_format: Option<String>, - key_format_versions: Option<String>, -} +pub struct ExtXKey(DecryptionKey); impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; @@ -44,13 +38,7 @@ impl ExtXKey { /// ); /// ``` pub const fn new(method: EncryptionMethod, uri: Url) -> Self { - Self { - method, - uri: Some(uri), - iv: None, - key_format: None, - key_format_versions: None, - } + Self(DecryptionKey::new(method, uri)) } /// Makes a new `ExtXKey` tag without a decryption key. @@ -66,16 +54,16 @@ impl ExtXKey { /// ); /// ``` pub const fn empty() -> Self { - Self { + Self(DecryptionKey { method: EncryptionMethod::None, uri: None, iv: None, key_format: None, key_format_versions: None, - } + }) } - /// Returns whether the EncryptionMethod is None. + /// Returns whether the [EncryptionMethod] is [None](EncryptionMethod::None). /// # Example /// ``` /// use hls_m3u8::tags::ExtXKey; @@ -89,313 +77,7 @@ impl ExtXKey { /// ); /// ``` pub fn is_empty(&self) -> bool { - if self.method == EncryptionMethod::None { - true - } else { - false - } - } - - /// Returns the [EncryptionMethod]. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// assert_eq!( - /// key.method(), - /// EncryptionMethod::Aes128 - /// ); - /// ``` - pub const fn method(&self) -> EncryptionMethod { - self.method - } - - /// Sets the [EncryptionMethod]. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_method(EncryptionMethod::SampleAes); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() - /// ); - /// ``` - pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self { - self.method = value; - self - } - - /// Returns an `URI` that specifies how to obtain the key. - /// - /// This attribute is required, if the [EncryptionMethod] is not None. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// assert_eq!( - /// key.uri(), - /// &Some("https://www.example.com".parse().unwrap()) - /// ); - /// ``` - pub const fn uri(&self) -> &Option<Url> { - &self.uri - } - - /// Sets the `URI` attribute. - /// - /// This attribute is required, if the [EncryptionMethod] is not None. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_uri("http://www.google.com".parse().unwrap()); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-KEY:METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() - /// ); - /// ``` - pub fn set_uri(&mut self, value: Url) -> &mut Self { - self.uri = Some(value); - self - } - - /// Returns the IV (Initialization Vector) attribute. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_iv([ - /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 - /// ]); - /// - /// assert_eq!( - /// key.iv(), - /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - /// ); - /// ``` - pub fn iv(&self) -> Option<[u8; 16]> { - if let Some(iv) = &self.iv { - Some(iv.to_slice()) - } else { - None - } - } - - /// Sets the `IV` attribute. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_iv([ - /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 - /// ]); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() - /// ); - /// ``` - pub fn set_iv<T>(&mut self, value: T) -> &mut Self - where - T: Into<[u8; 16]>, - { - self.iv = Some(InitializationVector(value.into())); - self - } - - /// Returns a string that specifies how the key is - /// represented in the resource identified by the URI. - /// - //// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_key_format("key_format_attribute"); - /// - /// assert_eq!( - /// key.key_format(), - /// &Some("key_format_attribute".to_string()) - /// ); - /// ``` - pub const fn key_format(&self) -> &Option<String> { - &self.key_format - } - - /// Sets the `KEYFORMAT` attribute. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_key_format("key_format_attribute"); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() - /// ); - /// ``` - pub fn set_key_format<T: ToString>(&mut self, value: T) -> &mut Self { - self.key_format = Some(value.to_string()); - self - } - - /// Returns a string containing one or more positive - /// integers separated by the "/" character (for example, "1", "1/2", - /// or "1/2/5"). If more than one version of a particular `KEYFORMAT` - /// is defined, this attribute can be used to indicate which - /// version(s) this instance complies with. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_key_format_versions("1/2/3/4/5"); - /// - /// assert_eq!( - /// key.key_format_versions(), - /// &Some("1/2/3/4/5".to_string()) - /// ); - /// ``` - pub const fn key_format_versions(&self) -> &Option<String> { - &self.key_format_versions - } - - /// Sets the `KEYFORMATVERSIONS` attribute. - /// - /// This attribute is optional. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// key.set_key_format_versions("1/2/3/4/5"); - /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() - /// ); - /// ``` - pub fn set_key_format_versions<T: ToString>(&mut self, value: T) -> &mut Self { - self.key_format_versions = Some(value.to_string()); - self - } - - /// Returns the protocol compatibility version that this tag requires. - /// # Example - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion}; - /// - /// let mut key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com".parse().unwrap() - /// ); - /// - /// assert_eq!( - /// key.requires_version(), - /// ProtocolVersion::V1 - /// ); - /// ``` - pub fn requires_version(&self) -> ProtocolVersion { - if self.key_format.is_some() | self.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } - } -} - -impl fmt::Display for ExtXKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}METHOD={}", Self::PREFIX, self.method)?; - - if self.method == EncryptionMethod::None { - return Ok(()); - } - if let Some(uri) = &self.uri { - write!(f, ",URI={}", quote(uri))?; - } - if let Some(value) = &self.iv { - write!(f, ",IV={}", value)?; - } - if let Some(value) = &self.key_format { - write!(f, ",KEYFORMAT={}", quote(value))?; - } - if let Some(value) = &self.key_format_versions { - write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; - } - Ok(()) + self.0.method() == EncryptionMethod::None } } @@ -404,38 +86,27 @@ impl FromStr for ExtXKey { fn from_str(input: &str) -> Result<Self, Self::Err> { let input = tag(input, Self::PREFIX)?; - let mut method = None; - let mut uri = None; - let mut iv = None; - let mut key_format = None; - let mut key_format_versions = None; + Ok(Self(input.parse()?)) + } +} - for (key, value) in input.parse::<AttributePairs>()? { - match key.as_str() { - "METHOD" => method = Some((value.parse())?), - "URI" => uri = Some(unquote(value).parse()?), - "IV" => iv = Some((value.parse())?), - "KEYFORMAT" => key_format = Some(unquote(value)), - "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } +impl fmt::Display for ExtXKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.0) + } +} - let method = method.ok_or(Error::missing_value("EXT-X-KEY:METHOD"))?; - if method != EncryptionMethod::None && uri.is_none() { - return Err(Error::missing_value("EXT-X-KEY:URI")); - } +impl Deref for ExtXKey { + type Target = DecryptionKey; - Ok(ExtXKey { - method, - uri, - iv, - key_format, - key_format_versions, - }) + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ExtXKey { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } @@ -451,26 +122,16 @@ mod test { "#EXT-X-KEY:METHOD=NONE".to_string() ); - assert_eq!( - ExtXKey::empty().set_key_format("hi").to_string(), - "#EXT-X-KEY:METHOD=NONE".to_string() - ); - - let mut key = ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin".parse().unwrap(), - ); + let mut key = ExtXKey::empty(); + // it is expected, that all attributes will be ignored in an empty key! + key.set_key_format("hi"); key.set_iv([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ]); + key.set_uri("https://www.example.com".parse().unwrap()); + key.set_key_format_versions("1/2/3"); - assert_eq!( - key.to_string(), - "#EXT-X-KEY:METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52" - .to_string() - ); + assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); } #[test] @@ -492,29 +153,5 @@ mod test { key.set_iv([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ]); - - assert_eq!( - "#EXT-X-KEY:METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0X10ef8f758ca555115584bb5b3c687f52" - .parse::<ExtXKey>() - .unwrap(), - key - ); - - let mut key = ExtXKey::new( - EncryptionMethod::Aes128, - "http://www.example.com".parse().unwrap(), - ); - key.set_iv([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ]) - .set_key_format("baz"); - - assert_eq!( - r#"#EXT-X-KEY:METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# - .parse::<ExtXKey>().unwrap(), - key - ) } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs new file mode 100644 index 0000000..7e82ff9 --- /dev/null +++ b/src/types/decryption_key.rs @@ -0,0 +1,465 @@ +use std::fmt; +use std::str::FromStr; + +use derive_builder::Builder; +use url::Url; + +use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; +use crate::utils::{quote, unquote}; +use crate::Error; + +#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] +#[builder(setter(into))] +pub struct DecryptionKey { + pub(crate) method: EncryptionMethod, + #[builder(setter(into, strip_option), default)] + pub(crate) uri: Option<Url>, + #[builder(setter(into, strip_option), default)] + pub(crate) iv: Option<InitializationVector>, + #[builder(setter(into, strip_option), default)] + pub(crate) key_format: Option<String>, + #[builder(setter(into, strip_option), default)] + pub(crate) key_format_versions: Option<String>, +} + +impl DecryptionKey { + /// Makes a new `DecryptionKey`. + /// # Example + /// ``` + /// use url::Url; + /// + /// use hls_m3u8::types::{EncryptionMethod, DecryptionKey}; + /// + /// let key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"https://www.example.com/\"" + /// ); + /// ``` + pub const fn new(method: EncryptionMethod, uri: Url) -> Self { + Self { + method, + uri: Some(uri), + iv: None, + key_format: None, + key_format_versions: None, + } + } + + /// Returns the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.method(), + /// EncryptionMethod::Aes128 + /// ); + /// ``` + pub const fn method(&self) -> EncryptionMethod { + self.method + } + + /// Returns a Builder to build a `DecryptionKey`. + pub fn builder() -> DecryptionKeyBuilder { + DecryptionKeyBuilder::default() + } + + /// Sets the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_method(EncryptionMethod::SampleAes); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() + /// ); + /// ``` + pub fn set_method(&mut self, value: EncryptionMethod) { + self.method = value; + } + + /// Returns an `URI` that specifies how to obtain the key. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.uri(), + /// &Some("https://www.example.com".parse().unwrap()) + /// ); + /// ``` + pub const fn uri(&self) -> &Option<Url> { + &self.uri + } + + /// Sets the `URI` attribute. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_uri("http://www.google.com".parse().unwrap()); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() + /// ); + /// ``` + pub fn set_uri(&mut self, value: Url) { + self.uri = Some(value); + } + + /// Returns the IV (Initialization Vector) attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 + /// ]); + /// + /// assert_eq!( + /// key.iv(), + /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + /// ); + /// ``` + pub fn iv(&self) -> Option<[u8; 16]> { + if let Some(iv) = &self.iv { + Some(iv.to_slice()) + } else { + None + } + } + + /// Sets the `IV` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 + /// ]); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() + /// ); + /// ``` + pub fn set_iv<T>(&mut self, value: T) + where + T: Into<[u8; 16]>, + { + self.iv = Some(InitializationVector(value.into())); + } + + /// Returns a string that specifies how the key is + /// represented in the resource identified by the URI. + /// + //// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.key_format(), + /// &Some("key_format_attribute".to_string()) + /// ); + /// ``` + pub const fn key_format(&self) -> &Option<String> { + &self.key_format + } + + /// Sets the `KEYFORMAT` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() + /// ); + /// ``` + pub fn set_key_format<T: ToString>(&mut self, value: T) { + self.key_format = Some(value.to_string()); + } + + /// Returns a string containing one or more positive + /// integers separated by the "/" character (for example, "1", "1/2", + /// or "1/2/5"). If more than one version of a particular `KEYFORMAT` + /// is defined, this attribute can be used to indicate which + /// version(s) this instance complies with. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.key_format_versions(), + /// &Some("1/2/3/4/5".to_string()) + /// ); + /// ``` + pub const fn key_format_versions(&self) -> &Option<String> { + &self.key_format_versions + } + + /// Sets the `KEYFORMATVERSIONS` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() + /// ); + /// ``` + pub fn set_key_format_versions<T: ToString>(&mut self, value: T) { + self.key_format_versions = Some(value.to_string()); + } + + /// Returns the protocol compatibility version that this tag requires. + /// # Example + /// ``` + /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion, DecryptionKey}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.requires_version(), + /// ProtocolVersion::V1 + /// ); + /// ``` + pub fn requires_version(&self) -> ProtocolVersion { + if self.key_format.is_some() || self.key_format_versions.is_some() { + ProtocolVersion::V5 + } else if self.iv.is_some() { + ProtocolVersion::V2 + } else { + ProtocolVersion::V1 + } + } +} + +impl FromStr for DecryptionKey { + type Err = Error; + + fn from_str(input: &str) -> Result<Self, Self::Err> { + let mut method = None; + let mut uri = None; + let mut iv = None; + let mut key_format = None; + let mut key_format_versions = None; + + for (key, value) in input.parse::<AttributePairs>()? { + match key.as_str() { + "METHOD" => method = Some((value.parse())?), + "URI" => uri = Some(unquote(value).parse()?), + "IV" => iv = Some((value.parse())?), + "KEYFORMAT" => key_format = Some(unquote(value)), + "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + + let method = method.ok_or(Error::missing_value("METHOD"))?; + if method != EncryptionMethod::None && uri.is_none() { + return Err(Error::missing_value("URI")); + } + + Ok(DecryptionKey { + method, + uri, + iv, + key_format, + key_format_versions, + }) + } +} + +impl fmt::Display for DecryptionKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "METHOD={}", self.method)?; + + if self.method == EncryptionMethod::None { + return Ok(()); + } + if let Some(uri) = &self.uri { + write!(f, ",URI={}", quote(uri))?; + } + if let Some(value) = &self.iv { + write!(f, ",IV={}", value)?; + } + if let Some(value) = &self.key_format { + write!(f, ",KEYFORMAT={}", quote(value))?; + } + if let Some(value) = &self.key_format_versions { + write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::EncryptionMethod; + + #[test] + fn test_requires_version() { + let key = DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com".parse::<Url>().unwrap()) + .iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]) + .build() + .unwrap(); + } + + #[test] + fn test_display() { + let mut key = DecryptionKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); + + assert_eq!( + key.to_string(), + "METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52" + .to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!( + r#"METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + .parse::<DecryptionKey>() + .unwrap(), + DecryptionKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52".parse().unwrap() + ) + ); + + let mut key = DecryptionKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); + + assert_eq!( + "METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0X10ef8f758ca555115584bb5b3c687f52" + .parse::<DecryptionKey>() + .unwrap(), + key + ); + + let mut key = DecryptionKey::new( + EncryptionMethod::Aes128, + "http://www.example.com".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); + key.set_key_format("baz"); + + assert_eq!( + r#"METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# + .parse::<DecryptionKey>().unwrap(), + key + ) + } +} diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 447e8c8..8b008a7 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -10,7 +10,7 @@ use crate::Error; /// /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct InitializationVector(pub [u8; 16]); +pub struct InitializationVector(pub [u8; 16]); impl InitializationVector { pub const fn to_slice(&self) -> [u8; 16] { diff --git a/src/types/mod.rs b/src/types/mod.rs index ea47eaa..6301ba4 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,6 +3,7 @@ mod byte_range; mod closed_captions; mod decimal_floating_point; mod decimal_resolution; +mod decryption_key; mod encryption_method; mod hdcp_level; mod in_stream_id; @@ -15,10 +16,11 @@ pub use byte_range::*; pub use closed_captions::*; pub(crate) use decimal_floating_point::*; pub(crate) use decimal_resolution::*; +pub use decryption_key::*; pub use encryption_method::*; pub use hdcp_level::*; pub use in_stream_id::*; -pub(crate) use initialization_vector::*; +pub use initialization_vector::*; pub use media_type::*; pub use protocol_version::*; pub(crate) use signed_decimal_floating_point::*; From 612c3d15be2b300978d596735edc8f033144410c Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Tue, 17 Sep 2019 15:40:10 +0200 Subject: [PATCH 25/25] minor protocol_version fixes --- src/master_playlist.rs | 2 +- src/media_playlist.rs | 2 +- src/types/initialization_vector.rs | 1 + src/types/protocol_version.rs | 9 +++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/master_playlist.rs b/src/master_playlist.rs index ec11a86..418cdb6 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -158,7 +158,7 @@ impl MasterPlaylistBuilder { .flatten(), ) .max() - .unwrap_or(ProtocolVersion::V7) + .unwrap_or(ProtocolVersion::latest()) } fn validate_stream_inf_tags(&self) -> crate::Result<()> { diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 9d9778e..3f673c0 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -200,7 +200,7 @@ impl MediaPlaylistBuilder { .unwrap_or(ProtocolVersion::V1) })) .max() - .unwrap_or(ProtocolVersion::V1) + .unwrap_or(ProtocolVersion::latest()) } /// Adds a media segment to the resulting playlist. diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 8b008a7..7c64323 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -13,6 +13,7 @@ use crate::Error; pub struct InitializationVector(pub [u8; 16]); impl InitializationVector { + /// Converts the initialization vector to a slice. pub const fn to_slice(&self) -> [u8; 16] { self.0 } diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 40092ed..1d54938 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -17,6 +17,14 @@ pub enum ProtocolVersion { V6, V7, } + +impl ProtocolVersion { + /// Returns the newest ProtocolVersion, that is supported by this library. + pub const fn latest() -> Self { + Self::V7 + } +} + impl fmt::Display for ProtocolVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let n = { @@ -33,6 +41,7 @@ impl fmt::Display for ProtocolVersion { write!(f, "{}", n) } } + impl FromStr for ProtocolVersion { type Err = Error;