diff --git a/.travis.yml b/.travis.yml index de777e1..10db05e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,15 @@ language: rust -# before_cache: | -# if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then -# cargo install cargo-tarpaulin -f -# fi +cache: cargo before_cache: | - bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh) + if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then + cargo install cargo-tarpaulin + fi + +before_cache: + - rm -rf /home/travis/.cargo/registry -cache: cargo rust: - stable @@ -26,7 +27,7 @@ script: after_success: | if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then # Uncomment the following line for coveralls.io - cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID + cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID --run-types Tests Doctests # Uncomment the following two lines create and upload a report for codecov.io cargo tarpaulin --out Xml diff --git a/Cargo.toml b/Cargo.toml index 22beadd..2d8fae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,15 @@ edition = "2018" categories = ["parser"] [badges] -travis-ci = {repository = "sile/hls_m3u8"} -codecov = {repository = "sile/hls_m3u8"} +travis-ci = { repository = "sile/hls_m3u8" } +codecov = { repository = "sile/hls_m3u8" } [dependencies] failure = "0.1.5" derive_builder = "0.7.2" chrono = "0.4.9" +strum = { version = "0.16.0", features = ["derive"] } +derive_more = "0.15.0" [dev-dependencies] clap = "2" diff --git a/src/attribute.rs b/src/attribute.rs index 510ecce..98db989 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -55,11 +55,14 @@ impl FromStr for AttributePairs { let pair = split(line.trim(), '='); if pair.len() < 2 { - return Err(Error::invalid_input()); + continue; } - let key = pair[0].to_uppercase(); - let value = pair[1].to_string(); + let key = pair[0].trim().to_uppercase(); + let value = pair[1].trim().to_string(); + if value.is_empty() { + continue; + } result.insert(key.trim().to_string(), value.trim().to_string()); } @@ -123,7 +126,10 @@ mod test { let mut iterator = pairs.iter(); assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3")); - assert!("FOO=BAR,VAL".parse::().is_err()); + let mut pairs = AttributePairs::new(); + pairs.insert("FOO".to_string(), "BAR".to_string()); + + assert_eq!("FOO=BAR,VAL".parse::().unwrap(), pairs); } #[test] diff --git a/src/error.rs b/src/error.rs index 14c43f4..cd13c38 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,3 @@ -use std::error; use std::fmt; use failure::{Backtrace, Context, Fail}; @@ -118,13 +117,6 @@ impl From> for Error { } impl Error { - 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())) } @@ -218,3 +210,9 @@ impl From<::chrono::ParseError> for Error { Error::chrono(value) } } + +impl From<::strum::ParseError> for Error { + fn from(value: ::strum::ParseError) -> Self { + Error::custom(value) // TODO! + } +} diff --git a/src/lib.rs b/src/lib.rs index 75e8bd3..54a0964 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,17 @@ +#![forbid(unsafe_code)] #![warn( //clippy::pedantic, clippy::nursery, clippy::cargo )] -#![warn(missing_docs)] +#![allow(clippy::multiple_crate_versions)] +#![warn( + missing_docs, + missing_copy_implementations, + missing_debug_implementations, + trivial_casts, // TODO (needed?) + trivial_numeric_casts +)] //! [HLS] m3u8 parser/generator. //! //! [HLS]: https://tools.ietf.org/html/rfc8216 diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 2e38a59..3b162a7 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -21,7 +21,7 @@ use crate::Error; /// [Master Playlist]: crate::MasterPlaylist /// [Media Playlist]: crate::MediaPlaylist /// [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(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXIFrameStreamInf { uri: String, stream_inf: StreamInf, diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index dabb012..03d45ea 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -102,7 +102,7 @@ impl ExtXMedia { /// Makes a new [ExtXMedia] tag. pub fn new(media_type: MediaType, group_id: T, name: T) -> Self { - ExtXMedia { + Self { media_type, uri: None, group_id: group_id.to_string(), @@ -118,72 +118,478 @@ impl ExtXMedia { } } - /// Makes a [ExtXMediaBuilder] for [ExtXMedia]. + /// Returns a builder for [ExtXMedia]. pub fn builder() -> ExtXMediaBuilder { ExtXMediaBuilder::default() } - /// Returns the type of the media associated with this tag. + /// Returns the type of the media, associated with this tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// assert_eq!( + /// ExtXMedia::new(MediaType::Audio, "audio", "name").media_type(), + /// MediaType::Audio + /// ); + /// ``` pub const fn media_type(&self) -> MediaType { self.media_type } + /// Sets the type of the media, associated with this tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// + /// media.set_media_type(MediaType::Video); + /// + /// assert_eq!( + /// media.media_type(), + /// MediaType::Video + /// ); + /// ``` + pub fn set_media_type(&mut self, value: MediaType) -> &mut Self { + self.media_type = value; + self + } + /// Returns the identifier that specifies the group to which the rendition belongs. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// assert_eq!( + /// ExtXMedia::new(MediaType::Audio, "audio", "name").group_id(), + /// &"audio".to_string() + /// ); + /// ``` pub const fn group_id(&self) -> &String { &self.group_id } + /// Sets the identifier that specifies the group, to which the rendition belongs. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// + /// media.set_group_id("video"); + /// + /// assert_eq!( + /// media.group_id(), + /// &"video".to_string() + /// ); + /// ``` + pub fn set_group_id>(&mut self, value: T) -> &mut Self { + self.group_id = value.into(); + self + } + /// Returns a human-readable description of the rendition. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// assert_eq!( + /// ExtXMedia::new(MediaType::Audio, "audio", "name").name(), + /// &"name".to_string() + /// ); + /// ``` pub const fn name(&self) -> &String { &self.name } - /// Returns the URI that identifies the media playlist. - pub fn uri(&self) -> Option<&String> { - self.uri.as_ref() + /// Sets a human-readable description of the rendition. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// + /// media.set_name("new_name"); + /// + /// assert_eq!( + /// media.name(), + /// &"new_name".to_string() + /// ); + /// ``` + pub fn set_name>(&mut self, value: T) -> &mut Self { + self.name = value.into(); + self + } + + /// Returns the `URI`, that identifies the [MediaPlaylist]. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.uri(), &None); + /// + /// media.set_uri(Some("https://www.example.com/")); + /// + /// assert_eq!( + /// media.uri(), + /// &Some("https://www.example.com/".into()) + /// ); + /// ``` + pub const fn uri(&self) -> &Option { + &self.uri + } + + /// Sets the `URI`, that identifies the [MediaPlaylist]. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.uri(), &None); + /// + /// media.set_uri(Some("https://www.example.com/")); + /// + /// assert_eq!( + /// media.uri(), + /// &Some("https://www.example.com/".into()) + /// ); + /// ``` + pub fn set_uri>(&mut self, value: Option) -> &mut Self { + self.uri = value.map(|v| v.into()); + self } /// Returns the name of the primary language used in the rendition. - pub fn language(&self) -> Option<&String> { - self.language.as_ref() + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.language(), &None); + /// + /// media.set_language(Some("english")); + /// + /// assert_eq!( + /// media.language(), + /// &Some("english".into()) + /// ); + /// ``` + pub const fn language(&self) -> &Option { + &self.language + } + + /// Sets the name of the primary language used in the rendition. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.language(), &None); + /// + /// media.set_language(Some("english")); + /// + /// assert_eq!( + /// media.language(), + /// &Some("english".into()) + /// ); + /// ``` + pub fn set_language>(&mut self, value: Option) -> &mut Self { + self.language = value.map(|v| v.into()); + self } /// Returns the name of a language associated with the rendition. - pub fn assoc_language(&self) -> Option<&String> { - self.assoc_language.as_ref() + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.assoc_language(), &None); + /// + /// media.set_assoc_language(Some("spanish")); + /// + /// assert_eq!( + /// media.assoc_language(), + /// &Some("spanish".into()) + /// ); + /// ``` + pub const fn assoc_language(&self) -> &Option { + &self.assoc_language + } + + /// Sets the name of a language associated with the rendition. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.assoc_language(), &None); + /// + /// media.set_assoc_language(Some("spanish")); + /// + /// assert_eq!( + /// media.assoc_language(), + /// &Some("spanish".into()) + /// ); + /// ``` + pub fn set_assoc_language>(&mut self, value: Option) -> &mut Self { + self.assoc_language = value.map(|v| v.into()); + self } /// Returns whether this is the default rendition. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.is_default(), false); + /// + /// media.set_default(true); + /// + /// assert_eq!( + /// media.is_default(), + /// true + /// ); + /// ``` pub const fn is_default(&self) -> bool { self.is_default } + /// Sets the `default` flag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.is_default(), false); + /// + /// media.set_default(true); + /// + /// assert_eq!(media.is_default(), true); + /// ``` + pub fn set_default(&mut self, value: bool) -> &mut Self { + self.is_default = value; + self + } + /// Returns whether the client may choose to /// play this rendition in the absence of explicit user preference. - pub const fn autoselect(&self) -> bool { + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.is_autoselect(), false); + /// + /// media.set_autoselect(true); + /// + /// assert_eq!(media.is_autoselect(), true); + /// ``` + pub const fn is_autoselect(&self) -> bool { self.is_autoselect } + /// Sets the `autoselect` flag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.is_autoselect(), false); + /// + /// media.set_autoselect(true); + /// + /// assert_eq!(media.is_autoselect(), true); + /// ``` + pub fn set_autoselect(&mut self, value: bool) -> &mut Self { + self.is_autoselect = value; + self + } + /// Returns whether the rendition contains content that is considered essential to play. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.is_forced(), false); + /// + /// media.set_forced(true); + /// + /// assert_eq!(media.is_forced(), true); + /// ``` pub const fn is_forced(&self) -> bool { self.is_forced } - /// Returns the identifier that specifies a rendition within the segments in the media playlist. + /// Sets the `forced` flag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.is_forced(), false); + /// + /// media.set_forced(true); + /// + /// assert_eq!(media.is_forced(), true); + /// ``` + pub fn set_forced(&mut self, value: bool) -> &mut Self { + self.is_forced = value; + self + } + + /// Returns the identifier that specifies a rendition within the segments in the + /// [MediaPlaylist]. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::{InStreamId, MediaType}; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.instream_id(), None); + /// + /// media.set_instream_id(Some(InStreamId::Cc1)); + /// + /// assert_eq!(media.instream_id(), Some(InStreamId::Cc1)); + /// ``` pub const fn instream_id(&self) -> Option { self.instream_id } + /// Sets the [InStreamId]. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::{InStreamId, MediaType}; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.instream_id(), None); + /// + /// media.set_instream_id(Some(InStreamId::Cc1)); + /// + /// assert_eq!(media.instream_id(), Some(InStreamId::Cc1)); + /// ``` + pub fn set_instream_id(&mut self, value: Option) -> &mut Self { + self.instream_id = value; + self + } + /// Returns a string that represents uniform type identifiers (UTI). /// /// Each UTI indicates an individual characteristic of the rendition. - pub fn characteristics(&self) -> Option<&String> { - self.characteristics.as_ref() + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.characteristics(), &None); + /// + /// media.set_characteristics(Some("characteristic")); + /// + /// assert_eq!(media.characteristics(), &Some("characteristic".into())); + /// ``` + pub const fn characteristics(&self) -> &Option { + &self.characteristics } - /// Returns a string that represents the parameters of the rendition. - pub fn channels(&self) -> Option<&String> { - self.channels.as_ref() + /// Sets the characteristics. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.characteristics(), &None); + /// + /// media.set_characteristics(Some("characteristic")); + /// + /// assert_eq!(media.characteristics(), &Some("characteristic".into())); + /// ``` + pub fn set_characteristics>(&mut self, value: Option) -> &mut Self { + self.characteristics = value.map(|v| v.into()); + self + } + + /// Returns a [String] that represents the parameters of the rendition. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.channels(), &None); + /// + /// media.set_channels(Some("channel")); + /// + /// assert_eq!(media.channels(), &Some("channel".into())); + /// ``` + pub const fn channels(&self) -> &Option { + &self.channels + } + + /// Sets the channels. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::MediaType; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.characteristics(), &None); + /// + /// media.set_characteristics(Some("characteristic")); + /// + /// assert_eq!(media.characteristics(), &Some("characteristic".into())); + /// ``` + pub fn set_channels>(&mut self, value: Option) -> &mut Self { + self.channels = value.map(|v| v.into()); + self } } @@ -243,7 +649,7 @@ impl FromStr for ExtXMedia { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - let mut builder = ExtXMedia::builder(); + let mut builder = Self::builder(); for (key, value) in input.parse::()? { match key.as_str() { @@ -345,6 +751,162 @@ mod test { .to_string() ); + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("sp") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"sp/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"sp\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + .to_string() + ); + // ---- + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("englo/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"englo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + .to_string() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("frelo/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"frelo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + .to_string() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("es") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("splo/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"splo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"es\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + .to_string() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"eng/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + .to_string() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("fre/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"fre/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + .to_string() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("es") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"sp/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"es\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + .to_string() + ); + // ---- assert_eq!( ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(), "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string() @@ -353,6 +915,208 @@ mod test { #[test] fn test_parser() { + // TODO: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/adding_alternate_media_to_a_playlist + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"eng/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("fre/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"fre/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .language("sp") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"sp/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"sp\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + // ---- + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("englo/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"englo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("frelo/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"frelo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-lo") + .language("es") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("splo/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"splo/prog_index.m3u8\",\ + GROUP-ID=\"audio-lo\",\ + LANGUAGE=\"es\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("eng") + .name("English") + .is_autoselect(true) + .is_default(true) + .uri("eng/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"eng/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("fre") + .name("Français") + .is_autoselect(true) + .is_default(false) + .uri("fre/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"fre/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio-hi") + .language("es") + .name("Espanol") + .is_autoselect(true) + .is_default(false) + .uri("sp/prog_index.m3u8") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"sp/prog_index.m3u8\",\ + GROUP-ID=\"audio-hi\",\ + LANGUAGE=\"es\",\ + NAME=\"Espanol\",\ + AUTOSELECT=YES" + .parse() + .unwrap() + ); + // ---- assert_eq!( ExtXMedia::new(MediaType::Audio, "foo", "bar"), "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"" @@ -363,9 +1127,41 @@ mod test { #[test] fn test_required_version() { + macro_rules! gen_required_version { + ( $( $id:expr => $output:expr, )* ) => { + $( + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::ClosedCaptions) + .group_id("audio") + .name("English") + .instream_id($id) + .build() + .unwrap() + .required_version(), + $output + ); + )* + } + } + + gen_required_version![ + InStreamId::Cc1 => ProtocolVersion::V1, + InStreamId::Cc2 => ProtocolVersion::V1, + InStreamId::Cc3 => ProtocolVersion::V1, + InStreamId::Cc4 => ProtocolVersion::V1, + InStreamId::Service1 => ProtocolVersion::V7, + ]; + assert_eq!( - ExtXMedia::new(MediaType::Audio, "foo", "bar").required_version(), + ExtXMedia::builder() + .media_type(MediaType::Audio) + .group_id("audio") + .name("English") + .build() + .unwrap() + .required_version(), ProtocolVersion::V1 - ) + ); } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 2eb942e..797474f 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -100,9 +100,9 @@ mod test { EncryptionMethod::Aes128, "https://www.example.com/hls-key/key.bin", ); - key.set_iv([ + key.set_iv(Some([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ]); + ])); assert_eq!( key.to_string(), @@ -129,9 +129,9 @@ mod test { EncryptionMethod::Aes128, "https://www.example.com/hls-key/key.bin", ); - key.set_iv([ + key.set_iv(Some([ 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,\ diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 2f45f66..29125d8 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -12,7 +12,7 @@ use crate::Error; /// [4.3.4.2. EXT-X-STREAM-INF] /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(PartialOrd, Debug, Clone, PartialEq, Eq)] pub struct ExtXStreamInf { uri: String, frame_rate: Option, @@ -57,7 +57,7 @@ impl ExtXStreamInf { /// Sets the maximum frame rate for all the video in the variant stream. pub fn set_frame_rate(&mut self, value: Option) -> &mut Self { - self.frame_rate = value.map(|v| v.into()); + self.frame_rate = value.map(|v| (v as u32).into()); self } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index e26196a..fabf7af 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -6,13 +6,13 @@ use std::time::Duration; use chrono::{DateTime, FixedOffset}; use crate::attribute::AttributePairs; -use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion}; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::{quote, tag, unquote}; use crate::Error; -/// [4.3.2.7. EXT-X-DATERANGE] +/// [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 +/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 /// /// TODO: Implement properly #[allow(missing_docs)] @@ -63,6 +63,36 @@ pub struct ExtXDateRange { impl ExtXDateRange { pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; + + /// Makes a new [ExtXDateRange] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDateRange; + /// use chrono::{DateTime, FixedOffset}; + /// use chrono::offset::TimeZone; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// ExtXDateRange::new("id", FixedOffset::east(8 * HOURS_IN_SECS) + /// .ymd(2010, 2, 19) + /// .and_hms_milli(14, 54, 23, 31)); + /// ``` + pub fn new(id: T, start_date: DateTime) -> Self { + Self { + id: id.to_string(), + class: None, + start_date, + end_date: None, + duration: None, + planned_duration: None, + scte35_cmd: None, + scte35_out: None, + scte35_in: None, + end_on_next: false, + client_attributes: BTreeMap::new(), + } + } } impl RequiredVersion for ExtXDateRange { @@ -82,15 +112,11 @@ impl fmt::Display for ExtXDateRange { 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))?; + if let Some(value) = &self.duration { + write!(f, ",DURATION={}", value.as_secs_f64())?; } - if let Some(x) = self.planned_duration { - write!( - f, - ",PLANNED-DURATION={}", - DecimalFloatingPoint::from_duration(x) - )?; + if let Some(value) = &self.planned_duration { + write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?; } if let Some(value) = &self.scte35_cmd { write!(f, ",SCTE35-CMD={}", quote(value))?; @@ -137,12 +163,10 @@ impl FromStr for ExtXDateRange { "START-DATE" => start_date = Some(unquote(value)), "END-DATE" => end_date = Some(unquote(value).parse()?), "DURATION" => { - let seconds: DecimalFloatingPoint = (value.parse())?; - duration = Some(seconds.to_duration()); + duration = Some(Duration::from_secs_f64(value.parse()?)); } "PLANNED-DURATION" => { - let seconds: DecimalFloatingPoint = (value.parse())?; - planned_duration = Some(seconds.to_duration()); + planned_duration = Some(Duration::from_secs_f64(value.parse()?)); } "SCTE35-CMD" => scte35_cmd = Some(unquote(value)), "SCTE35-OUT" => scte35_out = Some(unquote(value)), @@ -164,15 +188,15 @@ impl FromStr for ExtXDateRange { } } - let id = id.ok_or_else(|| Error::missing_value("EXT-X-ID"))?; + let id = id.ok_or_else(|| Error::missing_value("ID"))?; let start_date = start_date - .ok_or_else(|| Error::missing_value("EXT-X-START-DATE"))? + .ok_or_else(|| Error::missing_value("START-DATE"))? .parse()?; if end_on_next && class.is_none() { return Err(Error::invalid_input()); } - Ok(ExtXDateRange { + Ok(Self { id, class, start_date, @@ -191,7 +215,21 @@ impl FromStr for ExtXDateRange { #[cfg(test)] mod test { use super::*; + use chrono::offset::TimeZone; - #[test] // TODO; write some tests - fn it_works() {} + const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + + #[test] + fn test_required_version() { + assert_eq!( + ExtXDateRange::new( + "id", + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31) + ) + .required_version(), + ProtocolVersion::V1 + ); + } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index cc496e8..99c51a5 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, RequiredVersion}; +use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; @@ -39,7 +39,7 @@ impl ExtInf { /// let ext_inf = ExtInf::new(Duration::from_secs(5)); /// ``` pub const fn new(duration: Duration) -> Self { - ExtInf { + Self { duration, title: None, } @@ -55,7 +55,7 @@ impl ExtInf { /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// ``` pub fn with_title(duration: Duration, title: T) -> Self { - ExtInf { + Self { duration, title: Some(title.to_string()), } @@ -170,7 +170,6 @@ impl FromStr for ExtInf { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - dbg!(&input); let tokens = input.splitn(2, ',').collect::>(); if tokens.is_empty() { @@ -180,7 +179,7 @@ impl FromStr for ExtInf { ))); } - let duration = tokens[0].parse::()?.to_duration(); + let duration = Duration::from_secs_f64(tokens[0].parse()?); let title = { if tokens.len() >= 2 { @@ -194,7 +193,7 @@ impl FromStr for ExtInf { } }; - Ok(ExtInf { duration, title }) + Ok(Self { duration, title }) } } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index e81479d..fd987c9 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -2,7 +2,7 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{DecryptionKey, EncryptionMethod, KeyFormatVersions}; +use crate::types::{DecryptionKey, EncryptionMethod}; use crate::utils::tag; use crate::Error; @@ -64,13 +64,13 @@ impl ExtXKey { /// "#EXT-X-KEY:METHOD=NONE" /// ); /// ``` - pub fn empty() -> Self { + pub const fn empty() -> Self { Self(DecryptionKey { method: EncryptionMethod::None, uri: None, iv: None, key_format: None, - key_format_versions: KeyFormatVersions::new(), + key_format_versions: None, }) } @@ -134,13 +134,13 @@ mod test { ); let mut key = ExtXKey::empty(); - // it is expected, that all attributes will be ignored in an empty key! + // it is expected, that all attributes will be ignored for an empty key! key.set_key_format(Some(KeyFormat::Identity)); - key.set_iv([ + key.set_iv(Some([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ]); + ])); key.set_uri(Some("https://www.example.com")); - key.set_key_format_versions(vec![1, 2, 3]); + key.set_key_format_versions(Some(vec![1, 2, 3])); assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); } @@ -163,8 +163,8 @@ mod test { EncryptionMethod::Aes128, "https://www.example.com/hls-key/key.bin", ); - key.set_iv([ + key.set_iv(Some([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ]); + ])); } } diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 7967a8d..8c771cc 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -8,7 +8,7 @@ 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 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct ExtXIndependentSegments; impl ExtXIndependentSegments { diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index df61df7..9f34070 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -9,7 +9,7 @@ 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 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(PartialOrd, Debug, Clone, Copy, PartialEq, Eq)] pub struct ExtXStart { time_offset: SignedDecimalFloatingPoint, precise: bool, diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index 7175d5c..a87fc4e 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -144,23 +144,29 @@ mod tests { #[test] fn test_parser() { - let byte_range = ByteRange { - length: 99999, - start: Some(2), - }; - assert_eq!(byte_range, "99999@2".parse::().unwrap()); + assert_eq!( + ByteRange { + length: 99999, + start: Some(2), + }, + "99999@2".parse::().unwrap() + ); - let byte_range = ByteRange { - length: 99999, - start: Some(2), - }; - assert_eq!(byte_range, "99999@2".parse::().unwrap()); + assert_eq!( + ByteRange { + length: 99999, + start: Some(2), + }, + "99999@2".parse::().unwrap() + ); - let byte_range = ByteRange { - length: 99999, - start: None, - }; - assert_eq!(byte_range, "99999".parse::().unwrap()); + assert_eq!( + ByteRange { + length: 99999, + start: None, + }, + "99999".parse::().unwrap() + ); assert!("".parse::().is_err()); } diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 3ca291b..ceee6f7 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -1,6 +1,7 @@ -use std::fmt; -use std::str::FromStr; -use std::time::Duration; +use core::ops::Deref; +use core::str::FromStr; + +use derive_more::Display; use crate::Error; @@ -8,78 +9,71 @@ use crate::Error; /// /// See: [4.2. Attribute Lists] /// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +/// [4.2. Attribute Lists]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.2 +#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display)] pub(crate) struct DecimalFloatingPoint(f64); impl DecimalFloatingPoint { - /// Makes a new `DecimalFloatingPoint` instance. + /// Makes a new [DecimalFloatingPoint] instance. /// /// # Errors /// /// 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) -> crate::Result { - if n.is_sign_negative() || n.is_infinite() { + pub fn new(value: f64) -> crate::Result { + if value.is_sign_negative() || value.is_infinite() { return Err(Error::invalid_input()); } - Ok(Self(n)) + Ok(Self(value)) } - /// Converts `DecimalFloatingPoint` to `f64`. + pub(crate) const fn from_f64_unchecked(value: f64) -> Self { + Self(value) + } + + /// Converts [DecimalFloatingPoint] to [f64]. pub const fn as_f64(self) -> f64 { self.0 } - - pub(crate) fn to_duration(self) -> Duration { - Duration::from_secs_f64(self.0) - } - - pub(crate) fn from_duration(value: Duration) -> Self { - Self::from(value) - } -} - -impl From for DecimalFloatingPoint { - fn from(f: u32) -> Self { - Self(f64::from(f)) - } -} - -impl From for DecimalFloatingPoint { - fn from(value: Duration) -> Self { - Self(value.as_secs_f64()) - } } impl Eq for DecimalFloatingPoint {} -impl fmt::Display for DecimalFloatingPoint { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - +// this trait is implemented manually, so it doesn't construct a [DecimalFloatingPoint], +// with a negative value. impl FromStr for DecimalFloatingPoint { type Err = Error; fn from_str(input: &str) -> Result { - if !input.chars().all(|c| c.is_digit(10) || c == '.') { - return Err(Error::invalid_input()); - } Self::new(input.parse()?) } } +impl Deref for DecimalFloatingPoint { + type Target = f64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl From for DecimalFloatingPoint { fn from(value: f64) -> Self { - Self(value) + let mut result = value; + + // guard against the unlikely case of an infinite value... + if result.is_infinite() { + result = 0.0; + } + + Self(result.abs()) } } impl From for DecimalFloatingPoint { fn from(value: f32) -> Self { - Self(value.into()) + (value as f64).into() } } @@ -87,6 +81,24 @@ impl From for DecimalFloatingPoint { mod tests { use super::*; + macro_rules! test_from { + ( $($input:expr),* ) => { + use ::core::convert::From; + + #[test] + fn test_from() { + $( + assert_eq!( + DecimalFloatingPoint::from($input), + DecimalFloatingPoint::new(1.0).unwrap(), + ); + )* + } + } + } + + test_from![1u8, 1u16, 1u32, 1.0f32, -1.0f32, 1.0f64, -1.0f64]; + #[test] pub fn test_display() { let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap(); @@ -111,6 +123,7 @@ mod tests { ); assert!("1#".parse::().is_err()); + assert!("-1.0".parse::().is_err()); } #[test] @@ -125,28 +138,15 @@ mod tests { } #[test] - fn test_from_duration() { + fn test_from_inf() { assert_eq!( - DecimalFloatingPoint::from_duration(Duration::from_nanos(11_234_500_112_345)), - DecimalFloatingPoint::new(11234.500112345).unwrap() + DecimalFloatingPoint::from(::std::f64::INFINITY), + DecimalFloatingPoint::new(0.0).unwrap() ); } #[test] - fn test_from() { - assert_eq!( - DecimalFloatingPoint::from(1u32), - DecimalFloatingPoint::new(1.0).unwrap() - ); - - assert_eq!( - DecimalFloatingPoint::from(1 as f64), - DecimalFloatingPoint::new(1.0).unwrap() - ); - - assert_eq!( - DecimalFloatingPoint::from(1 as f32), - DecimalFloatingPoint::new(1.0).unwrap() - ); + fn test_deref() { + assert_eq!(DecimalFloatingPoint::from(0.1).floor(), 0.0); } } diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index b15550f..81fde55 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -1,6 +1,7 @@ -use std::fmt; use std::str::FromStr; +use derive_more::Display; + use crate::Error; /// Decimal resolution. @@ -8,7 +9,8 @@ use crate::Error; /// See: [4.2. Attribute Lists] /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)] +#[display(fmt = "{}x{}", width, height)] pub(crate) struct DecimalResolution { width: usize, height: usize, @@ -43,20 +45,10 @@ impl DecimalResolution { } } -impl fmt::Display for DecimalResolution { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}x{}", self.width, self.height) - } -} - -/// [DecimalResolution] can be constructed from a tuple; (width, height). -impl From<(T, U)> for DecimalResolution -where - T: Into, - U: Into, -{ - fn from(value: (T, U)) -> Self { - Self::new(value.0.into(), value.1.into()) +/// [DecimalResolution] can be constructed from a tuple; `(width, height)`. +impl From<(usize, usize)> for DecimalResolution { + fn from(value: (usize, usize)) -> Self { + DecimalResolution::new(value.0, value.1) } } @@ -73,12 +65,9 @@ impl FromStr for DecimalResolution { ))); } - let width = tokens[0]; - let height = tokens[1]; - Ok(Self { - width: width.parse()?, - height: height.parse()?, + width: tokens[0].parse()?, + height: tokens[1].parse()?, }) } } @@ -129,4 +118,12 @@ mod tests { 12 ); } + + #[test] + fn test_from() { + assert_eq!( + DecimalResolution::from((1920, 1080)), + DecimalResolution::new(1920, 1080) + ); + } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 1159461..e821f57 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -12,7 +12,7 @@ use crate::utils::{quote, unquote}; use crate::Error; #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] -#[builder(setter(into))] +#[builder(setter(into), build_fn(validate = "Self::validate"))] /// [DecryptionKey] contains data, that is shared between [ExtXSessionKey] and [ExtXKey]. /// /// [ExtXSessionKey]: crate::tags::ExtXSessionKey @@ -30,9 +30,18 @@ pub struct DecryptionKey { /// A string that specifies how the key is /// represented in the resource identified by the `URI`. pub(crate) key_format: Option, - #[builder(setter(into), default)] - /// The `KEYFORMATVERSIONS` attribute. - pub(crate) key_format_versions: KeyFormatVersions, + #[builder(setter(into, strip_option), default)] + /// The [KeyFormatVersions] attribute. + pub(crate) key_format_versions: Option, +} + +impl DecryptionKeyBuilder { + fn validate(&self) -> Result<(), String> { + if self.method != Some(EncryptionMethod::None) && self.uri.is_none() { + return Err(Error::custom("Missing URL").to_string()); + } + Ok(()) + } } impl DecryptionKey { @@ -54,7 +63,7 @@ impl DecryptionKey { uri: Some(uri.to_string()), iv: None, key_format: None, - key_format_versions: KeyFormatVersions::new(), + key_format_versions: None, } } @@ -103,13 +112,15 @@ impl DecryptionKey { /// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() /// ); /// ``` - pub fn set_method(&mut self, value: EncryptionMethod) { + 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. + /// # Note + /// This attribute is required, if the [EncryptionMethod] is not `None`. /// /// # Example /// ``` @@ -152,14 +163,13 @@ impl DecryptionKey { /// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() /// ); /// ``` - pub fn set_uri(&mut self, value: Option) { + pub fn set_uri(&mut self, value: Option) -> &mut Self { self.uri = value.map(|v| v.to_string()); + self } /// Returns the IV (Initialization Vector) attribute. /// - /// This attribute is optional. - /// /// # Example /// ``` /// # use hls_m3u8::types::DecryptionKey; @@ -170,9 +180,10 @@ impl DecryptionKey { /// "https://www.example.com/" /// ); /// - /// key.set_iv([ + /// # assert_eq!(key.iv(), None); + /// key.set_iv(Some([ /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 - /// ]); + /// ])); /// /// assert_eq!( /// key.iv(), @@ -189,8 +200,6 @@ impl DecryptionKey { /// Sets the `IV` attribute. /// - /// This attribute is optional. - /// /// # Example /// ``` /// # use hls_m3u8::types::DecryptionKey; @@ -201,27 +210,26 @@ impl DecryptionKey { /// "https://www.example.com/" /// ); /// - /// key.set_iv([ + /// key.set_iv(Some([ /// 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(&mut self, value: T) + pub fn set_iv(&mut self, value: Option) -> &mut Self where T: Into<[u8; 16]>, { - self.iv = Some(InitializationVector(value.into())); + self.iv = value.map(|v| InitializationVector(v.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::types::DecryptionKey; @@ -243,9 +251,7 @@ impl DecryptionKey { self.key_format } - /// Sets the `KEYFORMAT` attribute. - /// - /// This attribute is optional. + /// Sets the [KeyFormat] attribute. /// /// # Example /// ``` @@ -264,14 +270,13 @@ impl DecryptionKey { /// Some(KeyFormat::Identity) /// ); /// ``` - pub fn set_key_format>(&mut self, value: Option) { + pub fn set_key_format>(&mut self, value: Option) -> &mut Self { self.key_format = value.map(|v| v.into()); + self } /// Returns the [KeyFormatVersions] attribute. /// - /// This attribute is optional. - /// /// # Example /// ``` /// # use hls_m3u8::types::DecryptionKey; @@ -282,21 +287,19 @@ impl DecryptionKey { /// "https://www.example.com/" /// ); /// - /// key.set_key_format_versions(vec![1, 2, 3, 4, 5]); + /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5])); /// /// assert_eq!( /// key.key_format_versions(), - /// &KeyFormatVersions::from(vec![1, 2, 3, 4, 5]) + /// &Some(KeyFormatVersions::from(vec![1, 2, 3, 4, 5])) /// ); /// ``` - pub const fn key_format_versions(&self) -> &KeyFormatVersions { + pub const fn key_format_versions(&self) -> &Option { &self.key_format_versions } /// Sets the [KeyFormatVersions] attribute. /// - /// This attribute is optional. - /// /// # Example /// ``` /// # use hls_m3u8::types::DecryptionKey; @@ -307,21 +310,25 @@ impl DecryptionKey { /// "https://www.example.com/" /// ); /// - /// key.set_key_format_versions(vec![1, 2, 3, 4, 5]); + /// key.set_key_format_versions(Some(vec![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>(&mut self, value: T) { - self.key_format_versions = value.into(); + pub fn set_key_format_versions>( + &mut self, + value: Option, + ) -> &mut Self { + self.key_format_versions = value.map(|v| v.into()); + self } } impl RequiredVersion for DecryptionKey { fn required_version(&self) -> ProtocolVersion { - if self.key_format.is_some() || !self.key_format_versions.is_default() { + if self.key_format.is_some() || self.key_format_versions.is_some() { ProtocolVersion::V5 } else if self.iv.is_some() { ProtocolVersion::V2 @@ -360,12 +367,12 @@ impl FromStr for DecryptionKey { return Err(Error::missing_value("URI")); } - Ok(DecryptionKey { + Ok(Self { method, uri, iv, key_format, - key_format_versions: key_format_versions.unwrap_or_default(), + key_format_versions, }) } } @@ -386,8 +393,11 @@ impl fmt::Display for DecryptionKey { if let Some(value) = &self.key_format { write!(f, ",KEYFORMAT={}", quote(value))?; } - if !self.key_format_versions.is_default() { - write!(f, ",KEYFORMATVERSIONS={}", &self.key_format_versions)?; + + if let Some(key_format_versions) = &self.key_format_versions { + if !key_format_versions.is_default() { + write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?; + } } Ok(()) } @@ -410,6 +420,7 @@ mod test { .key_format_versions(vec![1, 2, 3, 4, 5]) .build() .unwrap(); + assert_eq!( key.to_string(), "METHOD=AES-128,\ @@ -419,7 +430,13 @@ mod test { KEYFORMATVERSIONS=\"1/2/3/4/5\"\ " .to_string() - ) + ); + + assert!(DecryptionKey::builder().build().is_err()); + assert!(DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .build() + .is_err()); } #[test] @@ -428,9 +445,9 @@ mod test { EncryptionMethod::Aes128, "https://www.example.com/hls-key/key.bin", ); - key.set_iv([ + key.set_iv(Some([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ]); + ])); assert_eq!( key.to_string(), @@ -458,9 +475,9 @@ mod test { EncryptionMethod::Aes128, "https://www.example.com/hls-key/key.bin", ); - key.set_iv([ + key.set_iv(Some([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ]); + ])); assert_eq!( "METHOD=AES-128,\ @@ -472,9 +489,9 @@ mod test { ); let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"); - key.set_iv([ + key.set_iv(Some([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ]); + ])); key.set_key_format(Some(KeyFormat::Identity)); assert_eq!( @@ -485,7 +502,30 @@ mod test { .parse::() .unwrap(), key - ) + ); + + key.set_key_format_versions(Some(vec![1, 2, 3])); + assert_eq!( + "METHOD=AES-128,\ + URI=\"http://www.example.com\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52,\ + KEYFORMAT=\"identity\",\ + KEYFORMATVERSIONS=\"1/2/3\"" + .parse::() + .unwrap(), + key + ); + + assert_eq!( + "METHOD=AES-128,\ + URI=\"http://www.example.com\",\ + UNKNOWNTAG=abcd" + .parse::() + .unwrap(), + DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com") + ); + assert!("METHOD=AES-128,URI=".parse::().is_err()); + assert!("garbage".parse::().is_err()); } #[test] diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index ac51a4f..fb3673f 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -1,7 +1,4 @@ -use std::fmt; -use std::str::FromStr; - -use crate::Error; +use strum::{Display, EnumString}; /// Encryption method. /// @@ -9,7 +6,8 @@ use crate::Error; /// /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 #[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] +#[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum EncryptionMethod { /// `None` means that [MediaSegment]s are not encrypted. /// @@ -26,6 +24,7 @@ pub enum EncryptionMethod { /// [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 + #[strum(serialize = "AES-128")] Aes128, /// `SampleAes` means that the [MediaSegment]s /// contain media samples, such as audio or video, that are encrypted @@ -48,32 +47,6 @@ pub enum EncryptionMethod { SampleAes, } -impl fmt::Display for EncryptionMethod { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self { - EncryptionMethod::Aes128 => "AES-128".fmt(f), - EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), - EncryptionMethod::None => "NONE".fmt(f), - } - } -} - -impl FromStr for EncryptionMethod { - type Err = Error; - - fn from_str(input: &str) -> Result { - match input { - "AES-128" => Ok(EncryptionMethod::Aes128), - "SAMPLE-AES" => Ok(EncryptionMethod::SampleAes), - "NONE" => Ok(EncryptionMethod::None), - _ => Err(Error::custom(format!( - "Unknown encryption method: {:?}", - input - ))), - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index 28de702..a268181 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -1,7 +1,4 @@ -use std::fmt; -use std::str::FromStr; - -use crate::Error; +use strum::{Display, EnumString}; /// HDCP level. /// @@ -9,33 +6,14 @@ use crate::Error; /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 #[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] +#[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum HdcpLevel { + #[strum(serialize = "TYPE-0")] Type0, None, } -impl fmt::Display for HdcpLevel { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self { - HdcpLevel::Type0 => "TYPE-0".fmt(f), - HdcpLevel::None => "NONE".fmt(f), - } - } -} - -impl FromStr for HdcpLevel { - type Err = Error; - - fn from_str(input: &str) -> Result { - match input { - "TYPE-0" => Ok(HdcpLevel::Type0), - "NONE" => Ok(HdcpLevel::None), - _ => Err(Error::custom(format!("Unknown HDCP level: {:?}", input))), - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index 23c51bf..1bd7cab 100644 --- a/src/types/in_stream_id.rs +++ b/src/types/in_stream_id.rs @@ -1,7 +1,4 @@ -use std::fmt; -use std::str::FromStr; - -use crate::Error; +use strum::{Display, EnumString}; /// Identifier of a rendition within the segments in a media playlist. /// @@ -9,7 +6,8 @@ use crate::Error; /// /// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 #[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] +#[strum(serialize_all = "UPPERCASE")] pub enum InStreamId { Cc1, Cc2, @@ -80,89 +78,6 @@ pub enum InStreamId { Service63, } -impl fmt::Display for InStreamId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - format!("{:?}", self).to_uppercase().fmt(f) - } -} - -impl FromStr for InStreamId { - type Err = Error; - - fn from_str(input: &str) -> Result { - Ok(match input { - "CC1" => Self::Cc1, - "CC2" => Self::Cc2, - "CC3" => Self::Cc3, - "CC4" => Self::Cc4, - "SERVICE1" => Self::Service1, - "SERVICE2" => Self::Service2, - "SERVICE3" => Self::Service3, - "SERVICE4" => Self::Service4, - "SERVICE5" => Self::Service5, - "SERVICE6" => Self::Service6, - "SERVICE7" => Self::Service7, - "SERVICE8" => Self::Service8, - "SERVICE9" => Self::Service9, - "SERVICE10" => Self::Service10, - "SERVICE11" => Self::Service11, - "SERVICE12" => Self::Service12, - "SERVICE13" => Self::Service13, - "SERVICE14" => Self::Service14, - "SERVICE15" => Self::Service15, - "SERVICE16" => Self::Service16, - "SERVICE17" => Self::Service17, - "SERVICE18" => Self::Service18, - "SERVICE19" => Self::Service19, - "SERVICE20" => Self::Service20, - "SERVICE21" => Self::Service21, - "SERVICE22" => Self::Service22, - "SERVICE23" => Self::Service23, - "SERVICE24" => Self::Service24, - "SERVICE25" => Self::Service25, - "SERVICE26" => Self::Service26, - "SERVICE27" => Self::Service27, - "SERVICE28" => Self::Service28, - "SERVICE29" => Self::Service29, - "SERVICE30" => Self::Service30, - "SERVICE31" => Self::Service31, - "SERVICE32" => Self::Service32, - "SERVICE33" => Self::Service33, - "SERVICE34" => Self::Service34, - "SERVICE35" => Self::Service35, - "SERVICE36" => Self::Service36, - "SERVICE37" => Self::Service37, - "SERVICE38" => Self::Service38, - "SERVICE39" => Self::Service39, - "SERVICE40" => Self::Service40, - "SERVICE41" => Self::Service41, - "SERVICE42" => Self::Service42, - "SERVICE43" => Self::Service43, - "SERVICE44" => Self::Service44, - "SERVICE45" => Self::Service45, - "SERVICE46" => Self::Service46, - "SERVICE47" => Self::Service47, - "SERVICE48" => Self::Service48, - "SERVICE49" => Self::Service49, - "SERVICE50" => Self::Service50, - "SERVICE51" => Self::Service51, - "SERVICE52" => Self::Service52, - "SERVICE53" => Self::Service53, - "SERVICE54" => Self::Service54, - "SERVICE55" => Self::Service55, - "SERVICE56" => Self::Service56, - "SERVICE57" => Self::Service57, - "SERVICE58" => Self::Service58, - "SERVICE59" => Self::Service59, - "SERVICE60" => Self::Service60, - "SERVICE61" => Self::Service61, - "SERVICE62" => Self::Service62, - "SERVICE63" => Self::Service63, - _ => return Err(Error::custom(format!("Unknown instream id: {:?}", input))), - }) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index b6d8c48..388a264 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -13,7 +13,7 @@ use crate::Error; pub struct InitializationVector(pub [u8; 16]); impl InitializationVector { - /// Converts the initialization vector to a slice. + /// Converts the [InitializationVector] to a slice. pub const fn to_slice(&self) -> [u8; 16] { self.0 } @@ -51,21 +51,106 @@ impl fmt::Display for InitializationVector { impl FromStr for InitializationVector { type Err = Error; - fn from_str(s: &str) -> Result { - if !(s.starts_with("0x") || s.starts_with("0X")) { + fn from_str(input: &str) -> Result { + if !(input.starts_with("0x") || input.starts_with("0X")) { return Err(Error::invalid_input()); } - if s.len() - 2 != 32 { + if input.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 mut result = [0; 16]; + for (i, c) in input.as_bytes().chunks(2).skip(1).enumerate() { let d = std::str::from_utf8(c).map_err(Error::custom)?; let b = u8::from_str_radix(d, 16).map_err(Error::custom)?; - v[i] = b; + result[i] = b; } - Ok(InitializationVector(v)) + Ok(Self(result)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!( + "0x10ef8f758ca555115584bb5b3c687f52".to_string(), + InitializationVector([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82 + ]) + .to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!( + "0x10ef8f758ca555115584bb5b3c687f52" + .parse::() + .unwrap(), + InitializationVector([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82 + ]) + ); + + assert_eq!( + "0X10ef8f758ca555115584bb5b3c687f52" + .parse::() + .unwrap(), + InitializationVector([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82 + ]) + ); + + assert_eq!( + "0X10EF8F758CA555115584BB5B3C687F52" + .parse::() + .unwrap(), + InitializationVector([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82 + ]) + ); + + assert!("garbage".parse::().is_err()); + assert!("0xgarbage".parse::().is_err()); + assert!("0x12".parse::().is_err()); + assert!("0X10EF8F758CA555115584BB5B3C687F5Z" + .parse::() + .is_err()); + } + + #[test] + fn test_as_ref() { + assert_eq!( + InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).as_ref(), + &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + } + + #[test] + fn test_deref() { + assert_eq!( + InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).deref(), + &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + } + + #[test] + fn test_from() { + assert_eq!( + InitializationVector::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + ); + } + + #[test] + fn test_to_slice() { + assert_eq!( + InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).to_slice(), + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); } } diff --git a/src/types/key_format.rs b/src/types/key_format.rs index 43ef6a7..c42f9a9 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -54,6 +54,8 @@ mod tests { assert_eq!(KeyFormat::Identity, quote("identity").parse().unwrap()); assert_eq!(KeyFormat::Identity, "identity".parse().unwrap()); + + assert!("garbage".parse::().is_err()); } #[test] diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs index 5309899..5ee66dd 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -37,7 +37,7 @@ impl KeyFormatVersions { /// Returns `true`, if [KeyFormatVersions] has the default value of `vec![1]`. pub fn is_default(&self) -> bool { - self.0 == vec![1] || self.0.is_empty() + self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() } } diff --git a/src/types/media_type.rs b/src/types/media_type.rs index 5bc10d4..52a7e57 100644 --- a/src/types/media_type.rs +++ b/src/types/media_type.rs @@ -1,15 +1,9 @@ -use std::fmt; -use std::str::FromStr; +use strum::{Display, EnumString}; -use crate::Error; - -/// Media type. -/// -/// See: [4.3.4.1. EXT-X-MEDIA] -/// -/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 +/// Specifies the media type. #[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Ord, PartialOrd, Display, EnumString, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum MediaType { Audio, Video, @@ -17,29 +11,29 @@ pub enum MediaType { ClosedCaptions, } -impl fmt::Display for MediaType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self { - MediaType::Audio => "AUDIO".fmt(f), - MediaType::Video => "VIDEO".fmt(f), - MediaType::Subtitles => "SUBTITLES".fmt(f), - MediaType::ClosedCaptions => "CLOSED-CAPTIONS".fmt(f), - } - } -} - -impl FromStr for MediaType { - type Err = Error; - - fn from_str(input: &str) -> Result { - Ok(match input { - "AUDIO" => MediaType::Audio, - "VIDEO" => MediaType::Video, - "SUBTITLES" => MediaType::Subtitles, - "CLOSED-CAPTIONS" => MediaType::ClosedCaptions, - _ => { - return Err(Error::invalid_input()); - } - }) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parser() { + assert_eq!(MediaType::Audio, "AUDIO".parse().unwrap()); + assert_eq!(MediaType::Video, "VIDEO".parse().unwrap()); + assert_eq!(MediaType::Subtitles, "SUBTITLES".parse().unwrap()); + assert_eq!( + MediaType::ClosedCaptions, + "CLOSED-CAPTIONS".parse().unwrap() + ); + } + + #[test] + fn test_display() { + assert_eq!(MediaType::Audio.to_string(), "AUDIO".to_string()); + assert_eq!(MediaType::Video.to_string(), "VIDEO".to_string()); + assert_eq!(MediaType::Subtitles.to_string(), "SUBTITLES".to_string()); + assert_eq!( + MediaType::ClosedCaptions.to_string(), + "CLOSED-CAPTIONS".to_string() + ); } } diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index a39747d..9a3e42f 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -27,9 +27,12 @@ pub trait RequiredVersion { fn required_version(&self) -> ProtocolVersion; } -/// [7. Protocol Version Compatibility] +/// # [7. Protocol Version Compatibility] +/// The [ProtocolVersion] specifies, which m3u8 revision is required, to parse +/// a certain tag correctly. /// -/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7 +/// [7. Protocol Version Compatibility]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-7 #[allow(missing_docs)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ProtocolVersion { @@ -43,7 +46,13 @@ pub enum ProtocolVersion { } impl ProtocolVersion { - /// Returns the newest ProtocolVersion, that is supported by this library. + /// Returns the newest [ProtocolVersion], that is supported by this library. + /// + /// # Example + /// ``` + /// # use hls_m3u8::types::ProtocolVersion; + /// assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7); + /// ``` pub const fn latest() -> Self { Self::V7 } @@ -51,18 +60,15 @@ impl ProtocolVersion { impl fmt::Display for ProtocolVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let n = { - match &self { - Self::V1 => 1, - Self::V2 => 2, - Self::V3 => 3, - Self::V4 => 4, - Self::V5 => 5, - Self::V6 => 6, - Self::V7 => 7, - } - }; - write!(f, "{}", n) + match &self { + Self::V1 => write!(f, "1"), + Self::V2 => write!(f, "2"), + Self::V3 => write!(f, "3"), + Self::V4 => write!(f, "4"), + Self::V5 => write!(f, "5"), + Self::V6 => write!(f, "6"), + Self::V7 => write!(f, "7"), + } } } @@ -115,5 +121,18 @@ mod tests { assert_eq!(ProtocolVersion::V5, "5".parse().unwrap()); assert_eq!(ProtocolVersion::V6, "6".parse().unwrap()); assert_eq!(ProtocolVersion::V7, "7".parse().unwrap()); + + assert_eq!(ProtocolVersion::V7, " 7 ".parse().unwrap()); + assert!("garbage".parse::().is_err()); + } + + #[test] + fn test_default() { + assert_eq!(ProtocolVersion::default(), ProtocolVersion::V1); + } + + #[test] + fn test_latest() { + assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7); } } diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index d70c472..9623539 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -1,14 +1,12 @@ -use std::fmt; -use std::str::FromStr; - -use crate::Error; +use core::ops::Deref; +use derive_more::{Display, FromStr}; /// Signed decimal floating-point number. /// /// See: [4.2. Attribute Lists] /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display, FromStr)] pub(crate) struct SignedDecimalFloatingPoint(f64); impl SignedDecimalFloatingPoint { @@ -16,11 +14,15 @@ impl SignedDecimalFloatingPoint { /// /// # Panics /// The given value must be finite, otherwise this function will panic! - pub fn new(n: f64) -> Self { - if n.is_infinite() { + pub fn new(value: f64) -> Self { + if value.is_infinite() { panic!("Floating point value must be finite!"); } - Self(n) + Self(value) + } + + pub(crate) const fn from_f64_unchecked(value: f64) -> Self { + Self(value) } /// Converts [DecimalFloatingPoint] to [f64]. @@ -29,32 +31,47 @@ impl SignedDecimalFloatingPoint { } } -impl From for SignedDecimalFloatingPoint { - fn from(f: i32) -> Self { - Self(f64::from(f)) +impl Deref for SignedDecimalFloatingPoint { + type Target = f64; + + fn deref(&self) -> &Self::Target { + &self.0 } } impl Eq for SignedDecimalFloatingPoint {} -impl fmt::Display for SignedDecimalFloatingPoint { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl FromStr for SignedDecimalFloatingPoint { - type Err = Error; - - fn from_str(input: &str) -> Result { - Ok(Self::new(input.parse().map_err(Error::parse_float_error)?)) - } -} - #[cfg(test)] mod tests { use super::*; + macro_rules! test_from { + ( $( $input:expr => $output:expr ),* ) => { + use ::core::convert::From; + + #[test] + fn test_from() { + $( + assert_eq!( + $input, + $output, + ); + )* + } + } + } + + test_from![ + SignedDecimalFloatingPoint::from(1u8) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1i8) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1u16) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1i16) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1u32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1i32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1.0f32) => SignedDecimalFloatingPoint::new(1.0), + SignedDecimalFloatingPoint::from(1.0f64) => SignedDecimalFloatingPoint::new(1.0) + ]; + #[test] fn test_display() { assert_eq!( @@ -75,13 +92,17 @@ mod tests { SignedDecimalFloatingPoint::new(1.0), "1.0".parse::().unwrap() ); + + assert!("garbage".parse::().is_err()); } #[test] - fn test_from() { - assert_eq!( - SignedDecimalFloatingPoint::from(1i32), - SignedDecimalFloatingPoint::new(1.0) - ); + fn test_as_f64() { + assert_eq!(SignedDecimalFloatingPoint::new(1.0).as_f64(), 1.0); + } + + #[test] + fn test_deref() { + assert_eq!(SignedDecimalFloatingPoint::from(0.1).floor(), 0.0); } } diff --git a/src/types/stream_inf.rs b/src/types/stream_inf.rs index 0e3c9d8..f8b329f 100644 --- a/src/types/stream_inf.rs +++ b/src/types/stream_inf.rs @@ -3,14 +3,13 @@ use std::str::FromStr; use crate::attribute::AttributePairs; use crate::types::{DecimalResolution, HdcpLevel}; -use crate::utils::parse_u64; use crate::utils::{quote, unquote}; use crate::Error; /// [4.3.4.2. EXT-X-STREAM-INF] /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] pub struct StreamInf { bandwidth: u64, average_bandwidth: Option, @@ -182,6 +181,8 @@ impl StreamInf { /// /// stream.set_resolution(1920, 1080); /// assert_eq!(stream.resolution(), Some((1920, 1080))); + /// # stream.set_resolution(1280, 10); + /// # assert_eq!(stream.resolution(), Some((1280, 10))); /// ``` pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self { if let Some(res) = &mut self.resolution { @@ -259,8 +260,8 @@ impl FromStr for StreamInf { for (key, value) in input.parse::()? { match key.as_str() { - "BANDWIDTH" => bandwidth = Some(parse_u64(value)?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(parse_u64(value)?), + "BANDWIDTH" => bandwidth = Some(value.parse::()?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some(value.parse::()?), "CODECS" => codecs = Some(unquote(value)), "RESOLUTION" => resolution = Some(value.parse()?), "HDCP-LEVEL" => hdcp_level = Some(value.parse()?), @@ -284,3 +285,66 @@ impl FromStr for StreamInf { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + let mut stream_inf = StreamInf::new(200); + stream_inf.set_average_bandwidth(Some(15)); + stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + stream_inf.set_resolution(1920, 1080); + stream_inf.set_hdcp_level(Some(HdcpLevel::Type0)); + stream_inf.set_video(Some("video")); + + assert_eq!( + stream_inf.to_string(), + "BANDWIDTH=200,\ + AVERAGE-BANDWIDTH=15,\ + CODECS=\"mp4a.40.2,avc1.4d401e\",\ + RESOLUTION=1920x1080,\ + HDCP-LEVEL=TYPE-0,\ + VIDEO=\"video\"" + .to_string() + ); + } + + #[test] + fn test_parser() { + let mut stream_inf = StreamInf::new(200); + stream_inf.set_average_bandwidth(Some(15)); + stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e")); + stream_inf.set_resolution(1920, 1080); + stream_inf.set_hdcp_level(Some(HdcpLevel::Type0)); + stream_inf.set_video(Some("video")); + + assert_eq!( + stream_inf, + "BANDWIDTH=200,\ + AVERAGE-BANDWIDTH=15,\ + CODECS=\"mp4a.40.2,avc1.4d401e\",\ + RESOLUTION=1920x1080,\ + HDCP-LEVEL=TYPE-0,\ + VIDEO=\"video\"" + .parse() + .unwrap() + ); + + assert_eq!( + stream_inf, + "BANDWIDTH=200,\ + AVERAGE-BANDWIDTH=15,\ + CODECS=\"mp4a.40.2,avc1.4d401e\",\ + RESOLUTION=1920x1080,\ + HDCP-LEVEL=TYPE-0,\ + VIDEO=\"video\",\ + UNKNOWN=\"value\"" + .parse() + .unwrap() + ); + + assert!("garbage".parse::().is_err()); + } +} diff --git a/src/utils.rs b/src/utils.rs index ac0adb1..440db7c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,26 @@ use crate::Error; +macro_rules! impl_from { + ( $($( $type:tt ),* => $target:path ),* ) => { + use ::core::convert::From; + + $( // repeat $target + $( // repeat $type + impl From<$type> for $target { + fn from(value: $type) -> Self { + Self::from_f64_unchecked(value.into()) + } + } + )* + )* + }; +} + +impl_from![ + u8, u16, u32 => crate::types::DecimalFloatingPoint, + u8, i8, u16, i16, u32, i32, f32, f64 => crate::types::SignedDecimalFloatingPoint +]; + pub(crate) fn parse_yes_or_no>(s: T) -> crate::Result { match s.as_ref() { "YES" => Ok(true), @@ -8,11 +29,6 @@ pub(crate) fn parse_yes_or_no>(s: T) -> crate::Result { } } -pub(crate) fn parse_u64>(s: T) -> crate::Result { - let n = s.as_ref().parse().map_err(Error::unknown)?; // TODO: Error::number - Ok(n) -} - /// According to the documentation the following characters are forbidden /// inside a quoted string: /// - carriage return (`\r`) @@ -64,13 +80,6 @@ mod tests { assert!(parse_yes_or_no("garbage").is_err()); } - #[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()); diff --git a/tarpaulin-report.html b/tarpaulin-report.html new file mode 100644 index 0000000..e550c1f --- /dev/null +++ b/tarpaulin-report.html @@ -0,0 +1,352 @@ + + + + + + + +
+ + + + + + \ No newline at end of file