diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 269b5ab..9127cb2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,60 +4,13 @@ name: Rust on: [push, pull_request] jobs: - build: - name: Build - runs-on: ${{ matrix.os }} - strategy: - matrix: - rust: - - stable - os: - - ubuntu-latest - # execute cargo build - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.rust }} - override: true - - uses: actions-rs/cargo@v1 - with: - command: build - arguments: --all-features - - test: - name: Test - runs-on: ${{ matrix.os }} - strategy: - matrix: - rust: - - stable - os: - - ubuntu-latest - # execute cargo test - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.rust }} - override: true - - uses: actions-rs/cargo@v1 - with: - command: test - rustfmt: - name: Rustfmt runs-on: ubuntu-latest - strategy: - matrix: - rust: - - stable steps: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.rust }} - override: true + toolchain: stable - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 with: @@ -65,20 +18,14 @@ jobs: args: --all -- --check clippy: - name: Clippy runs-on: ubuntu-latest - strategy: - matrix: - rust: - - stable steps: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.rust }} - override: true + toolchain: stable - run: rustup component add clippy - uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + # args: -- -D warnings diff --git a/.gitignore b/.gitignore index 143b1ca..e99ca84 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /target/ **/*.rs.bk Cargo.lock +tarpaulin-report.html diff --git a/.travis.yml b/.travis.yml index 0a8114c..03e1306 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,17 @@ language: rust -sudo: required + +cache: cargo + +before_cache: | + cargo install cargo-tarpaulin + cargo install cargo-update + cargo install-update --all + + +# before_cache: +# - rm -rf /home/travis/.cargo/registry + + rust: - stable - beta @@ -8,32 +20,14 @@ matrix: allow_failures: - rust: nightly -env: - global: - - RUSTFLAGS="-C link-dead-code" - -addons: - apt: - packages: - - libcurl4-openssl-dev - - libelf-dev - - libdw-dev - - cmake - - gcc - - binutils-dev - - libiberty-dev +script: +- cargo clean +- cargo build +- cargo test after_success: | - wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && - tar xzf master.tar.gz && - cd kcov-master && - mkdir build && - cd build && - cmake .. && - make && - make install DESTDIR=../../kcov-build && - cd ../.. && - rm -rf kcov-master && - for file in target/debug/hls_m3u8-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done && - bash <(curl -s https://codecov.io/bash) && - echo "Uploaded code coverage" + # this does require a -Z flag for Doctests, which is unstable! + if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then + cargo tarpaulin --run-types Tests Doctests --out Xml + bash <(curl -s https://codecov.io/bash) + fi 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 2da9be6..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()); } @@ -122,6 +125,11 @@ mod test { let mut iterator = pairs.iter(); assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3")); + + let mut pairs = AttributePairs::new(); + pairs.insert("FOO".to_string(), "BAR".to_string()); + + assert_eq!("FOO=BAR,VAL".parse::().unwrap(), pairs); } #[test] @@ -136,4 +144,18 @@ mod test { let mut iterator = attrs.iter(); assert!(iterator.any(|(k, v)| k == "key_02" && v == "value_02")); } + + #[test] + fn test_into_iter() { + let mut map = HashMap::new(); + map.insert("k".to_string(), "v".to_string()); + + let mut attrs = AttributePairs::new(); + attrs.insert("k".to_string(), "v".to_string()); + + assert_eq!( + attrs.into_iter().collect::>(), + map.into_iter().collect::>() + ); + } } 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/line.rs b/src/line.rs index 6d8ca95..67a3724 100644 --- a/src/line.rs +++ b/src/line.rs @@ -26,8 +26,7 @@ impl FromStr for Lines { for l in input.lines() { let line = l.trim(); - // ignore empty lines - if line.len() == 0 { + if line.is_empty() { continue; } @@ -39,7 +38,7 @@ impl FromStr for Lines { continue; } else if line.starts_with("#EXT") { Line::Tag(line.parse()?) - } else if line.starts_with("#") { + } else if line.starts_with('#') { continue; // ignore comments } else { // stream inf line needs special treatment diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 6f52f0f..456c205 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -103,7 +103,7 @@ impl MasterPlaylistBuilder { let required_version = self.required_version(); let specified_version = self .version_tag - .unwrap_or(required_version.into()) + .unwrap_or_else(|| required_version.into()) .version(); if required_version > specified_version { @@ -164,7 +164,7 @@ impl MasterPlaylistBuilder { .flatten(), ) .max() - .unwrap_or(ProtocolVersion::latest()) + .unwrap_or_else(ProtocolVersion::latest) } fn validate_stream_inf_tags(&self) -> crate::Result<()> { @@ -188,24 +188,23 @@ impl MasterPlaylistBuilder { } } match t.closed_captions() { - Some(&ClosedCaptions::GroupId(ref group_id)) => { + &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) => { + &Some(ClosedCaptions::None) => { has_none_closed_captions = true; } None => {} } } - if has_none_closed_captions { - if !value + if has_none_closed_captions + && !value .iter() - .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) - { - return Err(Error::invalid_input()); - } + .all(|t| t.closed_captions() == &Some(ClosedCaptions::None)) + { + return Err(Error::invalid_input()); } } Ok(()) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index da20213..455b80a 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -70,7 +70,7 @@ impl MediaPlaylistBuilder { let required_version = self.required_version(); let specified_version = self .version_tag - .unwrap_or(required_version.into()) + .unwrap_or_else(|| required_version.into()) .version(); if required_version > specified_version { @@ -109,7 +109,7 @@ impl MediaPlaylistBuilder { } }; - if !(rounded_segment_duration <= max_segment_duration) { + if rounded_segment_duration > max_segment_duration { return Err(Error::custom(format!( "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", segment_duration, @@ -122,7 +122,7 @@ impl MediaPlaylistBuilder { // 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())?; + let last_uri = last_range_uri.ok_or_else(Error::invalid_input)?; if last_uri != s.uri() { return Err(Error::invalid_input()); } @@ -200,7 +200,7 @@ impl MediaPlaylistBuilder { .unwrap_or(ProtocolVersion::V1) })) .max() - .unwrap_or(ProtocolVersion::latest()) + .unwrap_or_else(ProtocolVersion::latest) } /// Adds a media segment to the resulting playlist. diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index bff523f..874d156 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -50,7 +50,7 @@ impl ExtXVersion { /// ProtocolVersion::V6 /// ); /// ``` - pub const fn version(&self) -> ProtocolVersion { + pub const fn version(self) -> ProtocolVersion { self.0 } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 167b358..076478d 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, @@ -31,6 +31,12 @@ impl ExtXIFrameStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; /// Makes a new [ExtXIFrameStreamInf] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXIFrameStreamInf; + /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); + /// ``` pub fn new(uri: T, bandwidth: u64) -> Self { ExtXIFrameStreamInf { uri: uri.to_string(), @@ -43,7 +49,6 @@ impl ExtXIFrameStreamInf { /// # Example /// ``` /// # use hls_m3u8::tags::ExtXIFrameStreamInf; - /// # /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); /// assert_eq!(stream.uri(), &"https://www.example.com".to_string()); /// ``` @@ -91,13 +96,12 @@ impl FromStr for ExtXIFrameStreamInf { let mut uri = None; for (key, value) in input.parse::()? { - match key.as_str() { - "URI" => uri = Some(unquote(value)), - _ => {} + if let "URI" = key.as_str() { + uri = Some(unquote(value)); } } - let uri = uri.ok_or(Error::missing_value("URI"))?; + let uri = uri.ok_or_else(|| Error::missing_value("URI"))?; Ok(Self { uri, @@ -140,6 +144,8 @@ mod test { .unwrap(), ExtXIFrameStreamInf::new("foo", 1000) ); + + assert!("garbage".parse::().is_err()); } #[test] @@ -149,4 +155,22 @@ mod test { ProtocolVersion::V1 ); } + + #[test] + fn test_deref() { + assert_eq!( + ExtXIFrameStreamInf::new("https://www.example.com", 20).average_bandwidth(), + None + ) + } + + #[test] + fn test_deref_mut() { + assert_eq!( + ExtXIFrameStreamInf::new("https://www.example.com", 20) + .set_average_bandwidth(Some(4)) + .average_bandwidth(), + Some(4) + ) + } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 9232b25..9437fbd 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -68,7 +68,7 @@ impl ExtXMediaBuilder { fn validate(&self) -> Result<(), String> { let media_type = self .media_type - .ok_or(Error::missing_attribute("MEDIA-TYPE").to_string())?; + .ok_or_else(|| Error::missing_attribute("MEDIA-TYPE").to_string())?; if MediaType::ClosedCaptions == media_type { if self.uri.is_some() { @@ -78,11 +78,9 @@ impl ExtXMediaBuilder { .to_string()); } self.instream_id - .ok_or(Error::missing_attribute("INSTREAM-ID").to_string())?; - } else { - if self.instream_id.is_some() { - return Err(Error::custom("Unexpected attribute: \"INSTREAM-ID\"!").to_string()); - } + .ok_or_else(|| Error::missing_attribute("INSTREAM-ID").to_string())?; + } else if self.instream_id.is_some() { + return Err(Error::custom("Unexpected attribute: \"INSTREAM-ID\"!").to_string()); } if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) { @@ -91,10 +89,8 @@ impl ExtXMediaBuilder { ); } - if MediaType::Subtitles != media_type { - if self.is_forced.is_some() { - return Err(Error::invalid_input().to_string()); - } + if MediaType::Subtitles != media_type && self.is_forced.is_some() { + return Err(Error::invalid_input().to_string()); } Ok(()) @@ -106,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(), @@ -122,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 } } @@ -247,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() { @@ -349,6 +751,233 @@ 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::builder() + .media_type(MediaType::Audio) + .group_id("audio-aacl-312") + .language("en") + .name("English") + .is_autoselect(true) + .is_default(true) + .channels("2") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + GROUP-ID=\"audio-aacl-312\",\ + LANGUAGE=\"en\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES,\ + CHANNELS=\"2\"" + .to_string() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Subtitles) + .uri("french/ed.ttml") + .group_id("subs") + .language("fra") + .assoc_language("fra") + .name("French") + .is_autoselect(true) + .is_forced(true) + .characteristics("public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound") + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=SUBTITLES,\ + URI=\"french/ed.ttml\",\ + GROUP-ID=\"subs\",\ + LANGUAGE=\"fra\",\ + ASSOC-LANGUAGE=\"fra\",\ + NAME=\"French\",\ + AUTOSELECT=YES,\ + FORCED=YES,\ + CHARACTERISTICS=\"public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound\"".to_string() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::ClosedCaptions) + .group_id("cc") + .language("sp") + .name("CC2") + .instream_id(InStreamId::Cc2) + .is_autoselect(true) + .build() + .unwrap() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=CLOSED-CAPTIONS,\ + GROUP-ID=\"cc\",\ + LANGUAGE=\"sp\",\ + NAME=\"CC2\",\ + AUTOSELECT=YES,\ + INSTREAM-ID=\"CC2\"" + .to_string() + ); + + // ---- assert_eq!( ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(), "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string() @@ -357,6 +986,277 @@ 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::builder() + .media_type(MediaType::Audio) + .group_id("audio-aacl-312") + .language("en") + .name("English") + .is_autoselect(true) + .is_default(true) + .channels("2") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + GROUP-ID=\"audio-aacl-312\",\ + LANGUAGE=\"en\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES,\ + CHANNELS=\"2\"" + .parse() + .unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::Subtitles) + .uri("french/ed.ttml") + .group_id("subs") + .language("fra") + .assoc_language("fra") + .name("French") + .is_autoselect(true) + .characteristics("public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound") + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + URI=\"french/ed.ttml\",\ + TYPE=SUBTITLES,\ + GROUP-ID=\"subs\",\ + LANGUAGE=\"fra\",\ + ASSOC-LANGUAGE=\"fra\",\ + NAME=\"French\",\ + AUTOSELECT=YES,\ + FORCED=NO,\ + CHARACTERISTICS=\"public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound\"".parse().unwrap() + ); + + assert_eq!( + ExtXMedia::builder() + .media_type(MediaType::ClosedCaptions) + .group_id("cc") + .language("sp") + .name("CC2") + .instream_id(InStreamId::Cc2) + .is_autoselect(true) + .build() + .unwrap(), + "#EXT-X-MEDIA:\ + TYPE=CLOSED-CAPTIONS,\ + GROUP-ID=\"cc\",\ + LANGUAGE=\"sp\",\ + NAME=\"CC2\",\ + AUTOSELECT=YES,\ + INSTREAM-ID=\"CC2\",\ + UNKNOWN=TAG" + .parse() + .unwrap() + ); + // ---- assert_eq!( ExtXMedia::new(MediaType::Audio, "foo", "bar"), "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"" @@ -367,9 +1267,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_data.rs b/src/tags/master_playlist/session_data.rs index b97b9e4..f080d5f 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -61,7 +61,7 @@ impl ExtXSessionData { /// ); /// ``` pub fn new(data_id: T, data: SessionData) -> Self { - ExtXSessionData { + Self { data_id: data_id.to_string(), data, language: None, @@ -107,7 +107,7 @@ impl ExtXSessionData { /// ); /// ``` pub fn with_language(data_id: T, data: SessionData, language: T) -> Self { - ExtXSessionData { + Self { data_id: data_id.to_string(), data, language: Some(language.to_string()), @@ -256,13 +256,16 @@ 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(value) => write!(f, ",VALUE={}", quote(value))?, SessionData::Uri(value) => write!(f, ",URI={}", quote(value))?, } + if let Some(value) = &self.language { write!(f, ",LANGUAGE={}", quote(value))?; } + Ok(()) } } @@ -291,7 +294,7 @@ impl FromStr for ExtXSessionData { } } - let data_id = data_id.ok_or(Error::missing_value("EXT-X-DATA-ID"))?; + let data_id = data_id.ok_or_else(|| Error::missing_value("EXT-X-DATA-ID"))?; let data = { if let Some(value) = session_value { if uri.is_some() { @@ -306,7 +309,7 @@ impl FromStr for ExtXSessionData { } }; - Ok(ExtXSessionData { + Ok(Self { data_id, data, language, @@ -321,7 +324,10 @@ mod test { #[test] fn test_display() { assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"".to_string(), + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"com.example.lyrics\",\ + URI=\"lyrics.json\"" + .to_string(), ExtXSessionData::new( "com.example.lyrics", SessionData::Uri("lyrics.json".to_string()) @@ -330,8 +336,10 @@ mod test { ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ - VALUE=\"This is an example\",LANGUAGE=\"en\"" + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"com.example.title\",\ + VALUE=\"This is an example\",\ + LANGUAGE=\"en\"" .to_string(), ExtXSessionData::with_language( "com.example.title", @@ -342,8 +350,10 @@ mod test { ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ - VALUE=\"Este es un ejemplo\",LANGUAGE=\"es\"" + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"com.example.title\",\ + VALUE=\"Este es un ejemplo\",\ + LANGUAGE=\"es\"" .to_string(), ExtXSessionData::with_language( "com.example.title", @@ -354,17 +364,27 @@ mod test { ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\"".to_string(), + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + VALUE=\"bar\"" + .to_string(), ExtXSessionData::new("foo", SessionData::Value("bar".into())).to_string() ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\"".to_string(), + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + URI=\"bar\"" + .to_string(), ExtXSessionData::new("foo", SessionData::Uri("bar".into())).to_string() ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"".to_string(), + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + VALUE=\"bar\",\ + LANGUAGE=\"baz\"" + .to_string(), ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") .to_string() ); @@ -373,7 +393,9 @@ mod test { #[test] fn test_parser() { assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"" + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"com.example.lyrics\",\ + URI=\"lyrics.json\"" .parse::() .unwrap(), ExtXSessionData::new( @@ -383,8 +405,10 @@ mod test { ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ - LANGUAGE=\"en\", VALUE=\"This is an example\"" + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"com.example.title\",\ + LANGUAGE=\"en\",\ + VALUE=\"This is an example\"" .parse::() .unwrap(), ExtXSessionData::with_language( @@ -395,8 +419,10 @@ mod test { ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ - LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\"" + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"com.example.title\",\ + LANGUAGE=\"es\",\ + VALUE=\"Este es un ejemplo\"" .parse::() .unwrap(), ExtXSessionData::with_language( @@ -407,25 +433,47 @@ mod test { ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\"" + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + VALUE=\"bar\"" .parse::() .unwrap(), ExtXSessionData::new("foo", SessionData::Value("bar".into())) ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\"" + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + URI=\"bar\"" .parse::() .unwrap(), ExtXSessionData::new("foo", SessionData::Uri("bar".into())) ); assert_eq!( - "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"" + "#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + VALUE=\"bar\",\ + LANGUAGE=\"baz\",\ + UNKNOWN=TAG" .parse::() .unwrap(), ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") ); + + assert!("#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + LANGUAGE=\"baz\"" + .parse::() + .is_err()); + + assert!("#EXT-X-SESSION-DATA:\ + DATA-ID=\"foo\",\ + LANGUAGE=\"baz\",\ + VALUE=\"VALUE\",\ + URI=\"https://www.example.com/\"" + .parse::() + .is_err()); } #[test] diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 0162213..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,\ @@ -164,4 +164,36 @@ mod test { ProtocolVersion::V1 ); } + + #[test] + #[should_panic] + // ExtXSessionKey::new should panic, if the provided + // EncryptionMethod is None! + fn test_new_panic() { + ExtXSessionKey::new(EncryptionMethod::None, ""); + } + + #[test] + #[should_panic] + fn test_display_err() { + ExtXSessionKey(DecryptionKey::new(EncryptionMethod::None, "")).to_string(); + } + + #[test] + fn test_deref() { + let key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + + assert_eq!(key.method(), EncryptionMethod::Aes128); + assert_eq!(key.uri(), &Some("https://www.example.com/".into())); + } + + #[test] + fn test_deref_mut() { + let mut key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + + key.set_method(EncryptionMethod::None); + assert_eq!(key.method(), EncryptionMethod::None); + key.set_uri(Some("https://www.github.com/")); + assert_eq!(key.uri(), &Some("https://www.github.com/".into())); + } } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index 89ac00d..a025346 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, @@ -27,14 +27,13 @@ impl ExtXStreamInf { /// Creates a new [ExtXStreamInf] tag. /// - /// # Examples + /// # Example /// ``` /// # use hls_m3u8::tags::ExtXStreamInf; - /// # /// let stream = ExtXStreamInf::new("https://www.example.com/", 20); /// ``` pub fn new(uri: T, bandwidth: u64) -> Self { - ExtXStreamInf { + Self { uri: uri.to_string(), frame_rate: None, audio: None, @@ -44,41 +43,160 @@ impl ExtXStreamInf { } } + /// Returns the `URI` that identifies the associated media playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// let stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// + /// assert_eq!(stream.uri(), &"https://www.example.com/".to_string()); + /// ``` + pub const fn uri(&self) -> &String { + &self.uri + } + /// Sets the `URI` that identifies the associated media playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// + /// stream.set_uri("https://www.google.com/"); + /// assert_eq!(stream.uri(), &"https://www.google.com/".to_string()); + /// ``` pub fn set_uri(&mut self, value: T) -> &mut Self { self.uri = value.to_string(); self } - /// Returns the `URI` that identifies the associated media playlist. - pub const fn uri(&self) -> &String { - &self.uri - } - /// Sets the maximum frame rate for all the video in the variant stream. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # assert_eq!(stream.frame_rate(), None); + /// + /// stream.set_frame_rate(Some(59.9)); + /// assert_eq!(stream.frame_rate(), Some(59.9)); + /// ``` pub fn set_frame_rate(&mut self, value: Option) -> &mut Self { self.frame_rate = value.map(|v| v.into()); self } /// Returns the maximum frame rate for all the video in the variant stream. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # assert_eq!(stream.frame_rate(), None); + /// + /// stream.set_frame_rate(Some(59.9)); + /// assert_eq!(stream.frame_rate(), Some(59.9)); + /// ``` pub fn frame_rate(&self) -> Option { - self.frame_rate.map_or(None, |v| Some(v.as_f64())) + self.frame_rate.map(|v| v.as_f64()) } /// Returns the group identifier for the audio in the variant stream. - pub fn audio(&self) -> Option<&String> { - self.audio.as_ref() + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # assert_eq!(stream.audio(), &None); + /// + /// stream.set_audio(Some("audio")); + /// assert_eq!(stream.audio(), &Some("audio".to_string())); + /// ``` + pub const fn audio(&self) -> &Option { + &self.audio + } + + /// Sets the group identifier for the audio in the variant stream. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # assert_eq!(stream.audio(), &None); + /// + /// stream.set_audio(Some("audio")); + /// assert_eq!(stream.audio(), &Some("audio".to_string())); + /// ``` + pub fn set_audio>(&mut self, value: Option) -> &mut Self { + self.audio = value.map(|v| v.into()); + self } /// Returns the group identifier for the subtitles in the variant stream. - pub fn subtitles(&self) -> Option<&String> { - self.subtitles.as_ref() + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # assert_eq!(stream.subtitles(), &None); + /// + /// stream.set_subtitles(Some("subs")); + /// assert_eq!(stream.subtitles(), &Some("subs".to_string())); + /// ``` + pub const fn subtitles(&self) -> &Option { + &self.subtitles } - /// Returns the value of `CLOSED-CAPTIONS` attribute. - pub fn closed_captions(&self) -> Option<&ClosedCaptions> { - self.closed_captions.as_ref() + /// Sets the group identifier for the subtitles in the variant stream. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # assert_eq!(stream.subtitles(), &None); + /// + /// stream.set_subtitles(Some("subs")); + /// assert_eq!(stream.subtitles(), &Some("subs".to_string())); + /// ``` + pub fn set_subtitles>(&mut self, value: Option) -> &mut Self { + self.subtitles = value.map(|v| v.into()); + self + } + + /// Returns the value of [ClosedCaptions] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// use hls_m3u8::types::ClosedCaptions; + /// + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # assert_eq!(stream.closed_captions(), &None); + /// + /// stream.set_closed_captions(Some(ClosedCaptions::None)); + /// assert_eq!(stream.closed_captions(), &Some(ClosedCaptions::None)); + /// ``` + pub const fn closed_captions(&self) -> &Option { + &self.closed_captions + } + + /// Returns the value of [ClosedCaptions] attribute. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStreamInf; + /// use hls_m3u8::types::ClosedCaptions; + /// + /// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20); + /// # assert_eq!(stream.closed_captions(), &None); + /// + /// stream.set_closed_captions(Some(ClosedCaptions::None)); + /// assert_eq!(stream.closed_captions(), &Some(ClosedCaptions::None)); + /// ``` + pub fn set_closed_captions(&mut self, value: Option) -> &mut Self { + self.closed_captions = value; + self } } @@ -113,8 +231,10 @@ impl FromStr for ExtXStreamInf { fn from_str(input: &str) -> Result { let mut lines = input.lines(); - let first_line = lines.next().ok_or(Error::missing_value("first_line"))?; - let uri = lines.next().ok_or(Error::missing_value("URI"))?; + let first_line = lines + .next() + .ok_or_else(|| Error::missing_value("first_line"))?; + let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?; let input = tag(first_line, Self::PREFIX)?; @@ -128,7 +248,7 @@ impl FromStr for ExtXStreamInf { "FRAME-RATE" => frame_rate = Some((value.parse())?), "AUDIO" => audio = Some(unquote(value)), "SUBTITLES" => subtitles = Some(unquote(value)), - "CLOSED-CAPTIONS" => closed_captions = Some((value.parse())?), + "CLOSED-CAPTIONS" => closed_captions = Some(value.parse()?), _ => {} } } @@ -174,6 +294,14 @@ mod test { ); } + #[test] + fn test_display() { + assert_eq!( + ExtXStreamInf::new("http://www.example.com/", 1000).to_string(), + "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string() + ); + } + #[test] fn test_required_version() { assert_eq!( @@ -183,10 +311,20 @@ mod test { } #[test] - fn test_display() { + fn test_deref() { assert_eq!( - ExtXStreamInf::new("http://www.example.com/", 1000).to_string(), - "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string() + ExtXStreamInf::new("http://www.example.com", 1000).bandwidth(), + 1000 + ); + } + + #[test] + fn test_deref_mut() { + assert_eq!( + ExtXStreamInf::new("http://www.example.com", 1000) + .set_bandwidth(1) + .bandwidth(), + 1 ); } } diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 8ca5504..fa90b8b 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -47,7 +47,7 @@ impl ExtXDiscontinuitySequence { /// /// assert_eq!(discontinuity_sequence.seq_num(), 5); /// ``` - pub const fn seq_num(&self) -> u64 { + pub const fn seq_num(self) -> u64 { self.0 } @@ -115,4 +115,12 @@ mod test { "#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap() ); } + + #[test] + fn test_seq_num() { + let mut sequence = ExtXDiscontinuitySequence::new(123); + assert_eq!(sequence.seq_num(), 123); + sequence.set_seq_num(1); + assert_eq!(sequence.seq_num(), 1); + } } diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index c841b19..7387577 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -45,7 +45,7 @@ impl ExtXMediaSequence { /// /// assert_eq!(media_sequence.seq_num(), 5); /// ``` - pub const fn seq_num(&self) -> u64 { + pub const fn seq_num(self) -> u64 { self.0 } @@ -113,4 +113,12 @@ mod test { "#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap() ); } + + #[test] + fn test_seq_num() { + let mut sequence = ExtXMediaSequence::new(123); + assert_eq!(sequence.seq_num(), 123); + sequence.set_seq_num(1); + assert_eq!(sequence.seq_num(), 1); + } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 37c3330..caefa24 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -98,11 +98,11 @@ impl FromStr for ExtXByteRange { let length = tokens[0].parse()?; let start = { - let mut result = None; if tokens.len() == 2 { - result = Some(tokens[1].parse()?); + Some(tokens[1].parse()?) + } else { + None } - result }; Ok(ExtXByteRange::new(length, start)) diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 0734b6f..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,17 +188,15 @@ impl FromStr for ExtXDateRange { } } - let id = id.ok_or(Error::missing_value("EXT-X-ID"))?; + let id = id.ok_or_else(|| Error::missing_value("ID"))?; let start_date = start_date - .ok_or(Error::missing_value("EXT-X-START-DATE"))? + .ok_or_else(|| Error::missing_value("START-DATE"))? .parse()?; - if end_on_next { - if class.is_none() { - return Err(Error::invalid_input()); - } + if end_on_next && class.is_none() { + return Err(Error::invalid_input()); } - Ok(ExtXDateRange { + Ok(Self { id, class, start_date, @@ -193,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 8524d81..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,17 +170,16 @@ 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.len() == 0 { + if tokens.is_empty() { return Err(Error::custom(format!( "failed to parse #EXTINF tag, couldn't split input: {:?}", input ))); } - 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/media_segment/map.rs b/src/tags/media_segment/map.rs index 9b1ce94..29377ed 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -93,7 +93,7 @@ impl FromStr for ExtXMap { } } - let uri = uri.ok_or(Error::missing_value("EXT-X-URI"))?; + let uri = uri.ok_or_else(|| Error::missing_value("EXT-X-URI"))?; Ok(ExtXMap { uri, range }) } } 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 e30fa9c..ed5f573 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, @@ -18,46 +18,99 @@ pub struct ExtXStart { impl ExtXStart { pub(crate) const PREFIX: &'static str = "#EXT-X-START:"; - /// Makes a new `ExtXStart` tag. + /// Makes a new [ExtXStart] tag. /// /// # Panic /// Panics if the time_offset value is infinite. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStart; + /// ExtXStart::new(20.123456); + /// ``` 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: SignedDecimalFloatingPoint::new(time_offset).unwrap(), + Self { + time_offset: SignedDecimalFloatingPoint::new(time_offset), precise: false, } } - /// Makes a new `ExtXStart` tag with the given `precise` flag. + /// Makes a new [ExtXStart] tag with the given `precise` flag. /// /// # Panic /// Panics if the time_offset value is infinite. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStart; + /// let start = ExtXStart::with_precise(20.123456, true); + /// assert_eq!(start.precise(), true); + /// ``` 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: SignedDecimalFloatingPoint::new(time_offset).unwrap(), + Self { + time_offset: SignedDecimalFloatingPoint::new(time_offset), precise, } } /// Returns the time offset of the media segments in the playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStart; + /// let start = ExtXStart::new(20.123456); + /// assert_eq!(start.time_offset(), 20.123456); + /// ``` pub const fn time_offset(&self) -> f64 { self.time_offset.as_f64() } + /// Sets the time offset of the media segments in the playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStart; + /// let mut start = ExtXStart::new(20.123456); + /// # assert_eq!(start.time_offset(), 20.123456); + /// + /// start.set_time_offset(1.0); + /// + /// assert_eq!(start.time_offset(), 1.0); + /// ``` + pub fn set_time_offset(&mut self, value: f64) -> &mut Self { + self.time_offset = SignedDecimalFloatingPoint::new(value); + self + } + /// Returns whether clients should not render media stream whose presentation times are /// prior to the specified time offset. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStart; + /// let start = ExtXStart::with_precise(20.123456, true); + /// assert_eq!(start.precise(), true); + /// ``` pub const fn precise(&self) -> bool { self.precise } + + /// Sets the `precise` flag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXStart; + /// let mut start = ExtXStart::new(20.123456); + /// # assert_eq!(start.precise(), false); + /// + /// start.set_precise(true); + /// + /// assert_eq!(start.precise(), true); + /// ``` + pub fn set_precise(&mut self, value: bool) -> &mut Self { + self.precise = value; + self + } } impl RequiredVersion for ExtXStart { @@ -97,9 +150,9 @@ impl FromStr for ExtXStart { } } - let time_offset = time_offset.ok_or(Error::missing_value("EXT-X-TIME-OFFSET"))?; + let time_offset = time_offset.ok_or_else(|| Error::missing_value("EXT-X-TIME-OFFSET"))?; - Ok(ExtXStart { + Ok(Self { time_offset, precise, }) @@ -147,5 +200,12 @@ mod test { ExtXStart::with_precise(1.23, true), "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(), ); + + assert_eq!( + ExtXStart::with_precise(1.23, true), + "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG" + .parse() + .unwrap(), + ); } } diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index 36b0a74..a87fc4e 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -16,11 +16,18 @@ pub struct ByteRange { impl ByteRange { /// Creates a new [ByteRange]. + /// + /// # Example + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// ByteRange::new(22, Some(12)); + /// ``` pub const fn new(length: usize, start: Option) -> Self { Self { length, start } } /// Returns the length of the range. + /// /// # Example /// ``` /// # use hls_m3u8::types::ByteRange; @@ -32,6 +39,7 @@ impl ByteRange { } /// Sets the length of the range. + /// /// # Example /// ``` /// # use hls_m3u8::types::ByteRange; @@ -48,6 +56,7 @@ impl ByteRange { } /// Returns the start of the range. + /// /// # Example /// ``` /// # use hls_m3u8::types::ByteRange; @@ -59,6 +68,7 @@ impl ByteRange { } /// Sets the start of the range. + /// /// # Example /// ``` /// # use hls_m3u8::types::ByteRange; @@ -97,13 +107,13 @@ impl FromStr for ByteRange { let length = tokens[0].parse()?; let start = { - let mut result = None; if tokens.len() == 2 { - result = Some(tokens[1].parse()?); + Some(tokens[1].parse()?) + } else { + None } - result }; - Ok(ByteRange::new(length, start)) + Ok(Self::new(length, start)) } } @@ -134,22 +144,30 @@ 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/closed_captions.rs b/src/types/closed_captions.rs index 7b8a4c3..b3425a4 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -2,7 +2,7 @@ use std::fmt; use std::str::FromStr; use crate::utils::{quote, unquote}; -use crate::{Error, Result}; +use crate::Error; /// The identifier of a closed captions group or its absence. /// @@ -10,7 +10,7 @@ use crate::{Error, Result}; /// /// [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, PartialEq, Eq, Hash)] +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum ClosedCaptions { GroupId(String), None, @@ -19,19 +19,20 @@ pub enum ClosedCaptions { impl fmt::Display for ClosedCaptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { - ClosedCaptions::GroupId(value) => write!(f, "{}", quote(value)), - ClosedCaptions::None => "NONE".fmt(f), + Self::GroupId(value) => write!(f, "{}", quote(value)), + Self::None => write!(f, "NONE"), } } } impl FromStr for ClosedCaptions { type Err = Error; - fn from_str(s: &str) -> Result { - if s == "NONE" { - Ok(ClosedCaptions::None) + + fn from_str(input: &str) -> Result { + if input.trim() == "NONE" { + Ok(Self::None) } else { - Ok(ClosedCaptions::GroupId(unquote(s))) + Ok(Self::GroupId(unquote(input))) } } } @@ -42,21 +43,23 @@ mod tests { #[test] fn test_display() { - let closed_captions = ClosedCaptions::None; - assert_eq!(closed_captions.to_string(), "NONE".to_string()); + assert_eq!(ClosedCaptions::None.to_string(), "NONE".to_string()); - let closed_captions = ClosedCaptions::GroupId("value".into()); - assert_eq!(closed_captions.to_string(), "\"value\"".to_string()); + assert_eq!( + ClosedCaptions::GroupId("value".into()).to_string(), + "\"value\"".to_string() + ); } #[test] fn test_parser() { - let closed_captions = ClosedCaptions::None; - assert_eq!(closed_captions, "NONE".parse::().unwrap()); - - let closed_captions = ClosedCaptions::GroupId("value".into()); assert_eq!( - closed_captions, + ClosedCaptions::None, + "NONE".parse::().unwrap() + ); + + assert_eq!( + ClosedCaptions::GroupId("value".into()), "\"value\"".parse::().unwrap() ); } diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 8cff64a..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,77 +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_positive() || !n.is_finite() { + pub fn new(value: f64) -> crate::Result { + if value.is_sign_negative() || value.is_infinite() { return Err(Error::invalid_input()); } - Ok(DecimalFloatingPoint(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 { - let secs = self.0 as u64; - let nanos = (self.0.fract() * 1_000_000_000.0) as u32; - Duration::new(secs, nanos) - } - - pub(crate) fn from_duration(duration: Duration) -> Self { - let n = - (duration.as_secs() as f64) + (f64::from(duration.subsec_nanos()) / 1_000_000_000.0); - DecimalFloatingPoint(n) - } -} - -impl From for DecimalFloatingPoint { - fn from(f: u32) -> Self { - DecimalFloatingPoint(f64::from(f)) - } } 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()); - } - let n = input.parse()?; - DecimalFloatingPoint::new(n) + 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() } } @@ -86,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(); @@ -108,5 +121,32 @@ mod tests { decimal_floating_point, "4.1".parse::().unwrap() ); + + assert!("1#".parse::().is_err()); + assert!("-1.0".parse::().is_err()); + } + + #[test] + fn test_new() { + assert!(DecimalFloatingPoint::new(::std::f64::INFINITY).is_err()); + assert!(DecimalFloatingPoint::new(-1.0).is_err()); + } + + #[test] + fn test_as_f64() { + assert_eq!(DecimalFloatingPoint::new(1.0).unwrap().as_f64(), 1.0); + } + + #[test] + fn test_from_inf() { + assert_eq!( + DecimalFloatingPoint::from(::std::f64::INFINITY), + DecimalFloatingPoint::new(0.0).unwrap() + ); + } + + #[test] + 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 fb5719e..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,9 +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<(usize, usize)> for DecimalResolution { + fn from(value: (usize, usize)) -> Self { + DecimalResolution::new(value.0, value.1) } } @@ -62,12 +65,9 @@ impl FromStr for DecimalResolution { ))); } - let width = tokens[0]; - let height = tokens[1]; - - Ok(DecimalResolution { - width: width.parse()?, - height: height.parse()?, + Ok(Self { + width: tokens[0].parse()?, + height: tokens[1].parse()?, }) } } @@ -118,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 2e1ea20..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 @@ -355,17 +362,17 @@ impl FromStr for DecryptionKey { } } - let method = method.ok_or(Error::missing_value("METHOD"))?; + let method = method.ok_or_else(|| Error::missing_value("METHOD"))?; if method != EncryptionMethod::None && uri.is_none() { return Err(Error::missing_value("URI")); } - Ok(DecryptionKey { + Ok(Self { method, uri, iv, key_format, - key_format_versions: key_format_versions.unwrap_or(KeyFormatVersions::new()), + 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] @@ -494,6 +534,29 @@ mod test { DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") .required_version(), ProtocolVersion::V1 - ) + ); + + assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .key_format(KeyFormat::Identity) + .key_format_versions(vec![1, 2, 3]) + .build() + .unwrap() + .required_version(), + ProtocolVersion::V5 + ); + + assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) + .build() + .unwrap() + .required_version(), + ProtocolVersion::V2 + ); } } diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 066dcc0..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::*; @@ -104,5 +77,7 @@ mod tests { EncryptionMethod::None, "NONE".parse::().unwrap() ); + + assert!("unknown".parse::().is_err()); } } diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index 0b3bbe9..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::*; @@ -56,5 +34,7 @@ mod tests { let level = HdcpLevel::None; assert_eq!(level, "NONE".parse::().unwrap()); + + assert!("unk".parse::().is_err()); } } diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index cdff1f5..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,85 +78,96 @@ pub enum InStreamId { Service63, } -impl fmt::Display for InStreamId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - format!("{:?}", self).to_uppercase().fmt(f) - } -} +#[cfg(test)] +mod tests { + use super::*; -impl FromStr for InStreamId { - type Err = Error; + macro_rules! gen_tests { + ( $($string:expr => $enum:expr),* ) => { + #[test] + fn test_display() { + $( + assert_eq!($enum.to_string(), $string.to_string()); + )* + } - fn from_str(input: &str) -> Result { - Ok(match input { - "CC1" => InStreamId::Cc1, - "CC2" => InStreamId::Cc2, - "CC3" => InStreamId::Cc3, - "CC4" => InStreamId::Cc4, - "SERVICE1" => InStreamId::Service1, - "SERVICE2" => InStreamId::Service2, - "SERVICE3" => InStreamId::Service3, - "SERVICE4" => InStreamId::Service4, - "SERVICE5" => InStreamId::Service5, - "SERVICE6" => InStreamId::Service6, - "SERVICE7" => InStreamId::Service7, - "SERVICE8" => InStreamId::Service8, - "SERVICE9" => InStreamId::Service9, - "SERVICE10" => InStreamId::Service10, - "SERVICE11" => InStreamId::Service11, - "SERVICE12" => InStreamId::Service12, - "SERVICE13" => InStreamId::Service13, - "SERVICE14" => InStreamId::Service14, - "SERVICE15" => InStreamId::Service15, - "SERVICE16" => InStreamId::Service16, - "SERVICE17" => InStreamId::Service17, - "SERVICE18" => InStreamId::Service18, - "SERVICE19" => InStreamId::Service19, - "SERVICE20" => InStreamId::Service20, - "SERVICE21" => InStreamId::Service21, - "SERVICE22" => InStreamId::Service22, - "SERVICE23" => InStreamId::Service23, - "SERVICE24" => InStreamId::Service24, - "SERVICE25" => InStreamId::Service25, - "SERVICE26" => InStreamId::Service26, - "SERVICE27" => InStreamId::Service27, - "SERVICE28" => InStreamId::Service28, - "SERVICE29" => InStreamId::Service29, - "SERVICE30" => InStreamId::Service30, - "SERVICE31" => InStreamId::Service31, - "SERVICE32" => InStreamId::Service32, - "SERVICE33" => InStreamId::Service33, - "SERVICE34" => InStreamId::Service34, - "SERVICE35" => InStreamId::Service35, - "SERVICE36" => InStreamId::Service36, - "SERVICE37" => InStreamId::Service37, - "SERVICE38" => InStreamId::Service38, - "SERVICE39" => InStreamId::Service39, - "SERVICE40" => InStreamId::Service40, - "SERVICE41" => InStreamId::Service41, - "SERVICE42" => InStreamId::Service42, - "SERVICE43" => InStreamId::Service43, - "SERVICE44" => InStreamId::Service44, - "SERVICE45" => InStreamId::Service45, - "SERVICE46" => InStreamId::Service46, - "SERVICE47" => InStreamId::Service47, - "SERVICE48" => InStreamId::Service48, - "SERVICE49" => InStreamId::Service49, - "SERVICE50" => InStreamId::Service50, - "SERVICE51" => InStreamId::Service51, - "SERVICE52" => InStreamId::Service52, - "SERVICE53" => InStreamId::Service53, - "SERVICE54" => InStreamId::Service54, - "SERVICE55" => InStreamId::Service55, - "SERVICE56" => InStreamId::Service56, - "SERVICE57" => InStreamId::Service57, - "SERVICE58" => InStreamId::Service58, - "SERVICE59" => InStreamId::Service59, - "SERVICE60" => InStreamId::Service60, - "SERVICE61" => InStreamId::Service61, - "SERVICE62" => InStreamId::Service62, - "SERVICE63" => InStreamId::Service63, - _ => return Err(Error::custom(format!("Unknown instream id: {:?}", input))), - }) + #[test] + fn test_parser() { + $( + assert_eq!($enum, $string.parse::().unwrap()); + )* + assert!("invalid_input".parse::().is_err()); + } + }; } + + gen_tests![ + "CC1" => InStreamId::Cc1, + "CC2" => InStreamId::Cc2, + "CC3" => InStreamId::Cc3, + "CC4" => InStreamId::Cc4, + "SERVICE1" => InStreamId::Service1, + "SERVICE2" => InStreamId::Service2, + "SERVICE3" => InStreamId::Service3, + "SERVICE4" => InStreamId::Service4, + "SERVICE5" => InStreamId::Service5, + "SERVICE6" => InStreamId::Service6, + "SERVICE7" => InStreamId::Service7, + "SERVICE8" => InStreamId::Service8, + "SERVICE9" => InStreamId::Service9, + "SERVICE10" => InStreamId::Service10, + "SERVICE11" => InStreamId::Service11, + "SERVICE12" => InStreamId::Service12, + "SERVICE13" => InStreamId::Service13, + "SERVICE14" => InStreamId::Service14, + "SERVICE15" => InStreamId::Service15, + "SERVICE16" => InStreamId::Service16, + "SERVICE17" => InStreamId::Service17, + "SERVICE18" => InStreamId::Service18, + "SERVICE19" => InStreamId::Service19, + "SERVICE20" => InStreamId::Service20, + "SERVICE21" => InStreamId::Service21, + "SERVICE22" => InStreamId::Service22, + "SERVICE23" => InStreamId::Service23, + "SERVICE24" => InStreamId::Service24, + "SERVICE25" => InStreamId::Service25, + "SERVICE26" => InStreamId::Service26, + "SERVICE27" => InStreamId::Service27, + "SERVICE28" => InStreamId::Service28, + "SERVICE29" => InStreamId::Service29, + "SERVICE30" => InStreamId::Service30, + "SERVICE31" => InStreamId::Service31, + "SERVICE32" => InStreamId::Service32, + "SERVICE33" => InStreamId::Service33, + "SERVICE34" => InStreamId::Service34, + "SERVICE35" => InStreamId::Service35, + "SERVICE36" => InStreamId::Service36, + "SERVICE37" => InStreamId::Service37, + "SERVICE38" => InStreamId::Service38, + "SERVICE39" => InStreamId::Service39, + "SERVICE40" => InStreamId::Service40, + "SERVICE41" => InStreamId::Service41, + "SERVICE42" => InStreamId::Service42, + "SERVICE43" => InStreamId::Service43, + "SERVICE44" => InStreamId::Service44, + "SERVICE45" => InStreamId::Service45, + "SERVICE46" => InStreamId::Service46, + "SERVICE47" => InStreamId::Service47, + "SERVICE48" => InStreamId::Service48, + "SERVICE49" => InStreamId::Service49, + "SERVICE50" => InStreamId::Service50, + "SERVICE51" => InStreamId::Service51, + "SERVICE52" => InStreamId::Service52, + "SERVICE53" => InStreamId::Service53, + "SERVICE54" => InStreamId::Service54, + "SERVICE55" => InStreamId::Service55, + "SERVICE56" => InStreamId::Service56, + "SERVICE57" => InStreamId::Service57, + "SERVICE58" => InStreamId::Service58, + "SERVICE59" => InStreamId::Service59, + "SERVICE60" => InStreamId::Service60, + "SERVICE61" => InStreamId::Service61, + "SERVICE62" => InStreamId::Service62, + "SERVICE63" => InStreamId::Service63 + ]; } diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 7c64323..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 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; + 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)?; + 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 8448905..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() } } @@ -66,7 +66,7 @@ impl FromStr for KeyFormatVersions { fn from_str(input: &str) -> Result { let mut result = unquote(input) - .split("/") + .split('/') .filter_map(|v| v.parse().ok()) .collect::>(); 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 a10d893..9623539 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -1,55 +1,108 @@ -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 { - /// Makes a new `SignedDecimalFloatingPoint` instance. + /// Makes a new [SignedDecimalFloatingPoint] instance. /// - /// # Errors - /// - /// The given value must 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_finite() { - Err(Error::invalid_input()) - } else { - Ok(SignedDecimalFloatingPoint(n)) + /// # Panics + /// The given value must be finite, otherwise this function will panic! + pub fn new(value: f64) -> Self { + if value.is_infinite() { + panic!("Floating point value must be finite!"); } + 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 } } -impl From for SignedDecimalFloatingPoint { - fn from(f: i32) -> Self { - SignedDecimalFloatingPoint(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 { - SignedDecimalFloatingPoint::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!( + SignedDecimalFloatingPoint::new(1.0).to_string(), + 1.0f64.to_string() + ); + } + + #[test] + #[should_panic] + fn test_new_panic() { + SignedDecimalFloatingPoint::new(::std::f64::INFINITY); + } + + #[test] + fn test_parser() { + assert_eq!( + SignedDecimalFloatingPoint::new(1.0), + "1.0".parse::().unwrap() + ); + + assert!("garbage".parse::().is_err()); + } + + #[test] + 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 8b391a3..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()?), @@ -272,7 +273,7 @@ impl FromStr for StreamInf { } } - let bandwidth = bandwidth.ok_or(Error::missing_value("BANDWIDTH"))?; + let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?; Ok(Self { bandwidth, @@ -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 bb3662c..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`) @@ -61,14 +77,7 @@ mod tests { 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 + assert!(parse_yes_or_no("garbage").is_err()); } #[test] @@ -99,5 +108,7 @@ mod tests { let input = tag(input, "A").unwrap(); assert_eq!(input, "SampleString"); + + assert!(tag(input, "B").is_err()); } }