diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 269b5ab..ec69f4a 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: nightly - 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..1a30fe2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,16 @@ language: rust -sudo: required + +cache: cargo + +before_cache: | + cargo install cargo-tarpaulin || echo "cargo-tarpaulin already installed" + cargo install cargo-update || echo "cargo-update already installed" + cargo install cargo-audit || echo "cargo-audit already installed" + cargo install-update --all + +# before_cache: +# - rm -rf /home/travis/.cargo/registry + rust: - stable - beta @@ -8,32 +19,19 @@ matrix: allow_failures: - rust: nightly -env: - global: - - RUSTFLAGS="-C link-dead-code" +script: | + cargo clean + cargo build + cargo test -addons: - apt: - packages: - - libcurl4-openssl-dev - - libelf-dev - - libdw-dev - - cmake - - gcc - - binutils-dev - - libiberty-dev + # it's enough to run this once: + if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then + cargo audit + fi 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/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..4a92eff --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,10 @@ +error_on_unformatted = true +edition = "2018" +fn_single_line = true +force_multiline_blocks = true +format_code_in_doc_comments = true +format_macro_matchers = true +match_arm_blocks = true +reorder_impl_items = true +use_field_init_shorthand = true +wrap_comments = true diff --git a/src/attribute.rs b/src/attribute.rs index 2da9be6..91d42e6 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -8,41 +8,31 @@ use crate::Error; pub struct AttributePairs(HashMap); impl AttributePairs { - pub fn new() -> Self { - Self::default() - } + pub fn new() -> Self { Self::default() } } impl Deref for AttributePairs { type Target = HashMap; - fn deref(&self) -> &Self::Target { - &self.0 - } + fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for AttributePairs { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl IntoIterator for AttributePairs { - type Item = (String, String); type IntoIter = ::std::collections::hash_map::IntoIter; + type Item = (String, String); - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl<'a> IntoIterator for &'a AttributePairs { - type Item = (&'a String, &'a String); type IntoIter = ::std::collections::hash_map::Iter<'a, String, String>; + type Item = (&'a String, &'a String); - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } + fn into_iter(self) -> Self::IntoIter { self.0.iter() } } impl FromStr for AttributePairs { @@ -55,11 +45,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()); } @@ -87,11 +80,11 @@ fn split(value: &str, terminator: char) -> Vec { temp_string.push(c); } k if (k == terminator) => { - if !inside_quotes { + if inside_quotes { + temp_string.push(c); + } else { result.push(temp_string); temp_string = String::new(); - } else { - temp_string.push(c); } } _ => { @@ -122,6 +115,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 +134,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..0d85eca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,3 @@ -use std::error; use std::fmt; use failure::{Backtrace, Context, Fail}; @@ -6,7 +5,7 @@ use failure::{Backtrace, Context, Fail}; /// This crate specific `Result` type. pub type Result = std::result::Result; -/// The ErrorKind. +/// The [`ErrorKind`]. #[derive(Debug, Fail, Clone, PartialEq, Eq)] pub enum ErrorKind { #[fail(display = "ChronoParseError: {}", _0)] @@ -73,6 +72,10 @@ pub enum ErrorKind { /// An attribute is missing. MissingAttribute(String), + #[fail(display = "Unexpected Attribute: {:?}", _0)] + /// An unexpected value. + UnexpectedAttribute(String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -90,49 +93,34 @@ pub struct Error { } impl Fail for Error { - fn cause(&self) -> Option<&dyn Fail> { - self.inner.cause() - } + fn cause(&self) -> Option<&dyn Fail> { self.inner.cause() } - fn backtrace(&self) -> Option<&Backtrace> { - self.inner.backtrace() - } + fn backtrace(&self) -> Option<&Backtrace> { self.inner.backtrace() } } impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.fmt(f) } } impl From for Error { - fn from(kind: ErrorKind) -> Error { - Error::from(Context::new(kind)) - } + fn from(kind: ErrorKind) -> Error { Error::from(Context::new(kind)) } } impl From> for Error { - fn from(inner: Context) -> Error { - Error { inner } - } + fn from(inner: Context) -> Error { Error { inner } } } 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())) } - pub(crate) fn invalid_input() -> Self { - Self::from(ErrorKind::InvalidInput) + pub(crate) fn unexpected_attribute(value: T) -> Self { + Self::from(ErrorKind::UnexpectedAttribute(value.to_string())) } + pub(crate) fn invalid_input() -> Self { Self::from(ErrorKind::InvalidInput) } + pub(crate) fn parse_int_error(value: T) -> Self { Self::from(ErrorKind::ParseIntError(value.to_string())) } @@ -167,9 +155,7 @@ impl Error { Self::from(ErrorKind::UnknownProtocolVersion(value.to_string())) } - pub(crate) fn io(value: T) -> Self { - Self::from(ErrorKind::Io(value.to_string())) - } + pub(crate) fn io(value: T) -> Self { Self::from(ErrorKind::Io(value.to_string())) } pub(crate) fn required_version(required_version: T, specified_version: U) -> Self where @@ -196,25 +182,23 @@ impl Error { } impl From<::std::num::ParseIntError> for Error { - fn from(value: ::std::num::ParseIntError) -> Self { - Error::parse_int_error(value) - } + fn from(value: ::std::num::ParseIntError) -> Self { Error::parse_int_error(value) } } impl From<::std::num::ParseFloatError> for Error { - fn from(value: ::std::num::ParseFloatError) -> Self { - Error::parse_float_error(value) - } + fn from(value: ::std::num::ParseFloatError) -> Self { Error::parse_float_error(value) } } impl From<::std::io::Error> for Error { - fn from(value: ::std::io::Error) -> Self { - Error::io(value) - } + fn from(value: ::std::io::Error) -> Self { Error::io(value) } } impl From<::chrono::ParseError> for Error { - fn from(value: ::chrono::ParseError) -> Self { - Error::chrono(value) + fn from(value: ::chrono::ParseError) -> Self { 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..8923f44 100644 --- a/src/line.rs +++ b/src/line.rs @@ -9,9 +9,7 @@ use crate::Error; pub struct Lines(Vec); impl Lines { - pub fn new() -> Self { - Self::default() - } + pub fn new() -> Self { Self::default() } } impl FromStr for Lines { @@ -26,8 +24,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 +36,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 @@ -66,26 +63,20 @@ impl FromStr for Lines { } impl IntoIterator for Lines { - type Item = Line; type IntoIter = ::std::vec::IntoIter; + type Item = Line; - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } + fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl Deref for Lines { type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } + fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Lines { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } #[derive(Debug, PartialEq, Eq)] @@ -155,53 +146,53 @@ impl fmt::Display for Tag { impl FromStr for Tag { type Err = Error; - fn from_str(s: &str) -> Result { - if s.starts_with(tags::ExtM3u::PREFIX) { - s.parse().map(Tag::ExtM3u) - } else if s.starts_with(tags::ExtXVersion::PREFIX) { - s.parse().map(Tag::ExtXVersion) - } else if s.starts_with(tags::ExtInf::PREFIX) { - s.parse().map(Tag::ExtInf) - } else if s.starts_with(tags::ExtXByteRange::PREFIX) { - s.parse().map(Tag::ExtXByteRange) - } else if s.starts_with(tags::ExtXDiscontinuity::PREFIX) { - s.parse().map(Tag::ExtXDiscontinuity) - } else if s.starts_with(tags::ExtXKey::PREFIX) { - s.parse().map(Tag::ExtXKey) - } else if s.starts_with(tags::ExtXMap::PREFIX) { - s.parse().map(Tag::ExtXMap) - } else if s.starts_with(tags::ExtXProgramDateTime::PREFIX) { - s.parse().map(Tag::ExtXProgramDateTime) - } else if s.starts_with(tags::ExtXTargetDuration::PREFIX) { - s.parse().map(Tag::ExtXTargetDuration) - } else if s.starts_with(tags::ExtXDateRange::PREFIX) { - s.parse().map(Tag::ExtXDateRange) - } else if s.starts_with(tags::ExtXMediaSequence::PREFIX) { - s.parse().map(Tag::ExtXMediaSequence) - } else if s.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) { - s.parse().map(Tag::ExtXDiscontinuitySequence) - } else if s.starts_with(tags::ExtXEndList::PREFIX) { - s.parse().map(Tag::ExtXEndList) - } else if s.starts_with(tags::ExtXPlaylistType::PREFIX) { - s.parse().map(Tag::ExtXPlaylistType) - } else if s.starts_with(tags::ExtXIFramesOnly::PREFIX) { - s.parse().map(Tag::ExtXIFramesOnly) - } else if s.starts_with(tags::ExtXMedia::PREFIX) { - s.parse().map(Tag::ExtXMedia) - } else if s.starts_with(tags::ExtXStreamInf::PREFIX) { - s.parse().map(Tag::ExtXStreamInf) - } else if s.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { - s.parse().map(Tag::ExtXIFrameStreamInf) - } else if s.starts_with(tags::ExtXSessionData::PREFIX) { - s.parse().map(Tag::ExtXSessionData) - } else if s.starts_with(tags::ExtXSessionKey::PREFIX) { - s.parse().map(Tag::ExtXSessionKey) - } else if s.starts_with(tags::ExtXIndependentSegments::PREFIX) { - s.parse().map(Tag::ExtXIndependentSegments) - } else if s.starts_with(tags::ExtXStart::PREFIX) { - s.parse().map(Tag::ExtXStart) + fn from_str(input: &str) -> Result { + if input.starts_with(tags::ExtM3u::PREFIX) { + input.parse().map(Tag::ExtM3u) + } else if input.starts_with(tags::ExtXVersion::PREFIX) { + input.parse().map(Tag::ExtXVersion) + } else if input.starts_with(tags::ExtInf::PREFIX) { + input.parse().map(Tag::ExtInf) + } else if input.starts_with(tags::ExtXByteRange::PREFIX) { + input.parse().map(Tag::ExtXByteRange) + } else if input.starts_with(tags::ExtXDiscontinuity::PREFIX) { + input.parse().map(Tag::ExtXDiscontinuity) + } else if input.starts_with(tags::ExtXKey::PREFIX) { + input.parse().map(Tag::ExtXKey) + } else if input.starts_with(tags::ExtXMap::PREFIX) { + input.parse().map(Tag::ExtXMap) + } else if input.starts_with(tags::ExtXProgramDateTime::PREFIX) { + input.parse().map(Tag::ExtXProgramDateTime) + } else if input.starts_with(tags::ExtXTargetDuration::PREFIX) { + input.parse().map(Tag::ExtXTargetDuration) + } else if input.starts_with(tags::ExtXDateRange::PREFIX) { + input.parse().map(Tag::ExtXDateRange) + } else if input.starts_with(tags::ExtXMediaSequence::PREFIX) { + input.parse().map(Tag::ExtXMediaSequence) + } else if input.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) { + input.parse().map(Tag::ExtXDiscontinuitySequence) + } else if input.starts_with(tags::ExtXEndList::PREFIX) { + input.parse().map(Tag::ExtXEndList) + } else if input.starts_with(tags::ExtXPlaylistType::PREFIX) { + input.parse().map(Tag::ExtXPlaylistType) + } else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) { + input.parse().map(Tag::ExtXIFramesOnly) + } else if input.starts_with(tags::ExtXMedia::PREFIX) { + input.parse().map(Tag::ExtXMedia).map_err(Error::custom) + } else if input.starts_with(tags::ExtXStreamInf::PREFIX) { + input.parse().map(Tag::ExtXStreamInf) + } else if input.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { + input.parse().map(Tag::ExtXIFrameStreamInf) + } else if input.starts_with(tags::ExtXSessionData::PREFIX) { + input.parse().map(Tag::ExtXSessionData) + } else if input.starts_with(tags::ExtXSessionKey::PREFIX) { + input.parse().map(Tag::ExtXSessionKey) + } else if input.starts_with(tags::ExtXIndependentSegments::PREFIX) { + input.parse().map(Tag::ExtXIndependentSegments) + } else if input.starts_with(tags::ExtXStart::PREFIX) { + input.parse().map(Tag::ExtXStart) } else { - Ok(Tag::Unknown(s.to_string())) + Ok(Tag::Unknown(input.to_string())) } } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 6f52f0f..2af4ff7 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -21,8 +21,8 @@ pub struct MasterPlaylist { #[builder(default, setter(name = "version"))] /// Sets the protocol compatibility version of the resulting playlist. /// - /// If the resulting playlist has tags which requires a compatibility version greater than - /// `version`, + /// If the resulting playlist has tags which requires a compatibility + /// version greater than `version`, /// `build()` method will fail with an `ErrorKind::InvalidInput` error. /// /// The default is the maximum version among the tags in the playlist. @@ -47,14 +47,10 @@ pub struct MasterPlaylist { impl MasterPlaylist { /// Returns a Builder for a MasterPlaylist. - pub fn builder() -> MasterPlaylistBuilder { - MasterPlaylistBuilder::default() - } + pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub const fn version_tag(&self) -> ExtXVersion { - self.version_tag - } + pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. pub const fn independent_segments_tag(&self) -> Option { @@ -62,19 +58,13 @@ impl MasterPlaylist { } /// Returns the `EXT-X-START` tag contained in the playlist. - pub const fn start_tag(&self) -> Option { - self.start_tag - } + pub const fn start_tag(&self) -> Option { self.start_tag } /// Returns the `EXT-X-MEDIA` tags contained in the playlist. - pub fn media_tags(&self) -> &[ExtXMedia] { - &self.media_tags - } + pub fn media_tags(&self) -> &[ExtXMedia] { &self.media_tags } /// Returns the `EXT-X-STREAM-INF` tags contained in the playlist. - pub fn stream_inf_tags(&self) -> &[ExtXStreamInf] { - &self.stream_inf_tags - } + pub fn stream_inf_tags(&self) -> &[ExtXStreamInf] { &self.stream_inf_tags } /// Returns the `EXT-X-I-FRAME-STREAM-INF` tags contained in the playlist. pub fn i_frame_stream_inf_tags(&self) -> &[ExtXIFrameStreamInf] { @@ -82,20 +72,14 @@ impl MasterPlaylist { } /// Returns the `EXT-X-SESSION-DATA` tags contained in the playlist. - pub fn session_data_tags(&self) -> &[ExtXSessionData] { - &self.session_data_tags - } + pub fn session_data_tags(&self) -> &[ExtXSessionData] { &self.session_data_tags } /// Returns the `EXT-X-SESSION-KEY` tags contained in the playlist. - pub fn session_key_tags(&self) -> &[ExtXSessionKey] { - &self.session_key_tags - } + pub fn session_key_tags(&self) -> &[ExtXSessionKey] { &self.session_key_tags } } impl RequiredVersion for MasterPlaylist { - fn required_version(&self) -> ProtocolVersion { - self.version_tag.version() - } + fn required_version(&self) -> ProtocolVersion { self.version_tag.version() } } impl MasterPlaylistBuilder { @@ -103,7 +87,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 +148,7 @@ impl MasterPlaylistBuilder { .flatten(), ) .max() - .unwrap_or(ProtocolVersion::latest()) + .unwrap_or_else(ProtocolVersion::latest) } fn validate_stream_inf_tags(&self) -> crate::Result<()> { @@ -188,24 +172,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..31dde47 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -53,7 +53,8 @@ pub struct MediaPlaylist { end_list_tag: Option, /// Sets all [MediaSegment]s. segments: Vec, - /// Sets the allowable excess duration of each media segment in the associated playlist. + /// Sets the allowable excess duration of each media segment in the + /// associated playlist. /// /// # Error /// If there is a media segment of which duration exceeds @@ -70,7 +71,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 +110,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 +123,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 +201,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. @@ -221,38 +222,28 @@ impl MediaPlaylistBuilder { impl MediaPlaylist { /// Creates a [MediaPlaylistBuilder]. - pub fn builder() -> MediaPlaylistBuilder { - MediaPlaylistBuilder::default() - } + pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() } + /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub const fn version_tag(&self) -> ExtXVersion { - self.version_tag - } + pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } /// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist. - pub const fn target_duration_tag(&self) -> ExtXTargetDuration { - self.target_duration_tag - } + pub const fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag } /// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist. - pub const fn media_sequence_tag(&self) -> Option { - self.media_sequence_tag - } + pub const fn media_sequence_tag(&self) -> Option { self.media_sequence_tag } - /// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the playlist. + /// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the + /// playlist. pub const fn discontinuity_sequence_tag(&self) -> Option { self.discontinuity_sequence_tag } /// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist. - pub const fn playlist_type_tag(&self) -> Option { - self.playlist_type_tag - } + pub const fn playlist_type_tag(&self) -> Option { self.playlist_type_tag } /// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist. - pub const fn i_frames_only_tag(&self) -> Option { - self.i_frames_only_tag - } + pub const fn i_frames_only_tag(&self) -> Option { self.i_frames_only_tag } /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. pub const fn independent_segments_tag(&self) -> Option { @@ -260,19 +251,13 @@ impl MediaPlaylist { } /// Returns the `EXT-X-START` tag contained in the playlist. - pub const fn start_tag(&self) -> Option { - self.start_tag - } + pub const fn start_tag(&self) -> Option { self.start_tag } /// Returns the `EXT-X-ENDLIST` tag contained in the playlist. - pub const fn end_list_tag(&self) -> Option { - self.end_list_tag - } + pub const fn end_list_tag(&self) -> Option { self.end_list_tag } /// Returns the media segments contained in the playlist. - pub fn segments(&self) -> &[MediaSegment] { - &self.segments - } + pub fn segments(&self) -> &[MediaSegment] { &self.segments } } impl fmt::Display for MediaPlaylist { diff --git a/src/media_segment.rs b/src/media_segment.rs index e1d51bf..8f85a3c 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -76,48 +76,34 @@ impl fmt::Display for MediaSegment { impl MediaSegment { /// Creates a [MediaSegmentBuilder]. - pub fn builder() -> MediaSegmentBuilder { - MediaSegmentBuilder::default() - } + pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } + /// Returns the URI of the media segment. - pub const fn uri(&self) -> &String { - &self.uri - } + pub const fn uri(&self) -> &String { &self.uri } /// Returns the `EXT-X-INF` tag associated with the media segment. - pub const fn inf_tag(&self) -> &ExtInf { - &self.inf_tag - } + pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag } /// Returns the `EXT-X-BYTERANGE` tag associated with the media segment. - pub const fn byte_range_tag(&self) -> Option { - self.byte_range_tag - } + pub const fn byte_range_tag(&self) -> Option { self.byte_range_tag } /// Returns the `EXT-X-DATERANGE` tag associated with the media segment. - pub fn date_range_tag(&self) -> Option<&ExtXDateRange> { - self.date_range_tag.as_ref() - } + pub fn date_range_tag(&self) -> Option<&ExtXDateRange> { self.date_range_tag.as_ref() } /// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment. - pub const fn discontinuity_tag(&self) -> Option { - self.discontinuity_tag - } + pub const fn discontinuity_tag(&self) -> Option { self.discontinuity_tag } - /// Returns the `EXT-X-PROGRAM-DATE-TIME` tag associated with the media segment. + /// Returns the `EXT-X-PROGRAM-DATE-TIME` tag associated with the media + /// segment. pub fn program_date_time_tag(&self) -> Option<&ExtXProgramDateTime> { self.program_date_time_tag.as_ref() } /// Returns the `EXT-X-MAP` tag associated with the media segment. - pub fn map_tag(&self) -> Option<&ExtXMap> { - self.map_tag.as_ref() - } + pub fn map_tag(&self) -> Option<&ExtXMap> { self.map_tag.as_ref() } /// Returns the `EXT-X-KEY` tags associated with the media segment. - pub fn key_tags(&self) -> &[ExtXKey] { - &self.key_tags - } + pub fn key_tags(&self) -> &[ExtXKey] { &self.key_tags } } impl RequiredVersion for MediaSegment { diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index ac870b1..2fc1523 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -5,17 +5,35 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// # [4.3.1.1. EXTM3U] -/// The [ExtM3u] tag indicates that the file is an Extended [M3U] +/// # [4.4.1.1. EXTM3U] +/// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]** /// Playlist file. +/// It is the at the start of every [`Media Playlist`] and [`Master Playlist`]. /// -/// Its format is: -/// ```text -/// #EXTM3U +/// # Examples +/// Parsing from a [`str`]: +/// ``` +/// # use failure::Error; +/// # use hls_m3u8::tags::ExtM3u; +/// # +/// # fn main() -> Result<(), Error> { +/// assert_eq!("#EXTM3U".parse::()?, ExtM3u); +/// # +/// # Ok(()) +/// # } +/// ``` +/// Converting to a [`str`]: +/// ``` +/// # use hls_m3u8::tags::ExtM3u; +/// # +/// assert_eq!("#EXTM3U".to_string(), ExtM3u.to_string()); /// ``` /// -/// [M3U]: https://en.wikipedia.org/wiki/M3U -/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 +/// [`Media Playlist`]: crate::MediaPlaylist +/// [`Master Playlist`]: crate::MasterPlaylist +/// [`M3U`]: https://en.wikipedia.org/wiki/M3U +/// [4.4.1.1. EXTM3U]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.1.1 #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct ExtM3u; @@ -23,16 +41,13 @@ impl ExtM3u { pub(crate) const PREFIX: &'static str = "#EXTM3U"; } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtM3u { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtM3u { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX) } } impl FromStr for ExtM3u { @@ -40,7 +55,7 @@ impl FromStr for ExtM3u { fn from_str(input: &str) -> Result { tag(input, Self::PREFIX)?; - Ok(ExtM3u) + Ok(Self) } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index bff523f..d473810 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -5,40 +5,62 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// # [4.3.1.2. EXT-X-VERSION] -/// The [ExtXVersion] tag indicates the compatibility version of the -/// Playlist file, its associated media, and its server. +/// # [4.4.1.2. EXT-X-VERSION] +/// The [`ExtXVersion`] tag indicates the compatibility version of the +/// [`Master Playlist`] or [`Media Playlist`] file. +/// It applies to the entire Playlist. /// -/// The [ExtXVersion] tag applies to the entire Playlist file. Its -/// format is: -/// -/// ```text -/// #EXT-X-VERSION: +/// # Examples +/// Parsing from a [`str`]: /// ``` -/// where `n` is an integer indicating the protocol compatibility version -/// number. +/// # use failure::Error; +/// # use hls_m3u8::tags::ExtXVersion; +/// # +/// # fn main() -> Result<(), Error> { +/// use hls_m3u8::types::ProtocolVersion; /// -/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 +/// assert_eq!( +/// "#EXT-X-VERSION:5".parse::()?, +/// ExtXVersion::new(ProtocolVersion::V5) +/// ); +/// # +/// # Ok(()) +/// # } +/// ``` +/// Converting to a [`str`]: +/// ``` +/// # use hls_m3u8::tags::ExtXVersion; +/// # +/// use hls_m3u8::types::ProtocolVersion; +/// +/// assert_eq!( +/// "#EXT-X-VERSION:5".to_string(), +/// ExtXVersion::new(ProtocolVersion::V5).to_string() +/// ); +/// ``` +/// +/// [`Media Playlist`]: crate::MediaPlaylist +/// [`Master Playlist`]: crate::MasterPlaylist +/// [4.4.1.2. EXT-X-VERSION]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.1.2 #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct ExtXVersion(ProtocolVersion); impl ExtXVersion { pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:"; - /// Makes a new [ExtXVersion] tag. + /// Makes a new [`ExtXVersion`] tag. /// /// # Example /// ``` /// # use hls_m3u8::tags::ExtXVersion; /// use hls_m3u8::types::ProtocolVersion; /// - /// let version_tag = ExtXVersion::new(ProtocolVersion::V2); + /// let version = ExtXVersion::new(ProtocolVersion::V2); /// ``` - pub const fn new(version: ProtocolVersion) -> Self { - Self(version) - } + pub const fn new(version: ProtocolVersion) -> Self { Self(version) } - /// Returns the protocol compatibility version of the playlist, containing this tag. + /// Returns the [`ProtocolVersion`] of the playlist, containing this tag. /// /// # Example /// ``` @@ -50,33 +72,24 @@ impl ExtXVersion { /// ProtocolVersion::V6 /// ); /// ``` - pub const fn version(&self) -> ProtocolVersion { - self.0 - } + pub const fn version(self) -> ProtocolVersion { self.0 } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXVersion { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXVersion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.0) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } } impl Default for ExtXVersion { - fn default() -> Self { - Self(ProtocolVersion::V1) - } + fn default() -> Self { Self(ProtocolVersion::V1) } } impl From for ExtXVersion { - fn from(value: ProtocolVersion) -> Self { - Self(value) - } + fn from(value: ProtocolVersion) -> Self { Self(value) } } impl FromStr for ExtXVersion { @@ -84,7 +97,7 @@ impl FromStr for ExtXVersion { fn from_str(input: &str) -> Result { let version = tag(input, Self::PREFIX)?.parse()?; - Ok(ExtXVersion::new(version)) + Ok(Self::new(version)) } } @@ -115,4 +128,12 @@ mod test { ProtocolVersion::V1 ); } + + #[test] + fn test_default_and_from() { + assert_eq!( + ExtXVersion::default(), + ExtXVersion::from(ProtocolVersion::V1) + ); + } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 167b358..f886430 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -7,21 +7,18 @@ use crate::types::{ProtocolVersion, RequiredVersion, StreamInf}; use crate::utils::{quote, tag, unquote}; use crate::Error; -/// # [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] -/// The [ExtXIFrameStreamInf] tag identifies a [Media Playlist] file -/// containing the I-frames of a multimedia presentation. It stands -/// alone, in that it does not apply to a particular `URI` in the [Master Playlist]. +/// # [4.4.5.3. EXT-X-I-FRAME-STREAM-INF] +/// The [`ExtXIFrameStreamInf`] tag identifies a [`Media Playlist`] file, +/// containing the I-frames of a multimedia presentation. /// -/// Its format is: +/// I-frames are encoded video frames, whose decoding +/// does not depend on any other frame. /// -/// ```text -/// #EXT-X-I-FRAME-STREAM-INF: -/// ``` -/// -/// [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)] +/// [`Master Playlist`]: crate::MasterPlaylist +/// [`Media Playlist`]: crate::MediaPlaylist +/// [4.4.5.3. EXT-X-I-FRAME-STREAM-INF]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.3 +#[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXIFrameStreamInf { uri: String, stream_inf: StreamInf, @@ -30,7 +27,13 @@ pub struct ExtXIFrameStreamInf { impl ExtXIFrameStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; - /// Makes a new [ExtXIFrameStreamInf] tag. + /// 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(), @@ -38,20 +41,19 @@ impl ExtXIFrameStreamInf { } } - /// Returns the `URI`, that identifies the associated media playlist. + /// Returns the `URI`, that identifies the associated [`media playlist`]. /// /// # 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()); /// ``` - pub const fn uri(&self) -> &String { - &self.uri - } + /// + /// [`media playlist`]: crate::MediaPlaylist + pub const fn uri(&self) -> &String { &self.uri } - /// Sets the `URI`, that identifies the associated media playlist. + /// Sets the `URI`, that identifies the associated [`media playlist`]. /// /// # Example /// ``` @@ -62,16 +64,17 @@ impl ExtXIFrameStreamInf { /// stream.set_uri("../new/uri"); /// assert_eq!(stream.uri(), &"../new/uri".to_string()); /// ``` + /// + /// [`media playlist`]: crate::MediaPlaylist pub fn set_uri(&mut self, value: T) -> &mut Self { self.uri = value.to_string(); self } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXIFrameStreamInf { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXIFrameStreamInf { @@ -91,13 +94,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, @@ -109,15 +111,11 @@ impl FromStr for ExtXIFrameStreamInf { impl Deref for ExtXIFrameStreamInf { type Target = StreamInf; - fn deref(&self) -> &Self::Target { - &self.stream_inf - } + fn deref(&self) -> &Self::Target { &self.stream_inf } } impl DerefMut for ExtXIFrameStreamInf { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.stream_inf - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream_inf } } #[cfg(test)] @@ -140,6 +138,8 @@ mod test { .unwrap(), ExtXIFrameStreamInf::new("foo", 1000) ); + + assert!("garbage".parse::().is_err()); } #[test] @@ -149,4 +149,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..8358609 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -4,85 +4,126 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{InStreamId, MediaType, ProtocolVersion, RequiredVersion}; +use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion, RequiredVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; use crate::Error; -/// # [4.4.4.1. EXT-X-MEDIA] -/// The [ExtXMedia] tag is used to relate [Media Playlist]s that contain -/// alternative Renditions of the same content. For -/// example, three [ExtXMedia] tags can be used to identify audio-only -/// [Media Playlist]s, that contain English, French, and Spanish Renditions -/// of the same presentation. Or, two [ExtXMedia] tags can be used to -/// identify video-only [Media Playlist]s that show two different camera +/// # [4.4.5.1. EXT-X-MEDIA] +/// The [`ExtXMedia`] tag is used to relate [`Media Playlist`]s, +/// that contain alternative Renditions of the same content. +/// +/// For +/// example, three [`ExtXMedia`] tags can be used to identify audio-only +/// [`Media Playlist`]s, that contain English, French, and Spanish Renditions +/// of the same presentation. Or, two [`ExtXMedia`] tags can be used to +/// identify video-only [`Media Playlist`]s that show two different camera /// angles. /// -/// Its format is: -/// ```text -/// #EXT-X-MEDIA: -/// ``` -/// -/// [Media Playlist]: crate::MediaPlaylist -/// [4.4.4.1. EXT-X-MEDIA]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.4.1 +/// [`Media Playlist`]: crate::MediaPlaylist +/// [4.4.5.1. EXT-X-MEDIA]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.1 #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] #[builder(setter(into))] #[builder(build_fn(validate = "Self::validate"))] pub struct ExtXMedia { - /// Sets the media type of the rendition. + /// Sets the [`MediaType`] of the rendition. + /// + /// # Note + /// This attribute is **required**. media_type: MediaType, #[builder(setter(strip_option, into), default)] - /// Sets the URI that identifies the media playlist. + /// Sets the `URI` that identifies the [`Media Playlist`]. + /// + /// # Note + /// - This attribute is **required**, if the [`MediaType`] is + /// [`MediaType::Subtitles`]. + /// - This attribute is **not allowed**, if the [`MediaType`] is + /// [`MediaType::ClosedCaptions`]. + /// + /// [`Media Playlist`]: crate::MediaPlaylist uri: Option, - /// Sets the identifier that specifies the group to which the rendition belongs. + /// Sets the identifier, that specifies the group to which the rendition + /// belongs. + /// + /// # Note + /// This attribute is **required**. group_id: String, + #[builder(setter(strip_option, into), default)] /// Sets the name of the primary language used in the rendition. - #[builder(setter(strip_option, into), default)] + /// The value has to conform to [`RFC5646`]. + /// + /// # Note + /// This attribute is **optional**. + /// + /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 language: Option, - /// Sets the name of a language associated with the rendition. #[builder(setter(strip_option, into), default)] + /// Sets the name of a language associated with the rendition. + /// + /// # Note + /// This attribute is **optional**. + /// + /// [`language`]: #method.language assoc_language: Option, /// Sets a human-readable description of the rendition. + /// + /// # Note + /// This attribute is **required**. + /// + /// If the [`language`] attribute is present, this attribute should be in + /// that language. + /// + /// [`language`]: #method.language name: String, + #[builder(default)] /// Sets the value of the `default` flag. - #[builder(default)] + /// + /// # Note + /// This attribute is **optional**, its absence indicates an implicit value + /// of `false`. is_default: bool, + #[builder(default)] /// Sets the value of the `autoselect` flag. - #[builder(default)] + /// + /// # Note + /// This attribute is **optional**, its absence indicates an implicit value + /// of `false`. is_autoselect: bool, - /// Sets the value of the `forced` flag. #[builder(default)] + /// Sets the value of the `forced` flag. is_forced: bool, - /// Sets the identifier that specifies a rendition within the segments in the media playlist. #[builder(setter(strip_option, into), default)] + /// Sets the identifier that specifies a rendition within the segments in + /// the media playlist. instream_id: Option, + #[builder(setter(strip_option, into), default)] /// Sets the string that represents uniform type identifiers (UTI). - #[builder(setter(strip_option, into), default)] characteristics: Option, - /// Sets the string that represents the parameters of the rendition. #[builder(setter(strip_option, into), default)] - channels: Option, + /// Sets the parameters of the rendition. + channels: Option, } impl ExtXMediaBuilder { fn validate(&self) -> Result<(), String> { + // A MediaType is always required! 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 media_type == MediaType::Subtitles && self.uri.is_none() { + return Err(Error::missing_attribute("URI").to_string()); + } + + if media_type == MediaType::ClosedCaptions { if self.uri.is_some() { - return Err(Error::custom( - "Unexpected attribute: \"URL\" for MediaType::ClosedCaptions!", - ) - .to_string()); + return Err(Error::unexpected_attribute("URI").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()); + if self.instream_id.is_none() { + return Err(Error::missing_attribute("INSTREAM-ID").to_string()); } + } else if self.instream_id.is_some() { + return Err(Error::unexpected_attribute("INSTREAM-ID").to_string()); } if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) { @@ -91,10 +132,8 @@ impl ExtXMediaBuilder { ); } - if MediaType::Subtitles != media_type { - if self.is_forced.is_some() { - return Err(Error::invalid_input().to_string()); - } + if media_type != MediaType::Subtitles && self.is_forced.is_some() { + return Err(Error::invalid_input().to_string()); } Ok(()) @@ -104,9 +143,9 @@ impl ExtXMediaBuilder { impl ExtXMedia { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; - /// Makes a new [ExtXMedia] tag. + /// 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 +161,470 @@ impl ExtXMedia { } } - /// Makes a [ExtXMediaBuilder] for [ExtXMedia]. - pub fn builder() -> ExtXMediaBuilder { - ExtXMediaBuilder::default() + /// Returns a builder for [`ExtXMedia`]. + pub fn builder() -> ExtXMediaBuilder { ExtXMediaBuilder::default() } + + /// 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 type of the media associated with this tag. - pub const fn media_type(&self) -> MediaType { - self.media_type - } + /// 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 } - /// Returns the identifier that specifies the group to which the rendition belongs. - 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. - pub const fn name(&self) -> &String { - &self.name + /// + /// # 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 } + + /// Sets a human-readable description of the rendition. + /// + /// # Note + /// If the [`language`] attribute is present, this attribute should be in + /// that language. + /// + /// # 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()); + /// ``` + /// + /// [`language`]: #method.language + pub fn set_name>(&mut self, value: T) -> &mut Self { + self.name = value.into(); + self } - /// Returns the URI that identifies the media playlist. - pub fn uri(&self) -> Option<&String> { - self.uri.as_ref() + /// Returns the `URI`, that identifies the [`Media Playlist`]. + /// + /// # 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())); + /// ``` + /// + /// [`Media Playlist`]: crate::MediaPlaylist + pub const fn uri(&self) -> &Option { &self.uri } + + /// Sets the `URI`, that identifies the [`Media Playlist`]. + /// + /// # Note + /// This attribute is **required**, if the [`MediaType`] is + /// [`MediaType::Subtitles`]. This attribute is **not allowed**, if the + /// [`MediaType`] is [`MediaType::ClosedCaptions`]. + /// + /// # 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())); + /// ``` + /// + /// [`Media Playlist`]: crate::MediaPlaylist + 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. + /// The value has to conform to [`RFC5646`]. + /// + /// # 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())); + /// ``` + /// + /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 + 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. + /// An associated language is often used in a different role, than the + /// language specified by the [`language`] attribute (e.g., written versus + /// spoken, or a fallback dialect). + /// + /// # 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())); + /// ``` + /// + /// [`language`]: #method.language + 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. - pub const fn is_default(&self) -> bool { - self.is_default + /// 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. + /// A value of `true` indicates, that the client should play + /// this rendition of the content in the absence of information + /// from the user indicating a different choice. + /// + /// # 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 { - self.is_autoselect + /// + /// # 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. - pub const fn is_forced(&self) -> bool { - self.is_forced + /// 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 } + + /// 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 media playlist. - pub const fn instream_id(&self) -> Option { - self.instream_id + /// Returns the identifier that specifies a rendition within the segments in + /// the [`Media Playlist`]. + /// + /// # 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)); + /// ``` + /// + /// [`Media Playlist`]: crate::MediaPlaylist + pub const fn instream_id(&self) -> Option { self.instream_id } + + /// Sets the [`InStreamId`], that specifies a rendition within the + /// segments in the [`Media Playlist`]. + /// + /// # 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 } + + /// Sets the characteristics attribute, containing one or more Uniform Type + /// Identifiers separated by comma. + /// Each [`UTI`] indicates an individual characteristic of the Rendition. + /// + /// A [`subtitles`] Rendition may include the following characteristics: + /// "public.accessibility.transcribes-spoken-dialog", + /// "public.accessibility.describes-music-and-sound", and + /// "public.easy-to-read" (which indicates that the subtitles have + /// been edited for ease of reading). + /// + /// An AUDIO Rendition MAY include the following characteristic: + /// "public.accessibility.describes-video". + /// + /// The characteristics attribute may include private UTIs. + /// + /// # 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())); + /// ``` + /// + /// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI + /// [`subtitles`]: crate::types::MediaType::Subtitles + 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. - pub fn channels(&self) -> Option<&String> { - self.channels.as_ref() + /// Returns the channels. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::{Channels, MediaType}; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.channels(), &None); + /// + /// media.set_channels(Some(Channels::new(6))); + /// + /// assert_eq!(media.channels(), &Some(Channels::new(6))); + /// ``` + pub const fn channels(&self) -> &Option { &self.channels } + + /// Sets the channels. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMedia; + /// use hls_m3u8::types::{Channels, MediaType}; + /// + /// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name"); + /// # assert_eq!(media.channels(), &None); + /// + /// media.set_channels(Some(Channels::new(6))); + /// + /// assert_eq!(media.channels(), &Some(Channels::new(6))); + /// ``` + pub fn set_channels>(&mut self, value: Option) -> &mut Self { + self.channels = value.map(|v| v.into()); + self } } @@ -247,7 +684,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() { @@ -285,14 +722,16 @@ impl FromStr for ExtXMedia { builder.characteristics(unquote(value)); } "CHANNELS" => { - builder.channels(unquote(value)); + builder.channels(unquote(value).parse::()?); } _ => { // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. + // > ignore any attribute/value pair with an unrecognized + // AttributeName. } } } + builder.build().map_err(Error::builder_error) } } @@ -349,6 +788,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(Channels::new(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,19 +1023,345 @@ 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(Channels::new(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\"" .parse() .unwrap() - ) + ); + } + + #[test] + fn test_parser_error() { + assert!("".parse::().is_err()); + assert!("garbage".parse::().is_err()); + + assert!( + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\"" + .parse::() + .is_err() + ); + assert!("#EXT-X-MEDIA:TYPE=AUDIO,INSTREAM-ID=CC1" + .parse::() + .is_err()); + + assert!("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO" + .parse::() + .is_err()); + + assert!("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES" + .parse::() + .is_err()); } #[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..a6fd617 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -11,37 +11,43 @@ use crate::Error; /// The data of an [ExtXSessionData] tag. #[derive(Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] pub enum SessionData { - /// A String, that contains the data identified by [data_id](ExtXSessionData::data_id). - /// If a [language](ExtXSessionData::language) is specified, the value should - /// contain a human-readable string written in the specified language. + /// A String, that contains the data identified by + /// [`data_id`](ExtXSessionData::data_id). + /// If a [`language`](ExtXSessionData::language) is specified, the value + /// should contain a human-readable string written in the specified + /// language. Value(String), - /// An [uri], which points to a [json]. + /// An [`uri`], which points to a [`json`]. /// - /// [json]: https://tools.ietf.org/html/rfc8259 - /// [uri]: https://tools.ietf.org/html/rfc3986 + /// [`json`]: https://tools.ietf.org/html/rfc8259 + /// [`uri`]: https://tools.ietf.org/html/rfc3986 Uri(String), } /// # [4.3.4.4. EXT-X-SESSION-DATA] /// -/// The [ExtXSessionData] tag allows arbitrary session data to be -/// carried in a [Master Playlist]. +/// The [`ExtXSessionData`] tag allows arbitrary session data to be +/// carried in a [`Master Playlist`]. /// -/// [Master Playlist]: crate::MasterPlaylist +/// [`Master Playlist`]: crate::MasterPlaylist /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 #[derive(Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] #[builder(setter(into))] pub struct ExtXSessionData { - /// The identifier of the data. For more information look [here](ExtXSessionData::set_data_id). + /// The identifier of the data. + /// For more information look [`here`](ExtXSessionData::set_data_id). + /// /// # Note /// This field is required. data_id: String, - /// The data associated with the [data_id](ExtXSessionDataBuilder::data_id). - /// For more information look [here](SessionData). + /// The data associated with the + /// [`data_id`](ExtXSessionDataBuilder::data_id). + /// For more information look [`here`](SessionData). + /// /// # Note /// This field is required. data: SessionData, - /// The language of the [data](ExtXSessionDataBuilder::data). + /// The language of the [`data`](ExtXSessionDataBuilder::data). #[builder(setter(into, strip_option), default)] language: Option, } @@ -49,7 +55,7 @@ pub struct ExtXSessionData { impl ExtXSessionData { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:"; - /// Makes a new [ExtXSessionData] tag. + /// Makes a new [`ExtXSessionData`] tag. /// /// # Example /// ``` @@ -57,18 +63,18 @@ impl ExtXSessionData { /// /// ExtXSessionData::new( /// "com.example.movie.title", - /// SessionData::Uri("https://www.example.com/".to_string()) + /// SessionData::Uri("https://www.example.com/".to_string()), /// ); /// ``` pub fn new(data_id: T, data: SessionData) -> Self { - ExtXSessionData { + Self { data_id: data_id.to_string(), data, language: None, } } - /// Returns a new Builder for [ExtXSessionData]. + /// Returns a new Builder for [`ExtXSessionData`]. /// /// # Example /// ``` @@ -90,11 +96,9 @@ impl ExtXSessionData { /// ) /// ); /// ``` - pub fn builder() -> ExtXSessionDataBuilder { - ExtXSessionDataBuilder::default() - } + pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() } - /// Makes a new [ExtXSessionData] tag, with the given language. + /// Makes a new [`ExtXSessionData`] tag, with the given language. /// /// # Example /// ``` @@ -103,11 +107,11 @@ impl ExtXSessionData { /// let session_data = ExtXSessionData::with_language( /// "com.example.movie.title", /// SessionData::Value("some data".to_string()), - /// "english" + /// "english", /// ); /// ``` 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()), @@ -130,9 +134,7 @@ impl ExtXSessionData { /// &"com.example.movie.title".to_string() /// ) /// ``` - pub const fn data_id(&self) -> &String { - &self.data_id - } + pub const fn data_id(&self) -> &String { &self.data_id } /// Returns the `data`. /// @@ -150,11 +152,10 @@ impl ExtXSessionData { /// &SessionData::Value("some data".to_string()) /// ) /// ``` - pub const fn data(&self) -> &SessionData { - &self.data - } + pub const fn data(&self) -> &SessionData { &self.data } - /// Returns the `language` tag, that identifies the language of [SessionData]. + /// Returns the `language` tag, that identifies the language of + /// [`SessionData`]. /// /// # Example /// ``` @@ -171,12 +172,10 @@ impl ExtXSessionData { /// &Some("english".to_string()) /// ) /// ``` - pub const fn language(&self) -> &Option { - &self.language - } + pub const fn language(&self) -> &Option { &self.language } - /// Sets the `language` attribute, that identifies the language of [SessionData]. - /// See [rfc5646](https://tools.ietf.org/html/rfc5646). + /// Sets the `language` attribute, that identifies the language of + /// [`SessionData`]. See [rfc5646](https://tools.ietf.org/html/rfc5646). /// /// # Example /// ``` @@ -197,8 +196,8 @@ impl ExtXSessionData { self } - /// Sets the `data_id` attribute, that should conform to a [reverse DNS] naming convention, - /// such as `com.example.movie.title`. + /// Sets the `data_id` attribute, that should conform to a [reverse DNS] + /// naming convention, such as `com.example.movie.title`. /// /// # Note: /// There is no central registration authority, so a value @@ -224,7 +223,7 @@ impl ExtXSessionData { self } - /// Sets the [data](ExtXSessionData::data) of this tag. + /// Sets the [`data`](ExtXSessionData::data) of this tag. /// /// # Example /// ``` @@ -247,22 +246,23 @@ impl ExtXSessionData { } impl RequiredVersion for ExtXSessionData { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } 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(()) } } @@ -286,12 +286,13 @@ impl FromStr for ExtXSessionData { "LANGUAGE" => language = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. + // > ignore any attribute/value pair with an unrecognized + // AttributeName. } } } - 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 +307,7 @@ impl FromStr for ExtXSessionData { } }; - Ok(ExtXSessionData { + Ok(Self { data_id, data, language, @@ -321,7 +322,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 +334,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 +348,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 +362,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 +391,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 +403,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 +417,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 +431,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..c9776fc 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -7,9 +7,9 @@ use crate::utils::tag; use crate::Error; /// # [4.3.4.5. EXT-X-SESSION-KEY] -/// The [ExtXSessionKey] tag allows encryption keys from [Media Playlist]s -/// to be specified in a [Master Playlist]. This allows the client to -/// preload these keys without having to read the [Media Playlist]s +/// The [`ExtXSessionKey`] tag allows encryption keys from [`Media Playlist`]s +/// to be specified in a [`Master Playlist`]. This allows the client to +/// preload these keys without having to read the [`Media Playlist`]s /// first. /// /// Its format is: @@ -17,8 +17,8 @@ use crate::Error; /// #EXT-X-SESSION-KEY: /// ``` /// -/// [Media Playlist]: crate::MediaPlaylist -/// [Master Playlist]: crate::MasterPlaylist +/// [`Media Playlist`]: crate::MediaPlaylist +/// [`Master Playlist`]: crate::MasterPlaylist /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXSessionKey(DecryptionKey); @@ -26,21 +26,20 @@ pub struct ExtXSessionKey(DecryptionKey); impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; - /// Makes a new [ExtXSessionKey] tag. + /// Makes a new [`ExtXSessionKey`] tag. /// /// # Panic - /// An [ExtXSessionKey] should only be used, if the segments of the stream are encrypted. - /// Therefore this function will panic, if the `method` is [EncryptionMethod::None]. + /// An [`ExtXSessionKey`] should only be used, + /// if the segments of the stream are encrypted. + /// Therefore this function will panic, + /// if the `method` is [`EncryptionMethod::None`]. /// /// # Example /// ``` /// # use hls_m3u8::tags::ExtXSessionKey; /// use hls_m3u8::types::EncryptionMethod; /// - /// let session_key = ExtXSessionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let session_key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// ``` pub fn new(method: EncryptionMethod, uri: T) -> Self { if method == EncryptionMethod::None { @@ -52,9 +51,7 @@ impl ExtXSessionKey { } impl RequiredVersion for ExtXSessionKey { - fn required_version(&self) -> ProtocolVersion { - self.0.required_version() - } + fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } impl fmt::Display for ExtXSessionKey { @@ -78,15 +75,11 @@ impl FromStr for ExtXSessionKey { impl Deref for ExtXSessionKey { type Target = DecryptionKey; - fn deref(&self) -> &Self::Target { - &self.0 - } + fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for ExtXSessionKey { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } #[cfg(test)] @@ -100,9 +93,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 +122,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 +157,34 @@ 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..4772ae6 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, @@ -25,16 +25,15 @@ pub struct ExtXStreamInf { impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; - /// Creates a new [ExtXStreamInf] tag. + /// 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,48 +43,155 @@ 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. - pub fn frame_rate(&self) -> Option { - self.frame_rate.map_or(None, |v| Some(v.as_f64())) - } + /// + /// # 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(|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 } + + /// 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 `CLOSED-CAPTIONS` attribute. - pub fn closed_captions(&self) -> Option<&ClosedCaptions> { - self.closed_captions.as_ref() + /// 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 } } impl RequiredVersion for ExtXStreamInf { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXStreamInf { @@ -113,8 +219,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 +236,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()?), _ => {} } } @@ -147,15 +255,11 @@ impl FromStr for ExtXStreamInf { impl Deref for ExtXStreamInf { type Target = StreamInf; - fn deref(&self) -> &Self::Target { - &self.stream_inf - } + fn deref(&self) -> &Self::Target { &self.stream_inf } } impl DerefMut for ExtXStreamInf { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.stream_inf - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream_inf } } #[cfg(test)] @@ -174,6 +278,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 +295,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..1ca9d8e 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -6,9 +6,9 @@ use crate::utils::tag; /// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// -/// The [ExtXDiscontinuitySequence] tag allows synchronization between +/// The [`ExtXDiscontinuitySequence`] tag allows synchronization between /// different Renditions of the same Variant Stream or different Variant -/// Streams that have [ExtXDiscontinuity] tags in their [Media Playlist]s. +/// Streams that have [`ExtXDiscontinuity`] tags in their [`Media Playlist`]s. /// /// Its format is: /// ```text @@ -16,8 +16,8 @@ use crate::utils::tag; /// ``` /// where `number` is a [u64]. /// -/// [ExtXDiscontinuity]: crate::tags::ExtXDiscontinuity -/// [Media Playlist]: crate::MediaPlaylist +/// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity +/// [`Media Playlist`]: crate::MediaPlaylist /// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3 #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] @@ -33,9 +33,7 @@ impl ExtXDiscontinuitySequence { /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); /// ``` - pub const fn new(seq_num: u64) -> Self { - Self(seq_num) - } + pub const fn new(seq_num: u64) -> Self { Self(seq_num) } /// Returns the discontinuity sequence number of /// the first media segment that appears in the associated playlist. @@ -47,9 +45,7 @@ impl ExtXDiscontinuitySequence { /// /// assert_eq!(discontinuity_sequence.seq_num(), 5); /// ``` - pub const fn seq_num(&self) -> u64 { - self.0 - } + pub const fn seq_num(self) -> u64 { self.0 } /// Sets the sequence number. /// @@ -68,15 +64,11 @@ impl ExtXDiscontinuitySequence { } impl RequiredVersion for ExtXDiscontinuitySequence { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXDiscontinuitySequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.0) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } } impl FromStr for ExtXDiscontinuitySequence { @@ -115,4 +107,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/end_list.rs b/src/tags/media_playlist/end_list.rs index 2865b4c..119de43 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -6,16 +6,16 @@ use crate::utils::tag; use crate::Error; /// # [4.4.3.4. EXT-X-ENDLIST] -/// The [ExtXEndList] tag indicates, that no more [Media Segment]s will be -/// added to the [Media Playlist] file. +/// The [`ExtXEndList`] tag indicates, that no more [`Media Segment`]s will be +/// added to the [`Media Playlist`] file. /// /// Its format is: /// ```text /// #EXT-X-ENDLIST /// ``` /// -/// [Media Segment]: crate::MediaSegment -/// [Media Playlist]: crate::MediaPlaylist +/// [`Media Segment`]: crate::MediaSegment +/// [`Media Playlist`]: crate::MediaPlaylist /// [4.4.3.4. EXT-X-ENDLIST]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.4 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -26,15 +26,11 @@ impl ExtXEndList { } impl RequiredVersion for ExtXEndList { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXEndList { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } impl FromStr for ExtXEndList { diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index c0291ca..7c52ba4 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -6,7 +6,7 @@ use crate::utils::tag; use crate::Error; /// # [4.4.3.6. EXT-X-I-FRAMES-ONLY] -/// The [ExtXIFramesOnly] tag indicates that each [Media Segment] in the +/// The [`ExtXIFramesOnly`] tag indicates that each [`Media Segment`] in the /// Playlist describes a single I-frame. I-frames are encoded video /// frames, whose decoding does not depend on any other frame. I-frame /// Playlists can be used for trick play, such as fast forward, rapid @@ -17,7 +17,7 @@ use crate::Error; /// #EXT-X-I-FRAMES-ONLY /// ``` /// -/// [Media Segment]: crate::MediaSegment +/// [`Media Segment`]: crate::MediaSegment /// [4.4.3.6. EXT-X-I-FRAMES-ONLY]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.6 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -28,15 +28,11 @@ impl ExtXIFramesOnly { } impl RequiredVersion for ExtXIFramesOnly { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V4 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } impl fmt::Display for ExtXIFramesOnly { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } impl FromStr for ExtXIFramesOnly { @@ -61,9 +57,7 @@ mod test { } #[test] - fn test_parser() { - assert_eq!(ExtXIFramesOnly, "#EXT-X-I-FRAMES-ONLY".parse().unwrap(),) - } + fn test_parser() { assert_eq!(ExtXIFramesOnly, "#EXT-X-I-FRAMES-ONLY".parse().unwrap(),) } #[test] fn test_required_version() { diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index c841b19..e80a570 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -6,14 +6,14 @@ use crate::utils::tag; use crate::Error; /// # [4.4.3.2. EXT-X-MEDIA-SEQUENCE] -/// The [ExtXMediaSequence] tag indicates the Media Sequence Number of -/// the first [Media Segment] that appears in a Playlist file. +/// The [`ExtXMediaSequence`] tag indicates the Media Sequence Number of +/// the first [`Media Segment`] that appears in a Playlist file. /// /// Its format is: /// ```text /// #EXT-X-MEDIA-SEQUENCE: /// ``` -/// where `number` is a [u64]. +/// where `number` is a [`u64`]. /// /// [Media Segment]: crate::MediaSegment /// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]: @@ -24,16 +24,14 @@ pub struct ExtXMediaSequence(u64); impl ExtXMediaSequence { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:"; - /// Makes a new [ExtXMediaSequence] tag. + /// Makes a new [`ExtXMediaSequence`] tag. /// /// # Example /// ``` /// # use hls_m3u8::tags::ExtXMediaSequence; /// let media_sequence = ExtXMediaSequence::new(5); /// ``` - pub const fn new(seq_num: u64) -> Self { - Self(seq_num) - } + pub const fn new(seq_num: u64) -> Self { Self(seq_num) } /// Returns the sequence number of the first media segment, /// that appears in the associated playlist. @@ -45,9 +43,7 @@ impl ExtXMediaSequence { /// /// assert_eq!(media_sequence.seq_num(), 5); /// ``` - pub const fn seq_num(&self) -> u64 { - self.0 - } + pub const fn seq_num(self) -> u64 { self.0 } /// Sets the sequence number. /// @@ -66,15 +62,11 @@ impl ExtXMediaSequence { } impl RequiredVersion for ExtXMediaSequence { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXMediaSequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.0) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } } impl FromStr for ExtXMediaSequence { @@ -113,4 +105,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_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index b6ded13..80bcee8 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -7,8 +7,8 @@ use crate::Error; /// # [4.4.3.5. EXT-X-PLAYLIST-TYPE] /// -/// The [ExtXPlaylistType] tag provides mutability information about the -/// [Media Playlist]. It applies to the entire [Media Playlist]. +/// The [`ExtXPlaylistType`] tag provides mutability information about the +/// [`Media Playlist`]. It applies to the entire [`Media Playlist`]. /// /// Its format is: /// ```text @@ -20,10 +20,10 @@ use crate::Error; /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.5 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ExtXPlaylistType { - /// If the [ExtXPlaylistType] is Event, Media Segments can only be added to - /// the end of the Media Playlist. + /// If the [`ExtXPlaylistType`] is Event, Media Segments + /// can only be added to the end of the Media Playlist. Event, - /// If the [ExtXPlaylistType] is Video On Demand (Vod), + /// If the [`ExtXPlaylistType`] is Video On Demand (Vod), /// the Media Playlist cannot change. Vod, } @@ -33,9 +33,7 @@ impl ExtXPlaylistType { } impl RequiredVersion for ExtXPlaylistType { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXPlaylistType { diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index d7fe890..c70ab24 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -7,43 +7,52 @@ use crate::utils::tag; use crate::Error; /// # [4.4.3.1. EXT-X-TARGETDURATION] -/// The [ExtXTargetDuration] tag specifies the maximum [Media Segment] +/// The [`ExtXTargetDuration`] tag specifies the maximum [`Media Segment`] /// duration. /// -/// Its format is: -/// ```text -/// #EXT-X-TARGETDURATION: -/// ``` -/// where `s` is the target [Duration] in seconds. -/// -/// [Media Segment]: crate::MediaSegment +/// [`Media Segment`]: crate::MediaSegment /// [4.4.3.1. EXT-X-TARGETDURATION]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.1 +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.3.1 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct ExtXTargetDuration(Duration); impl ExtXTargetDuration { pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:"; - /// Makes a new [ExtXTargetduration] tag. + /// Makes a new [`ExtXTargetDuration`] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXTargetDuration; + /// use std::time::Duration; + /// + /// let target_duration = ExtXTargetDuration::new(Duration::from_secs(20)); + /// ``` /// /// # Note - /// The nanoseconds part of the [Duration] will be discarded. + /// The nanoseconds part of the [`Duration`] will be discarded. pub const fn new(duration: Duration) -> Self { // TOOD: round instead of discarding? Self(Duration::from_secs(duration.as_secs())) } - /// Returns the maximum media segment duration in the associated playlist. - pub const fn duration(&self) -> Duration { - self.0 - } + /// Returns the maximum media segment duration. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXTargetDuration; + /// use std::time::Duration; + /// + /// let target_duration = ExtXTargetDuration::new(Duration::from_nanos(2_000_000_000)); + /// + /// assert_eq!(target_duration.duration(), Duration::from_secs(2)); + /// ``` + pub const fn duration(&self) -> Duration { self.0 } } +/// This tag requires [`ProtocolVersion::V1`]. impl RequiredVersion for ExtXTargetDuration { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXTargetDuration { diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 37c3330..a79769f 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -8,7 +8,7 @@ use crate::Error; /// # [4.4.2.2. EXT-X-BYTERANGE] /// -/// The [ExtXByteRange] tag indicates that a [Media Segment] is a sub-range +/// The [`ExtXByteRange`] tag indicates that a [`Media Segment`] is a sub-range /// of the resource identified by its `URI`. /// /// Its format is: @@ -20,7 +20,7 @@ use crate::Error; /// If present, `o` is a [usize] indicating the start of the sub-range, /// as a byte offset from the beginning of the resource. /// -/// [Media Segment]: crate::MediaSegment +/// [`Media Segment`]: crate::MediaSegment /// [4.4.2.2. EXT-X-BYTERANGE]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.2 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -29,7 +29,7 @@ pub struct ExtXByteRange(ByteRange); impl ExtXByteRange { pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; - /// Makes a new [ExtXByteRange] tag. + /// Makes a new [`ExtXByteRange`] tag. /// /// # Example /// ``` @@ -40,7 +40,7 @@ impl ExtXByteRange { Self(ByteRange::new(length, start)) } - /// Converts the [ExtXByteRange] to a [ByteRange]. + /// Converts the [`ExtXByteRange`] to a [`ByteRange`]. /// /// # Example /// ``` @@ -50,29 +50,21 @@ impl ExtXByteRange { /// let byte_range = ExtXByteRange::new(20, Some(5)); /// let range: ByteRange = byte_range.to_range(); /// ``` - pub const fn to_range(&self) -> ByteRange { - self.0 - } + pub const fn to_range(&self) -> ByteRange { self.0 } } impl RequiredVersion for ExtXByteRange { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V4 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } impl Deref for ExtXByteRange { type Target = ByteRange; - fn deref(&self) -> &Self::Target { - &self.0 - } + fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for ExtXByteRange { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl fmt::Display for ExtXByteRange { @@ -98,11 +90,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..67c1089 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -6,29 +6,30 @@ 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)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXDateRange { - /// A string that uniquely identifies a [ExtXDateRange] in the Playlist. + /// A string that uniquely identifies a [`ExtXDateRange`] in the Playlist. /// This attribute is required. id: String, - /// A client-defined string that specifies some set of attributes and their associated value - /// semantics. All Date Ranges with the same CLASS attribute value MUST adhere to these - /// semantics. This attribute is OPTIONAL. + /// A client-defined string that specifies some set of attributes and their + /// associated value semantics. All Date Ranges with the same CLASS + /// attribute value MUST adhere to these semantics. This attribute is + /// OPTIONAL. class: Option, /// The date at which the Date Range begins. This attribute is REQUIRED. start_date: DateTime, - /// The date at which the Date Range ends. It MUST be equal to or later than the value of the - /// START-DATE attribute. This attribute is OPTIONAL. + /// The date at which the Date Range ends. It MUST be equal to or later than + /// the value of the START-DATE attribute. This attribute is OPTIONAL. end_date: Option>, /// The duration of the Date Range. It MUST NOT be negative. A single /// instant in time (e.g., crossing a finish line) SHOULD be @@ -45,11 +46,11 @@ pub struct ExtXDateRange { scte35_out: Option, /// scte35_in: Option, - /// This attribute indicates that the end of the range containing it is equal to the - /// START-DATE of its Following Range. The Following Range is the - /// Date Range of the same CLASS, that has the earliest START-DATE - /// after the START-DATE of the range in question. This attribute is - /// OPTIONAL. + /// This attribute indicates that the end of the range containing it is + /// equal to the START-DATE of its Following Range. The Following Range + /// is the Date Range of the same CLASS, that has the earliest + /// START-DATE after the START-DATE of the range in question. This + /// attribute is OPTIONAL. end_on_next: bool, /// The "X-" prefix defines a namespace reserved for client-defined /// attributes. The client-attribute MUST be a legal AttributeName. @@ -63,12 +64,62 @@ 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::offset::TimeZone; + /// use chrono::{DateTime, FixedOffset}; + /// + /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds + /// + /// let date_range = 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(), + } + } } +/// This tag requires [`ProtocolVersion::V1`]. +/// +/// # Example +/// ``` +/// # use hls_m3u8::tags::ExtXDateRange; +/// use chrono::offset::TimeZone; +/// use chrono::{DateTime, FixedOffset}; +/// use hls_m3u8::types::{ProtocolVersion, RequiredVersion}; +/// +/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds +/// +/// let date_range = ExtXDateRange::new( +/// "id", +/// FixedOffset::east(8 * HOURS_IN_SECS) +/// .ymd(2010, 2, 19) +/// .and_hms_milli(14, 54, 23, 31), +/// ); +/// assert_eq!(date_range.required_version(), ProtocolVersion::V1); +/// ``` impl RequiredVersion for ExtXDateRange { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXDateRange { @@ -82,15 +133,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 +184,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)), @@ -158,23 +203,22 @@ impl FromStr for ExtXDateRange { client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); } else { // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. + // > ignore any attribute/value pair with an + // unrecognized AttributeName. } } } } - 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 +237,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/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index a4846d9..ee0d607 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -6,15 +6,15 @@ use crate::utils::tag; use crate::Error; /// # [4.4.2.3. EXT-X-DISCONTINUITY] -/// The [ExtXDiscontinuity] tag indicates a discontinuity between the -/// [Media Segment] that follows it and the one that preceded it. +/// The [`ExtXDiscontinuity`] tag indicates a discontinuity between the +/// [`Media Segment`] that follows it and the one that preceded it. /// /// Its format is: /// ```text /// #EXT-X-DISCONTINUITY /// ``` /// -/// [Media Segment]: crate::MediaSegment +/// [`Media Segment`]: crate::MediaSegment /// [4.4.2.3. EXT-X-DISCONTINUITY]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -25,15 +25,11 @@ impl ExtXDiscontinuity { } impl RequiredVersion for ExtXDiscontinuity { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXDiscontinuity { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } impl FromStr for ExtXDiscontinuity { @@ -58,9 +54,7 @@ mod test { } #[test] - fn test_parser() { - assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap()) - } + fn test_parser() { assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap()) } #[test] fn test_required_version() { diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 8524d81..e13338f 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -2,14 +2,14 @@ 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; /// # [4.4.2.1. EXTINF] /// -/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies -/// only to the next [Media Segment]. +/// The [`ExtInf`] tag specifies the duration of a [`Media Segment`]. It applies +/// only to the next [`Media Segment`]. /// /// Its format is: /// ```text @@ -17,7 +17,7 @@ use crate::Error; /// ``` /// The title is an optional informative title about the [Media Segment]. /// -/// [Media Segment]: crate::media_segment::MediaSegment +/// [`Media Segment`]: crate::media_segment::MediaSegment /// [4.4.2.1. EXTINF]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.1 #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -29,7 +29,7 @@ pub struct ExtInf { impl ExtInf { pub(crate) const PREFIX: &'static str = "#EXTINF:"; - /// Makes a new [ExtInf] tag. + /// Makes a new [`ExtInf`] tag. /// /// # Example /// ``` @@ -39,13 +39,13 @@ impl ExtInf { /// let ext_inf = ExtInf::new(Duration::from_secs(5)); /// ``` pub const fn new(duration: Duration) -> Self { - ExtInf { + Self { duration, title: None, } } - /// Makes a new [ExtInf] tag with the given title. + /// Makes a new [`ExtInf`] tag with the given title. /// /// # Example /// ``` @@ -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()), } @@ -70,14 +70,9 @@ impl ExtInf { /// /// let ext_inf = ExtInf::new(Duration::from_secs(5)); /// - /// assert_eq!( - /// ext_inf.duration(), - /// Duration::from_secs(5) - /// ); + /// assert_eq!(ext_inf.duration(), Duration::from_secs(5)); /// ``` - pub const fn duration(&self) -> Duration { - self.duration - } + pub const fn duration(&self) -> Duration { self.duration } /// Sets the duration of the associated media segment. /// @@ -90,10 +85,7 @@ impl ExtInf { /// /// ext_inf.set_duration(Duration::from_secs(10)); /// - /// assert_eq!( - /// ext_inf.duration(), - /// Duration::from_secs(10) - /// ); + /// assert_eq!(ext_inf.duration(), Duration::from_secs(10)); /// ``` pub fn set_duration(&mut self, value: Duration) -> &mut Self { self.duration = value; @@ -109,14 +101,9 @@ impl ExtInf { /// /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// - /// assert_eq!( - /// ext_inf.title(), - /// &Some("title".to_string()) - /// ); + /// assert_eq!(ext_inf.title(), &Some("title".to_string())); /// ``` - pub const fn title(&self) -> &Option { - &self.title - } + pub const fn title(&self) -> &Option { &self.title } /// Sets the title of the associated media segment. /// @@ -129,10 +116,7 @@ impl ExtInf { /// /// ext_inf.set_title(Some("better title")); /// - /// assert_eq!( - /// ext_inf.title(), - /// &Some("better title".to_string()) - /// ); + /// assert_eq!(ext_inf.title(), &Some("better title".to_string())); /// ``` pub fn set_title(&mut self, value: Option) -> &mut Self { self.title = value.map(|v| v.to_string()); @@ -170,17 +154,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,14 +177,12 @@ impl FromStr for ExtInf { } }; - Ok(ExtInf { duration, title }) + Ok(Self { duration, title }) } } impl From for ExtInf { - fn from(value: Duration) -> Self { - Self::new(value) - } + fn from(value: Duration) -> Self { Self::new(value) } } #[cfg(test)] diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index e81479d..df15ef2 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -2,16 +2,16 @@ 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; /// # [4.4.2.4. EXT-X-KEY] -/// [Media Segment]s may be encrypted. The [ExtXKey] tag specifies how to -/// decrypt them. It applies to every [Media Segment] and to every Media -/// Initialization Section declared by an [ExtXMap] tag, that appears -/// between it and the next [ExtXKey] tag in the Playlist file with the -/// same [KeyFormat] attribute (or the end of the Playlist file). +/// [`Media Segment`]s may be encrypted. The [`ExtXKey`] tag specifies how to +/// decrypt them. It applies to every [`Media Segment`] and to every Media +/// Initialization Section declared by an [`ExtXMap`] tag, that appears +/// between it and the next [`ExtXKey`] tag in the Playlist file with the +/// same [`KeyFormat`] attribute (or the end of the Playlist file). /// /// The format is: /// ```text @@ -19,11 +19,12 @@ use crate::Error; /// ``` /// /// # Note -/// In case of an empty key (`EncryptionMethod::None`), all attributes will be ignored. +/// In case of an empty key (`EncryptionMethod::None`), +/// all attributes will be ignored. /// -/// [KeyFormat]: crate::types::KeyFormat -/// [ExtXMap]: crate::tags::ExtXMap -/// [Media Segment]: crate::MediaSegment +/// [`KeyFormat`]: crate::types::KeyFormat +/// [`ExtXMap`]: crate::tags::ExtXMap +/// [`Media Segment`]: crate::MediaSegment /// [4.4.2.4. EXT-X-KEY]: /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.4 #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -32,16 +33,14 @@ pub struct ExtXKey(DecryptionKey); impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; - /// Makes a new `ExtXKey` tag. - /// # Examples + /// Makes a new [`ExtXKey`] tag. + /// + /// # Example /// ``` /// use hls_m3u8::tags::ExtXKey; /// use hls_m3u8::types::EncryptionMethod; /// - /// let key = ExtXKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// /// assert_eq!( /// key.to_string(), @@ -52,44 +51,39 @@ impl ExtXKey { Self(DecryptionKey::new(method, uri)) } - /// Makes a new `ExtXKey` tag without a decryption key. - /// # Examples + /// Makes a new [`ExtXKey`] tag without a decryption key. + /// + /// # Example /// ``` /// use hls_m3u8::tags::ExtXKey; /// /// let key = ExtXKey::empty(); /// - /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-KEY:METHOD=NONE" - /// ); + /// assert_eq!(key.to_string(), "#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, }) } - /// Returns whether the [EncryptionMethod] is [None](EncryptionMethod::None). - /// # Examples + /// Returns whether the [`EncryptionMethod`] is + /// [`None`](EncryptionMethod::None). + /// + /// # Example /// ``` /// use hls_m3u8::tags::ExtXKey; /// use hls_m3u8::types::EncryptionMethod; /// /// let key = ExtXKey::empty(); /// - /// assert_eq!( - /// key.method() == EncryptionMethod::None, - /// key.is_empty() - /// ); + /// assert_eq!(key.method() == EncryptionMethod::None, key.is_empty()); /// ``` - pub fn is_empty(&self) -> bool { - self.0.method() == EncryptionMethod::None - } + pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None } } impl FromStr for ExtXKey { @@ -102,23 +96,17 @@ impl FromStr for ExtXKey { } impl fmt::Display for ExtXKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.0) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } } impl Deref for ExtXKey { type Target = DecryptionKey; - fn deref(&self) -> &Self::Target { - &self.0 - } + fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for ExtXKey { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } #[cfg(test)] @@ -134,13 +122,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 +151,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..05de349 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -7,7 +7,7 @@ use crate::utils::{quote, tag, unquote}; use crate::Error; /// # [4.4.2.5. EXT-X-MAP] -/// The [ExtXMap] tag specifies how to obtain the Media Initialization +/// The [`ExtXMap`] tag specifies how to obtain the Media Initialization /// Section, required to parse the applicable [Media Segment]s. /// /// Its format is: @@ -27,7 +27,7 @@ pub struct ExtXMap { impl ExtXMap { pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:"; - /// Makes a new `ExtXMap` tag. + /// Makes a new [`ExtXMap`] tag. pub fn new(uri: T) -> Self { ExtXMap { uri: uri.to_string(), @@ -35,7 +35,7 @@ impl ExtXMap { } } - /// Makes a new `ExtXMap` tag with the given range. + /// Makes a new [`ExtXMap`] tag with the given range. pub fn with_range(uri: T, range: ByteRange) -> Self { ExtXMap { uri: uri.to_string(), @@ -43,21 +43,16 @@ impl ExtXMap { } } - /// Returns the URI that identifies a resource that contains the media initialization section. - pub const fn uri(&self) -> &String { - &self.uri - } + /// Returns the `URI` that identifies a resource, + /// that contains the media initialization section. + pub const fn uri(&self) -> &String { &self.uri } /// Returns the range of the media initialization section. - pub const fn range(&self) -> Option { - self.range - } + pub const fn range(&self) -> Option { self.range } } impl RequiredVersion for ExtXMap { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V6 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V6 } } impl fmt::Display for ExtXMap { @@ -88,12 +83,13 @@ impl FromStr for ExtXMap { } _ => { // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. + // > ignore any attribute/value pair with an unrecognized + // AttributeName. } } } - 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/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index d6a68ce..4ce0aa2 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -9,10 +9,10 @@ use crate::utils::tag; use crate::Error; /// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] -/// The [ExtXProgramDateTime] tag associates the first sample of a -/// [Media Segment] with an absolute date and/or time. +/// The [`ExtXProgramDateTime`] tag associates the first sample of a +/// [`Media Segment`] with an absolute date and/or time. /// -/// [Media Segment]: crate::MediaSegment +/// [`Media Segment`]: crate::MediaSegment /// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtXProgramDateTime(DateTime); @@ -20,29 +20,26 @@ pub struct ExtXProgramDateTime(DateTime); impl ExtXProgramDateTime { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; - /// Makes a new `ExtXProgramDateTime` tag. + /// Makes a new [`ExtXProgramDateTime`] tag. /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXProgramDateTime; /// use chrono::{FixedOffset, TimeZone}; + /// use hls_m3u8::tags::ExtXProgramDateTime; /// /// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds /// /// let program_date_time = ExtXProgramDateTime::new( /// FixedOffset::east(8 * HOURS_IN_SECS) /// .ymd(2010, 2, 19) - /// .and_hms_milli(14, 54, 23, 31) + /// .and_hms_milli(14, 54, 23, 31), /// ); /// ``` - pub const fn new(date_time: DateTime) -> Self { - Self(date_time) - } + pub const fn new(date_time: DateTime) -> Self { Self(date_time) } - /// Returns the date-time of the first sample of the associated media segment. - pub const fn date_time(&self) -> DateTime { - self.0 - } + /// Returns the date-time of the first sample of the associated media + /// segment. + pub const fn date_time(&self) -> DateTime { self.0 } /// Sets the date-time of the first sample of the associated media segment. pub fn set_date_time(&mut self, value: DateTime) -> &mut Self { @@ -52,9 +49,7 @@ impl ExtXProgramDateTime { } impl RequiredVersion for ExtXProgramDateTime { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXProgramDateTime { @@ -78,15 +73,11 @@ impl FromStr for ExtXProgramDateTime { impl Deref for ExtXProgramDateTime { type Target = DateTime; - fn deref(&self) -> &Self::Target { - &self.0 - } + fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for ExtXProgramDateTime { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } #[cfg(test)] diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 7967a8d..9655e0f 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 { @@ -16,15 +16,11 @@ impl ExtXIndependentSegments { } impl RequiredVersion for ExtXIndependentSegments { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXIndependentSegments { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Self::PREFIX.fmt(f) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } impl FromStr for ExtXIndependentSegments { diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index e30fa9c..045bfa4 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,52 +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. - pub const fn time_offset(&self) -> f64 { - self.time_offset.as_f64() + /// + /// # 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. - pub const fn precise(&self) -> bool { - self.precise + /// 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 { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V1 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXStart { @@ -92,14 +139,15 @@ impl FromStr for ExtXStart { "PRECISE" => precise = (parse_yes_or_no(value))?, _ => { // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. + // > ignore any attribute/value pair with an unrecognized + // AttributeName. } } } - 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 +195,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..a1807d1 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -15,23 +15,27 @@ pub struct ByteRange { } impl ByteRange { - /// Creates a new [ByteRange]. - pub const fn new(length: usize, start: Option) -> Self { - Self { length, start } - } + /// 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; /// # /// assert_eq!(ByteRange::new(20, Some(3)).length(), 20); /// ``` - pub const fn length(&self) -> usize { - self.length - } + pub const fn length(&self) -> usize { self.length } /// Sets the length of the range. + /// /// # Example /// ``` /// # use hls_m3u8::types::ByteRange; @@ -48,17 +52,17 @@ impl ByteRange { } /// Returns the start of the range. + /// /// # Example /// ``` /// # use hls_m3u8::types::ByteRange; /// # /// assert_eq!(ByteRange::new(20, Some(3)).start(), Some(3)); /// ``` - pub const fn start(&self) -> Option { - self.start - } + pub const fn start(&self) -> Option { self.start } /// Sets the start of the range. + /// /// # Example /// ``` /// # use hls_m3u8::types::ByteRange; @@ -97,13 +101,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 +138,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/channels.rs b/src/types/channels.rs new file mode 100644 index 0000000..b620edb --- /dev/null +++ b/src/types/channels.rs @@ -0,0 +1,193 @@ +use core::fmt; +use core::str::FromStr; + +use crate::Error; + +/// Specifies a list of parameters. +/// +/// # `MediaType::Audio` +/// The first parameter is a count of audio channels expressed as a [`u64`], +/// indicating the maximum number of independent, simultaneous audio channels +/// present in any [`MediaSegment`] in the rendition. For example, an +/// `AC-3 5.1` rendition would have a `CHANNELS="6"` attribute. +/// +/// The second parameter identifies the encoding of object-based audio used by +/// the rendition. This parameter is a comma-separated list of Audio +/// Object Coding Identifiers. It is optional. An Audio Object +/// Coding Identifier is a string containing characters from the set +/// `[A..Z]`, `[0..9]`, and `'-'`. They are codec-specific. A parameter +/// value of consisting solely of the dash character (`'-'`) indicates +/// that the audio is not object-based. +/// +/// # Example +/// Creating a `CHANNELS="6"` attribute +/// ``` +/// # use hls_m3u8::types::Channels; +/// let mut channels = Channels::new(6); +/// +/// assert_eq!( +/// format!("CHANNELS=\"{}\"", channels), +/// "CHANNELS=\"6\"".to_string() +/// ); +/// ``` +/// +/// # Note +/// Currently there are no example playlists in the documentation, +/// or in popular m3u8 libraries, showing a usage for the second parameter +/// of [`Channels`], so if you have one please open an issue on github! +/// +/// [`MediaSegment`]: crate::MediaSegment +#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Channels { + first_parameter: u64, + second_parameter: Option>, +} + +impl Channels { + /// Makes a new [`Channels`] struct. + /// + /// # Example + /// ``` + /// # use hls_m3u8::types::Channels; + /// let mut channels = Channels::new(6); + /// ``` + pub const fn new(value: u64) -> Self { + Self { + first_parameter: value, + second_parameter: None, + } + } + + /// Returns the first parameter. + /// + /// # Example + /// ``` + /// # use hls_m3u8::types::Channels; + /// let mut channels = Channels::new(6); + /// + /// assert_eq!(channels.first_parameter(), 6); + /// ``` + pub const fn first_parameter(&self) -> u64 { self.first_parameter } + + /// Sets the first parameter. + /// + /// # Example + /// ``` + /// # use hls_m3u8::types::Channels; + /// let mut channels = Channels::new(3); + /// + /// channels.set_first_parameter(6); + /// assert_eq!(channels.first_parameter(), 6) + /// ``` + pub fn set_first_parameter(&mut self, value: u64) -> &mut Self { + self.first_parameter = value; + self + } + + /// Returns the second parameter, if there is any! + /// + /// # Example + /// ``` + /// # use hls_m3u8::types::Channels; + /// let mut channels = Channels::new(3); + /// # assert_eq!(channels.second_parameter(), &None); + /// + /// channels.set_second_parameter(Some(vec!["AAC", "MP3"])); + /// assert_eq!( + /// channels.second_parameter(), + /// &Some(vec!["AAC".to_string(), "MP3".to_string()]) + /// ) + /// ``` + /// + /// # Note + /// Currently there is no use for this parameter. + pub const fn second_parameter(&self) -> &Option> { &self.second_parameter } + + /// Sets the second parameter. + /// + /// # Example + /// ``` + /// # use hls_m3u8::types::Channels; + /// let mut channels = Channels::new(3); + /// # assert_eq!(channels.second_parameter(), &None); + /// + /// channels.set_second_parameter(Some(vec!["AAC", "MP3"])); + /// assert_eq!( + /// channels.second_parameter(), + /// &Some(vec!["AAC".to_string(), "MP3".to_string()]) + /// ) + /// ``` + /// + /// # Note + /// Currently there is no use for this parameter. + pub fn set_second_parameter(&mut self, value: Option>) -> &mut Self { + self.second_parameter = value.map(|v| v.into_iter().map(|s| s.to_string()).collect()); + self + } +} + +impl FromStr for Channels { + type Err = Error; + + fn from_str(input: &str) -> Result { + let parameters = input.split('/').collect::>(); + let first_parameter = parameters + .first() + .ok_or_else(|| Error::missing_attribute("First parameter of channels!"))? + .parse()?; + + let second_parameter = parameters + .get(1) + .map(|v| v.split(',').map(|v| v.to_string()).collect()); + + Ok(Self { + first_parameter, + second_parameter, + }) + } +} + +impl fmt::Display for Channels { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.first_parameter)?; + + if let Some(second) = &self.second_parameter { + write!(f, "/{}", second.join(","))?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + let mut channels = Channels::new(6); + assert_eq!(channels.to_string(), "6".to_string()); + + channels.set_first_parameter(7); + assert_eq!(channels.to_string(), "7".to_string()); + + assert_eq!( + "6/P,K,J".to_string(), + Channels::new(6) + .set_second_parameter(Some(vec!["P", "K", "J"])) + .to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!("6".parse::().unwrap(), Channels::new(6)); + let mut result = Channels::new(6); + result.set_second_parameter(Some(vec!["P", "K", "J"])); + + assert_eq!("6/P,K,J".parse::().unwrap(), result); + + assert!("garbage".parse::().is_err()); + 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..8ab8b20 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,84 +9,87 @@ 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() { + /// otherwise this function will return an error that has the kind + /// `ErrorKind::InvalidInput`. + 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 const fn as_f64(self) -> f64 { - self.0 - } + pub(crate) const fn from_f64_unchecked(value: f64) -> Self { Self(value) } - 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)) - } + /// Converts [`DecimalFloatingPoint`] to [`f64`]. + pub const fn as_f64(self) -> f64 { self.0 } } 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) - } + fn from_str(input: &str) -> Result { 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()) - } + fn from(value: f32) -> Self { (value as f64).into() } } #[cfg(test)] 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 +112,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..0deea04 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,22 +9,19 @@ 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, } impl DecimalResolution { - /// Creates a new DecimalResolution. - pub const fn new(width: usize, height: usize) -> Self { - Self { width, height } - } + /// Creates a new [`DecimalResolution`]. + pub const fn new(width: usize, height: usize) -> Self { Self { width, height } } /// Horizontal pixel dimension. - pub const fn width(&self) -> usize { - self.width - } + pub const fn width(&self) -> usize { self.width } /// Sets Horizontal pixel dimension. pub fn set_width(&mut self, value: usize) -> &mut Self { @@ -32,9 +30,7 @@ impl DecimalResolution { } /// Vertical pixel dimension. - pub const fn height(&self) -> usize { - self.height - } + pub const fn height(&self) -> usize { self.height } /// Sets Vertical pixel dimension. pub fn set_height(&mut self, value: usize) -> &mut Self { @@ -43,10 +39,9 @@ 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) } } impl FromStr for DecimalResolution { @@ -62,12 +57,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 +110,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..a6995cd 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -12,11 +12,12 @@ use crate::utils::{quote, unquote}; use crate::Error; #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] -#[builder(setter(into))] -/// [DecryptionKey] contains data, that is shared between [ExtXSessionKey] and [ExtXKey]. +#[builder(setter(into), build_fn(validate = "Self::validate"))] +/// [`DecryptionKey`] contains data, that is shared between [`ExtXSessionKey`] +/// and [`ExtXKey`]. /// -/// [ExtXSessionKey]: crate::tags::ExtXSessionKey -/// [ExtXKey]: crate::tags::ExtXKey +/// [`ExtXSessionKey`]: crate::tags::ExtXSessionKey +/// [`ExtXKey`]: crate::tags::ExtXKey pub struct DecryptionKey { /// The [EncryptionMethod]. pub(crate) method: EncryptionMethod, @@ -30,9 +31,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 { @@ -43,10 +53,7 @@ impl DecryptionKey { /// # use hls_m3u8::types::DecryptionKey; /// use hls_m3u8::types::EncryptionMethod; /// - /// let key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// ``` pub fn new(method: EncryptionMethod, uri: T) -> Self { Self { @@ -54,7 +61,7 @@ impl DecryptionKey { uri: Some(uri.to_string()), iv: None, key_format: None, - key_format_versions: KeyFormatVersions::new(), + key_format_versions: None, } } @@ -65,24 +72,14 @@ impl DecryptionKey { /// # use hls_m3u8::types::DecryptionKey; /// use hls_m3u8::types::EncryptionMethod; /// - /// let key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// - /// assert_eq!( - /// key.method(), - /// EncryptionMethod::Aes128 - /// ); + /// assert_eq!(key.method(), EncryptionMethod::Aes128); /// ``` - pub const fn method(&self) -> EncryptionMethod { - self.method - } + pub const fn method(&self) -> EncryptionMethod { self.method } /// Returns a Builder to build a [DecryptionKey]. - pub fn builder() -> DecryptionKeyBuilder { - DecryptionKeyBuilder::default() - } + pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } /// Sets the [EncryptionMethod]. /// @@ -91,10 +88,7 @@ impl DecryptionKey { /// # use hls_m3u8::types::DecryptionKey; /// use hls_m3u8::types::EncryptionMethod; /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// /// key.set_method(EncryptionMethod::SampleAes); /// @@ -103,32 +97,26 @@ 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 /// ``` /// # use hls_m3u8::types::DecryptionKey; /// use hls_m3u8::types::EncryptionMethod; /// - /// let key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// - /// assert_eq!( - /// key.uri(), - /// &Some("https://www.example.com/".to_string()) - /// ); + /// assert_eq!(key.uri(), &Some("https://www.example.com/".to_string())); /// ``` - pub const fn uri(&self) -> &Option { - &self.uri - } + pub const fn uri(&self) -> &Option { &self.uri } /// Sets the `URI` attribute. /// @@ -140,10 +128,7 @@ impl DecryptionKey { /// # use hls_m3u8::types::DecryptionKey; /// use hls_m3u8::types::EncryptionMethod; /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// /// key.set_uri(Some("http://www.google.com/")); /// @@ -152,27 +137,22 @@ 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; /// use hls_m3u8::types::EncryptionMethod; /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// - /// key.set_iv([ - /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 - /// ]); + /// # 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,139 +169,112 @@ impl DecryptionKey { /// Sets the `IV` attribute. /// - /// This attribute is optional. - /// /// # Example /// ``` /// # use hls_m3u8::types::DecryptionKey; /// use hls_m3u8::types::EncryptionMethod; /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// - /// key.set_iv([ - /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 - /// ]); + /// 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() + /// "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; - /// use hls_m3u8::types::{KeyFormat, EncryptionMethod}; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// /// key.set_key_format(Some(KeyFormat::Identity)); /// - /// assert_eq!( - /// key.key_format(), - /// Some(KeyFormat::Identity) - /// ); + /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); /// ``` - pub const fn key_format(&self) -> Option { - self.key_format - } + pub const fn key_format(&self) -> Option { self.key_format } - /// Sets the `KEYFORMAT` attribute. - /// - /// This attribute is optional. + /// Sets the [KeyFormat] attribute. /// /// # Example /// ``` /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::{KeyFormat, EncryptionMethod}; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); /// /// key.set_key_format(Some(KeyFormat::Identity)); /// - /// assert_eq!( - /// key.key_format(), - /// Some(KeyFormat::Identity) - /// ); + /// assert_eq!(key.key_format(), 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; - /// use hls_m3u8::types::{KeyFormatVersions, EncryptionMethod}; + /// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions}; /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "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; /// use hls_m3u8::types::EncryptionMethod; /// - /// let mut key = DecryptionKey::new( - /// EncryptionMethod::Aes128, - /// "https://www.example.com/" - /// ); + /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "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() + /// "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 @@ -350,22 +303,23 @@ impl FromStr for DecryptionKey { "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse()?), _ => { // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. + // > ignore any attribute/value pair with an unrecognized + // AttributeName. } } } - 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 +340,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 +367,7 @@ mod test { .key_format_versions(vec![1, 2, 3, 4, 5]) .build() .unwrap(); + assert_eq!( key.to_string(), "METHOD=AES-128,\ @@ -419,7 +377,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 +392,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 +422,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 +436,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 +449,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 +481,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..c19bf50 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -13,29 +13,22 @@ use crate::Error; pub struct InitializationVector(pub [u8; 16]); impl InitializationVector { - /// Converts the initialization vector to a slice. - pub const fn to_slice(&self) -> [u8; 16] { - self.0 - } + /// Converts the [InitializationVector] to a slice. + pub const fn to_slice(&self) -> [u8; 16] { self.0 } } impl From<[u8; 16]> for InitializationVector { - fn from(value: [u8; 16]) -> Self { - Self(value) - } + fn from(value: [u8; 16]) -> Self { Self(value) } } impl Deref for InitializationVector { type Target = [u8]; - fn deref(&self) -> &Self::Target { - &self.0 - } + + fn deref(&self) -> &Self::Target { &self.0 } } impl AsRef<[u8]> for InitializationVector { - fn as_ref(&self) -> &[u8] { - &self.0 - } + fn as_ref(&self) -> &[u8] { &self.0 } } impl fmt::Display for InitializationVector { @@ -51,21 +44,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..01510db 100644 --- a/src/types/key_format.rs +++ b/src/types/key_format.rs @@ -6,16 +6,15 @@ use crate::utils::{quote, tag, unquote}; use crate::Error; #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -/// KeyFormat specifies, how the key is represented in the resource identified by the URI +/// [`KeyFormat`] specifies, how the key is represented in the +/// resource identified by the `URI`. pub enum KeyFormat { /// The key is a single packed array of 16 octets in binary format. Identity, } impl Default for KeyFormat { - fn default() -> Self { - Self::Identity - } + fn default() -> Self { Self::Identity } } impl FromStr for KeyFormat { @@ -29,15 +28,11 @@ impl FromStr for KeyFormat { } impl fmt::Display for KeyFormat { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", quote("identity")) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", quote("identity")) } } impl RequiredVersion for KeyFormat { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V5 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } } #[cfg(test)] @@ -54,6 +49,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..9a871cb 100644 --- a/src/types/key_format_versions.rs +++ b/src/types/key_format_versions.rs @@ -8,25 +8,21 @@ use crate::Error; /// A list of [usize], that can be used to indicate which version(s) /// this instance complies with, if more than one version of a particular -/// [KeyFormat] is defined. +/// [`KeyFormat`] is defined. /// -/// [KeyFormat]: crate::types::KeyFormat +/// [`KeyFormat`]: crate::types::KeyFormat #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct KeyFormatVersions(Vec); impl Default for KeyFormatVersions { - fn default() -> Self { - Self(vec![1]) - } + fn default() -> Self { Self(vec![1]) } } impl KeyFormatVersions { - /// Makes a new [KeyFormatVersions]. - pub fn new() -> Self { - Self::default() - } + /// Makes a new [`KeyFormatVersions`]. + pub fn new() -> Self { Self::default() } - /// Add a value to the [KeyFormatVersions]. + /// Add a value to the [`KeyFormatVersions`]. pub fn push(&mut self, value: usize) { if self.is_default() { self.0 = vec![value]; @@ -35,30 +31,23 @@ 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() - } + /// Returns `true`, if [`KeyFormatVersions`] has the default value of + /// `vec![1]`. + pub fn is_default(&self) -> bool { self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() } } impl Deref for KeyFormatVersions { type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } + fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for KeyFormatVersions { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl RequiredVersion for KeyFormatVersions { - fn required_version(&self) -> ProtocolVersion { - ProtocolVersion::V5 - } + fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } } impl FromStr for KeyFormatVersions { @@ -66,7 +55,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::>(); @@ -100,9 +89,7 @@ impl fmt::Display for KeyFormatVersions { } impl>> From for KeyFormatVersions { - fn from(value: T) -> Self { - Self(value.into()) - } + fn from(value: T) -> Self { Self(value.into()) } } #[cfg(test)] 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/mod.rs b/src/types/mod.rs index 70c891f..64aa441 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,6 @@ //! Miscellaneous types. mod byte_range; +mod channels; mod closed_captions; mod decimal_floating_point; mod decimal_resolution; @@ -16,6 +17,7 @@ mod signed_decimal_floating_point; mod stream_inf; pub use byte_range::*; +pub use channels::*; pub use closed_captions::*; pub(crate) use decimal_floating_point::*; pub(crate) use decimal_resolution::*; diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index a39747d..c90c5a5 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,26 +46,28 @@ pub enum ProtocolVersion { } impl ProtocolVersion { - /// Returns the newest ProtocolVersion, that is supported by this library. - pub const fn latest() -> Self { - Self::V7 - } + /// 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 } } 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"), + } } } @@ -86,9 +91,7 @@ impl FromStr for ProtocolVersion { } impl Default for ProtocolVersion { - fn default() -> Self { - Self::V1 - } + fn default() -> Self { Self::V1 } } #[cfg(test)] @@ -115,5 +118,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..541503b 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -1,55 +1,100 @@ -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 const fn as_f64(self) -> f64 { - self.0 - } + 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..e40d037 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, @@ -48,9 +47,7 @@ impl StreamInf { /// let stream = StreamInf::new(20); /// assert_eq!(stream.bandwidth(), 20); /// ``` - pub const fn bandwidth(&self) -> u64 { - self.bandwidth - } + pub const fn bandwidth(&self) -> u64 { self.bandwidth } /// Sets the peak segment bit rate of the variant stream. /// @@ -77,9 +74,7 @@ impl StreamInf { /// let stream = StreamInf::new(20); /// assert_eq!(stream.video(), &None); /// ``` - pub const fn video(&self) -> &Option { - &self.video - } + pub const fn video(&self) -> &Option { &self.video } /// Sets the group identifier for the video in the variant stream. /// @@ -106,9 +101,7 @@ impl StreamInf { /// let stream = StreamInf::new(20); /// assert_eq!(stream.average_bandwidth(), None); /// ``` - pub const fn average_bandwidth(&self) -> Option { - self.average_bandwidth - } + pub const fn average_bandwidth(&self) -> Option { self.average_bandwidth } /// Sets the average segment bit rate of the variant stream. /// @@ -126,7 +119,8 @@ impl StreamInf { self } - /// A string that represents the list of codec types contained the variant stream. + /// A string that represents the list of codec types contained the variant + /// stream. /// /// # Examples /// ``` @@ -135,11 +129,10 @@ impl StreamInf { /// let stream = StreamInf::new(20); /// assert_eq!(stream.codecs(), &None); /// ``` - pub const fn codecs(&self) -> &Option { - &self.codecs - } + pub const fn codecs(&self) -> &Option { &self.codecs } - /// A string that represents the list of codec types contained the variant stream. + /// A string that represents the list of codec types contained the variant + /// stream. /// /// # Examples /// ``` @@ -182,6 +175,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 { @@ -202,9 +197,7 @@ impl StreamInf { /// let stream = StreamInf::new(20); /// assert_eq!(stream.hdcp_level(), None); /// ``` - pub const fn hdcp_level(&self) -> Option { - self.hdcp_level - } + pub const fn hdcp_level(&self) -> Option { self.hdcp_level } /// The HDCP level of the variant stream. /// @@ -259,20 +252,21 @@ 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()?), "VIDEO" => video = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. + // > ignore any attribute/value pair with an unrecognized + // AttributeName. } } } - let bandwidth = bandwidth.ok_or(Error::missing_value("BANDWIDTH"))?; + let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?; Ok(Self { bandwidth, @@ -284,3 +278,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..a48a2eb 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`) @@ -31,17 +47,17 @@ pub(crate) fn unquote(value: T) -> String { /// Puts a string inside quotes. pub(crate) fn quote(value: T) -> String { - // the replace is for the case, that quote is called on an already quoted string, which could - // cause problems! + // the replace is for the case, that quote is called on an already quoted + // string, which could cause problems! format!("\"{}\"", value.to_string().replace("\"", "")) } -/// Checks, if the given tag is at the start of the input. If this is the case, it will remove it -/// and return the rest of the input. +/// Checks, if the given tag is at the start of the input. If this is the case, +/// it will remove it and return the rest of the input. /// /// # Error -/// This function will return `Error::MissingTag`, if the input doesn't start with the tag, that -/// has been passed to this function. +/// This function will return `Error::MissingTag`, if the input doesn't start +/// with the tag, that has been passed to this function. pub(crate) fn tag(input: &str, tag: T) -> crate::Result<&str> where T: AsRef, @@ -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()); } }