From 0c4fa008e6fc37cb9e2bc6f43ae9c4c3cd9b35d1 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 22 Sep 2019 18:00:38 +0200 Subject: [PATCH] more documentation #31 + tests #25 --- src/attribute.rs | 2 +- src/error.rs | 8 + src/tags/basic/m3u.rs | 12 +- src/tags/basic/version.rs | 31 +++- .../master_playlist/i_frame_stream_inf.rs | 24 ++- src/tags/master_playlist/media.rs | 87 +++++++-- src/tags/master_playlist/session_data.rs | 127 +++++++++---- src/tags/master_playlist/session_key.rs | 52 ++++-- src/tags/master_playlist/stream_inf.rs | 4 +- .../media_playlist/discontinuity_sequence.rs | 50 +++++- src/tags/media_playlist/end_list.rs | 14 +- src/tags/media_playlist/i_frames_only.rs | 16 +- src/tags/media_playlist/media_sequence.rs | 47 ++++- src/tags/media_playlist/playlist_type.rs | 19 +- src/tags/media_playlist/target_duration.rs | 20 ++- src/tags/media_segment/byte_range.rs | 44 ++++- src/tags/media_segment/date_range.rs | 4 +- src/tags/media_segment/discontinuity.rs | 15 +- src/tags/media_segment/inf.rs | 130 ++++++++++---- src/tags/media_segment/key.rs | 35 +++- src/tags/media_segment/map.rs | 13 +- src/tags/media_segment/program_date_time.rs | 75 ++++++-- src/tags/mod.rs | 86 --------- src/types/decryption_key.rs | 160 ++++++++++------- src/types/key_format.rs | 68 +++++++ src/types/key_format_versions.rs | 170 ++++++++++++++++++ src/types/mod.rs | 4 + src/utils.rs | 2 +- 28 files changed, 984 insertions(+), 335 deletions(-) create mode 100644 src/types/key_format.rs create mode 100644 src/types/key_format_versions.rs diff --git a/src/attribute.rs b/src/attribute.rs index d0cf36e..2da9be6 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -110,7 +110,7 @@ mod test { #[test] fn test_parser() { - let pairs = ("FOO=BAR,BAR=\"baz,qux\",ABC=12.3") + let pairs = "FOO=BAR,BAR=\"baz,qux\",ABC=12.3" .parse::() .unwrap(); diff --git a/src/error.rs b/src/error.rs index c2db7c2..14c43f4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -69,6 +69,10 @@ pub enum ErrorKind { /// An Error from a Builder. BuilderError(String), + #[fail(display = "Missing Attribute: {}", _0)] + /// An attribute is missing. + MissingAttribute(String), + /// Hints that destructuring should not be exhaustive. /// /// This enum may grow additional variants, so this makes sure clients @@ -185,6 +189,10 @@ impl Error { pub(crate) fn chrono(value: T) -> Self { Self::from(ErrorKind::ChronoParseError(value.to_string())) } + + pub(crate) fn missing_attribute(value: T) -> Self { + Self::from(ErrorKind::MissingAttribute(value.to_string())) + } } impl From<::std::num::ParseIntError> for Error { diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index d64939c..ac870b1 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -5,10 +5,18 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.1.1. EXTM3U] +/// # [4.3.1.1. EXTM3U] +/// The [ExtM3u] tag indicates that the file is an Extended [M3U] +/// Playlist file. /// +/// Its format is: +/// ```text +/// #EXTM3U +/// ``` +/// +/// [M3U]: https://en.wikipedia.org/wiki/M3U /// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] pub struct ExtM3u; impl ExtM3u { diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index b5afef2..bff523f 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -5,21 +5,40 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.1.2. EXT-X-VERSION] +/// # [4.3.1.2. EXT-X-VERSION] +/// The [ExtXVersion] tag indicates the compatibility version of the +/// Playlist file, its associated media, and its server. +/// +/// The [ExtXVersion] tag applies to the entire Playlist file. Its +/// format is: +/// +/// ```text +/// #EXT-X-VERSION: +/// ``` +/// where `n` is an integer indicating the protocol compatibility version +/// number. /// /// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[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); + /// ``` pub const fn new(version: ProtocolVersion) -> Self { Self(version) } - /// Returns the protocol compatibility version of the playlist containing this tag. + /// Returns the protocol compatibility version of the playlist, containing this tag. /// /// # Example /// ``` @@ -84,8 +103,8 @@ mod test { #[test] fn test_parser() { assert_eq!( - "#EXT-X-VERSION:6".parse().ok(), - Some(ExtXVersion::new(ProtocolVersion::V6)) + "#EXT-X-VERSION:6".parse::().unwrap(), + ExtXVersion::new(ProtocolVersion::V6) ); } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index fc38814..167b358 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -10,7 +10,7 @@ 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]. +/// alone, in that it does not apply to a particular `URI` in the [Master Playlist]. /// /// Its format is: /// @@ -38,7 +38,7 @@ impl ExtXIFrameStreamInf { } } - /// Returns the URI, that identifies the associated media playlist. + /// Returns the `URI`, that identifies the associated media playlist. /// /// # Example /// ``` @@ -51,7 +51,7 @@ impl ExtXIFrameStreamInf { &self.uri } - /// Sets the URI, that identifies the associated media playlist. + /// Sets the `URI`, that identifies the associated media playlist. /// /// # Example /// ``` @@ -126,22 +126,20 @@ mod test { #[test] fn test_display() { - let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; - assert_eq!(ExtXIFrameStreamInf::new("foo", 1000).to_string(), text); + assert_eq!( + ExtXIFrameStreamInf::new("foo", 1000).to_string(), + "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000".to_string() + ); } #[test] fn test_parser() { - let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; - let i_frame_stream_inf = ExtXIFrameStreamInf::new("foo", 1000); assert_eq!( - text.parse::().unwrap(), - i_frame_stream_inf.clone() + "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000" + .parse::() + .unwrap(), + ExtXIFrameStreamInf::new("foo", 1000) ); - - assert_eq!(i_frame_stream_inf.uri(), "foo"); - assert_eq!(i_frame_stream_inf.bandwidth(), 1000); - // TODO: test all the optional fields } #[test] diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 4cf05ce..9232b25 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -8,9 +8,23 @@ use crate::types::{InStreamId, MediaType, ProtocolVersion, RequiredVersion}; use crate::utils::{parse_yes_or_no, quote, tag, unquote}; use crate::Error; -/// [4.3.4.1. EXT-X-MEDIA] +/// # [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 +/// angles. /// -/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 +/// 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 #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] #[builder(setter(into))] #[builder(build_fn(validate = "Self::validate"))] @@ -54,22 +68,27 @@ impl ExtXMediaBuilder { fn validate(&self) -> Result<(), String> { let media_type = self .media_type - .ok_or(Error::missing_value("self.media_type").to_string())?; + .ok_or(Error::missing_attribute("MEDIA-TYPE").to_string())?; if MediaType::ClosedCaptions == media_type { - if let None = self.uri { - return Err(Error::missing_value("self.uri").to_string()); + if self.uri.is_some() { + return Err(Error::custom( + "Unexpected attribute: \"URL\" for MediaType::ClosedCaptions!", + ) + .to_string()); } self.instream_id - .ok_or(Error::missing_value("self.instream_id").to_string())?; + .ok_or(Error::missing_attribute("INSTREAM-ID").to_string())?; } else { - if let Some(_) = &self.instream_id { - return Err(Error::invalid_input().to_string()); + if self.instream_id.is_some() { + return Err(Error::custom("Unexpected attribute: \"INSTREAM-ID\"!").to_string()); } } - if self.is_default.unwrap_or(false) && self.is_autoselect.unwrap_or(false) { - return Err(Error::invalid_input().to_string()); + if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) { + return Err( + Error::custom("If `DEFAULT` is true, `AUTOSELECT` has to be true too!").to_string(), + ); } if MediaType::Subtitles != media_type { @@ -85,7 +104,7 @@ 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 { media_type, @@ -284,6 +303,52 @@ mod test { #[test] fn test_display() { + // 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() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"eng/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"eng\",\ + NAME=\"English\",\ + DEFAULT=YES,\ + AUTOSELECT=YES" + .to_string() + ); + + 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() + .to_string(), + "#EXT-X-MEDIA:\ + TYPE=AUDIO,\ + URI=\"fre/prog_index.m3u8\",\ + GROUP-ID=\"audio\",\ + LANGUAGE=\"fre\",\ + NAME=\"Français\",\ + AUTOSELECT=YES" + .to_string() + ); + assert_eq!( ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(), "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string() diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 09d74ae..b97b9e4 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -22,11 +22,12 @@ pub enum SessionData { Uri(String), } -/// [4.3.4.4. EXT-X-SESSION-DATA] +/// # [4.3.4.4. EXT-X-SESSION-DATA] /// /// The [ExtXSessionData] tag allows arbitrary session data to be -/// carried in a [Master Playlist](crate::MasterPlaylist). +/// carried in a [Master Playlist]. /// +/// [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))] @@ -319,40 +320,73 @@ mod test { #[test] fn test_display() { - let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; - assert_eq!(tag.to_string(), text); + assert_eq!( + "#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()) + ) + .to_string() + ); - let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; - assert_eq!(tag.to_string(), text); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ + VALUE=\"This is an example\",LANGUAGE=\"en\"" + .to_string(), + ExtXSessionData::with_language( + "com.example.title", + SessionData::Value("This is an example".to_string()), + "en" + ) + .to_string() + ); - let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; - assert_eq!(tag.to_string(), text); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ + VALUE=\"Este es un ejemplo\",LANGUAGE=\"es\"" + .to_string(), + ExtXSessionData::with_language( + "com.example.title", + SessionData::Value("Este es un ejemplo".to_string()), + "es" + ) + .to_string() + ); + + assert_eq!( + "#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(), + ExtXSessionData::new("foo", SessionData::Uri("bar".into())).to_string() + ); + + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"".to_string(), + ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") + .to_string() + ); } #[test] fn test_parser() { - let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"" - .parse::() - .unwrap(); - assert_eq!( - tag, + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"" + .parse::() + .unwrap(), ExtXSessionData::new( "com.example.lyrics", SessionData::Uri("lyrics.json".to_string()) ) ); - let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ - LANGUAGE=\"en\", VALUE=\"This is an example\"" - .parse::() - .unwrap(); - assert_eq!( - tag, + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ + LANGUAGE=\"en\", VALUE=\"This is an example\"" + .parse::() + .unwrap(), ExtXSessionData::with_language( "com.example.title", SessionData::Value("This is an example".to_string()), @@ -360,13 +394,11 @@ mod test { ) ); - let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ - LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\"" - .parse::() - .unwrap(); - assert_eq!( - tag, + "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ + LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\"" + .parse::() + .unwrap(), ExtXSessionData::with_language( "com.example.title", SessionData::Value("Este es un ejemplo".to_string()), @@ -374,16 +406,37 @@ mod test { ) ); - let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; - assert_eq!(text.parse::().unwrap(), tag); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\"" + .parse::() + .unwrap(), + ExtXSessionData::new("foo", SessionData::Value("bar".into())) + ); - let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; - assert_eq!(text.parse::().unwrap(), tag); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\"" + .parse::() + .unwrap(), + ExtXSessionData::new("foo", SessionData::Uri("bar".into())) + ); - let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); - let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; - assert_eq!(text.parse::().unwrap(), tag); + assert_eq!( + "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"" + .parse::() + .unwrap(), + ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") + ); + } + + #[test] + fn test_required_version() { + assert_eq!( + ExtXSessionData::new( + "com.example.lyrics", + SessionData::Uri("lyrics.json".to_string()) + ) + .required_version(), + ProtocolVersion::V1 + ); } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index ba1d9fc..0162213 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -6,8 +6,19 @@ use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion, RequiredVer use crate::utils::tag; use crate::Error; -/// [4.3.4.5. EXT-X-SESSION-KEY] +/// # [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 +/// first. /// +/// Its format is: +/// ```text +/// #EXT-X-SESSION-KEY: +/// ``` +/// +/// [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); @@ -16,8 +27,21 @@ impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; /// Makes a new [ExtXSessionKey] tag. + /// /// # Panic - /// This method will panic, if the [EncryptionMethod] is 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/" + /// ); + /// ``` pub fn new(method: EncryptionMethod, uri: T) -> Self { if method == EncryptionMethod::None { panic!("The EncryptionMethod is not allowed to be None"); @@ -29,18 +53,15 @@ impl ExtXSessionKey { impl RequiredVersion for ExtXSessionKey { fn required_version(&self) -> ProtocolVersion { - if self.0.key_format.is_some() | self.0.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.0.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } + self.0.required_version() } } impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.0.method == EncryptionMethod::None { + return Err(fmt::Error); + } write!(f, "{}{}", Self::PREFIX, self.0) } } @@ -71,7 +92,7 @@ impl DerefMut for ExtXSessionKey { #[cfg(test)] mod test { use super::*; - use crate::types::EncryptionMethod; + use crate::types::{EncryptionMethod, KeyFormat}; #[test] fn test_display() { @@ -121,11 +142,16 @@ mod test { key ); - key.set_key_format("baz"); + key.set_key_format(Some(KeyFormat::Identity)); assert_eq!( - r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://www.example.com/hls-key/key.bin",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# - .parse::().unwrap(), + "#EXT-X-SESSION-KEY:\ + METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52,\ + KEYFORMAT=\"identity\"" + .parse::() + .unwrap(), key ) } diff --git a/src/tags/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index e50a297..89ac00d 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -44,16 +44,18 @@ impl ExtXStreamInf { } } + /// Sets the `URI` that identifies the associated media playlist. 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. + /// 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. pub fn set_frame_rate(&mut self, value: Option) -> &mut Self { self.frame_rate = value.map(|v| v.into()); self diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 28dda75..8ca5504 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -4,25 +4,67 @@ use std::str::FromStr; use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; -/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE] +/// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// -/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.3 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +/// 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. +/// +/// Its format is: +/// ```text +/// #EXT-X-DISCONTINUITY-SEQUENCE: +/// ``` +/// where `number` is a [u64]. +/// +/// [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)] pub struct ExtXDiscontinuitySequence(u64); impl ExtXDiscontinuitySequence { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; - /// Makes a new `ExtXDiscontinuitySequence` tag. + /// Makes a new [ExtXDiscontinuitySequence] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; + /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); + /// ``` 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. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; + /// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5); + /// + /// assert_eq!(discontinuity_sequence.seq_num(), 5); + /// ``` pub const fn seq_num(&self) -> u64 { self.0 } + + /// Sets the sequence number. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXDiscontinuitySequence; + /// let mut discontinuity_sequence = ExtXDiscontinuitySequence::new(5); + /// + /// discontinuity_sequence.set_seq_num(10); + /// assert_eq!(discontinuity_sequence.seq_num(), 10); + /// ``` + pub fn set_seq_num(&mut self, value: u64) -> &mut Self { + self.0 = value; + self + } } impl RequiredVersion for ExtXDiscontinuitySequence { diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index c4fcc5b..2865b4c 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -5,9 +5,19 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.4. EXT-X-ENDLIST] +/// # [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. /// -/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4 +/// Its format is: +/// ```text +/// #EXT-X-ENDLIST +/// ``` +/// +/// [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)] pub struct ExtXEndList; diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 796c68f..c0291ca 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -5,9 +5,21 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.6. EXT-X-I-FRAMES-ONLY] +/// # [4.4.3.6. EXT-X-I-FRAMES-ONLY] +/// 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 +/// reverse, and scrubbing. /// -/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6 +/// Its format is: +/// ```text +/// #EXT-X-I-FRAMES-ONLY +/// ``` +/// +/// [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)] pub struct ExtXIFramesOnly; diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index db4301e..c841b19 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -5,25 +5,64 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE] +/// # [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. /// -/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.2 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// Its format is: +/// ```text +/// #EXT-X-MEDIA-SEQUENCE: +/// ``` +/// where `number` is a [u64]. +/// +/// [Media Segment]: crate::MediaSegment +/// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.2 +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] 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) } /// Returns the sequence number of the first media segment, /// that appears in the associated playlist. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMediaSequence; + /// let media_sequence = ExtXMediaSequence::new(5); + /// + /// assert_eq!(media_sequence.seq_num(), 5); + /// ``` pub const fn seq_num(&self) -> u64 { self.0 } + + /// Sets the sequence number. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtXMediaSequence; + /// let mut media_sequence = ExtXMediaSequence::new(5); + /// + /// media_sequence.set_seq_num(10); + /// assert_eq!(media_sequence.seq_num(), 10); + /// ``` + pub fn set_seq_num(&mut self, value: u64) -> &mut Self { + self.0 = value; + self + } } impl RequiredVersion for ExtXMediaSequence { diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index e0b7ce8..b6ded13 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -5,26 +5,25 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/rfc8216#section-4.3.3.5) +/// # [4.4.3.5. EXT-X-PLAYLIST-TYPE] /// -/// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the -/// Media Playlist. It applies to the entire Media Playlist. -/// It is OPTIONAL. Its format is: +/// The [ExtXPlaylistType] tag provides mutability information about the +/// [Media Playlist]. It applies to the entire [Media Playlist]. /// +/// Its format is: /// ```text /// #EXT-X-PLAYLIST-TYPE: /// ``` /// -/// # Note -/// If the EXT-X-PLAYLIST-TYPE tag is omitted from a Media Playlist, the -/// Playlist can be updated according to the rules in Section 6.2.1 with -/// no additional restrictions. +/// [Media Playlist]: crate::MediaPlaylist +/// [4.4.3.5. EXT-X-PLAYLIST-TYPE]: +/// 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 + /// 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, } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index 522616d..d7fe890 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -6,19 +6,31 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.3.1. EXT-X-TARGETDURATION] +/// # [4.4.3.1. EXT-X-TARGETDURATION] +/// The [ExtXTargetDuration] tag specifies the maximum [Media Segment] +/// duration. /// -/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 +/// Its format is: +/// ```text +/// #EXT-X-TARGETDURATION: +/// ``` +/// where `s` is the target [Duration] in seconds. +/// +/// [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 #[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. /// - /// Note that the nanoseconds part of the `duration` will be discarded. + /// # Note + /// 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())) } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index e0d02b7..37c3330 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -1,26 +1,39 @@ use std::fmt; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.2.2. EXT-X-BYTERANGE] +/// # [4.4.2.2. EXT-X-BYTERANGE] /// -/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 +/// The [ExtXByteRange] tag indicates that a [Media Segment] is a sub-range +/// of the resource identified by its `URI`. +/// +/// Its format is: +/// ```text +/// #EXT-X-BYTERANGE:[@] +/// ``` +/// +/// where `n` is a [usize] indicating the length of the sub-range in bytes. +/// 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 +/// [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)] 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 /// ``` - /// use hls_m3u8::tags::ExtXByteRange; - /// + /// # use hls_m3u8::tags::ExtXByteRange; /// let byte_range = ExtXByteRange::new(20, Some(5)); /// ``` pub const fn new(length: usize, start: Option) -> Self { @@ -31,7 +44,7 @@ impl ExtXByteRange { /// /// # Example /// ``` - /// use hls_m3u8::tags::ExtXByteRange; + /// # use hls_m3u8::tags::ExtXByteRange; /// use hls_m3u8::types::ByteRange; /// /// let byte_range = ExtXByteRange::new(20, Some(5)); @@ -56,6 +69,12 @@ impl Deref for ExtXByteRange { } } +impl DerefMut for ExtXByteRange { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl fmt::Display for ExtXByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; @@ -132,6 +151,17 @@ mod test { assert_eq!(byte_range.start(), Some(22)); } + #[test] + fn test_deref_mut() { + let mut byte_range = ExtXByteRange::new(0, Some(22)); + + byte_range.set_length(100); + byte_range.set_start(Some(50)); + + assert_eq!(byte_range.length(), 100); + assert_eq!(byte_range.start(), Some(50)); + } + #[test] fn test_required_version() { assert_eq!( diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 2b6619a..0734b6f 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -18,8 +18,8 @@ use crate::Error; #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXDateRange { - /// A string that uniquely identifies a Date Range in the Playlist. - /// This attribute is REQUIRED. + /// 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 diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 2289a24..a4846d9 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -5,9 +5,18 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.2.3. EXT-X-DISCONTINUITY] +/// # [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. /// -/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3 +/// Its format is: +/// ```text +/// #EXT-X-DISCONTINUITY +/// ``` +/// +/// [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)] pub struct ExtXDiscontinuity; @@ -50,7 +59,7 @@ mod test { #[test] fn test_parser() { - assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap(),) + assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap()) } #[test] diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index af0178a..8524d81 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -6,10 +6,10 @@ use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.2.1. EXTINF](https://tools.ietf.org/html/rfc8216#section-4.3.2.1) +/// # [4.4.2.1. EXTINF] /// -/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies -/// only to the next [Media Segment]. This tag is REQUIRED for each [Media Segment]. +/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies +/// only to the next [Media Segment]. /// /// Its format is: /// ```text @@ -18,32 +18,8 @@ use crate::Error; /// The title is an optional informative title about the [Media Segment]. /// /// [Media Segment]: crate::media_segment::MediaSegment -/// -/// # Examples -/// Parsing from a String: -/// ``` -/// use std::time::Duration; -/// use hls_m3u8::tags::ExtInf; -/// -/// let ext_inf = "#EXTINF:8,".parse::().expect("Failed to parse tag!"); -/// -/// assert_eq!(ext_inf.duration(), Duration::from_secs(8)); -/// assert_eq!(ext_inf.title(), None); -/// ``` -/// -/// Converting to a String: -/// ``` -/// use std::time::Duration; -/// use hls_m3u8::tags::ExtInf; -/// -/// let ext_inf = ExtInf::with_title( -/// Duration::from_millis(88), -/// "title" -/// ); -/// -/// assert_eq!(ext_inf.duration(), Duration::from_millis(88)); -/// assert_eq!(ext_inf.to_string(), "#EXTINF:0.088,title".to_string()); -/// ``` +/// [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)] pub struct ExtInf { duration: Duration, @@ -53,7 +29,15 @@ pub struct ExtInf { impl ExtInf { pub(crate) const PREFIX: &'static str = "#EXTINF:"; - /// Makes a new `ExtInf` tag. + /// Makes a new [ExtInf] tag. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let ext_inf = ExtInf::new(Duration::from_secs(5)); + /// ``` pub const fn new(duration: Duration) -> Self { ExtInf { duration, @@ -61,7 +45,15 @@ impl ExtInf { } } - /// Makes a new `ExtInf` tag with the given title. + /// Makes a new [ExtInf] tag with the given title. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); + /// ``` pub fn with_title(duration: Duration, title: T) -> Self { ExtInf { duration, @@ -70,13 +62,81 @@ impl ExtInf { } /// Returns the duration of the associated media segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let ext_inf = ExtInf::new(Duration::from_secs(5)); + /// + /// assert_eq!( + /// ext_inf.duration(), + /// Duration::from_secs(5) + /// ); + /// ``` pub const fn duration(&self) -> Duration { self.duration } + /// Sets the duration of the associated media segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let mut ext_inf = ExtInf::new(Duration::from_secs(5)); + /// + /// ext_inf.set_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; + self + } + /// Returns the title of the associated media segment. - pub fn title(&self) -> Option<&String> { - self.title.as_ref() + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); + /// + /// assert_eq!( + /// ext_inf.title(), + /// &Some("title".to_string()) + /// ); + /// ``` + pub const fn title(&self) -> &Option { + &self.title + } + + /// Sets the title of the associated media segment. + /// + /// # Example + /// ``` + /// # use hls_m3u8::tags::ExtInf; + /// use std::time::Duration; + /// + /// let mut ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); + /// + /// ext_inf.set_title(Some("better title")); + /// + /// 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()); + self } } @@ -199,10 +259,10 @@ mod test { #[test] fn test_title() { - assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), None); + assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), &None); assert_eq!( ExtInf::with_title(Duration::from_secs(5), "title").title(), - Some(&"title".to_string()) + &Some("title".to_string()) ); } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index ecdc3d6..e81479d 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -2,15 +2,30 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use crate::types::{DecryptionKey, EncryptionMethod}; +use crate::types::{DecryptionKey, EncryptionMethod, KeyFormatVersions}; use crate::utils::tag; use crate::Error; -/// [4.3.2.4. EXT-X-KEY] +/// # [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). +/// +/// The format is: +/// ```text +/// #EXT-X-KEY: +/// ``` /// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 /// # Note /// In case of an empty key (`EncryptionMethod::None`), all attributes will be ignored. +/// +/// [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)] pub struct ExtXKey(DecryptionKey); @@ -49,13 +64,13 @@ impl ExtXKey { /// "#EXT-X-KEY:METHOD=NONE" /// ); /// ``` - pub const fn empty() -> Self { + pub fn empty() -> Self { Self(DecryptionKey { method: EncryptionMethod::None, uri: None, iv: None, key_format: None, - key_format_versions: None, + key_format_versions: KeyFormatVersions::new(), }) } @@ -109,7 +124,7 @@ impl DerefMut for ExtXKey { #[cfg(test)] mod test { use super::*; - use crate::types::EncryptionMethod; + use crate::types::{EncryptionMethod, KeyFormat}; #[test] fn test_display() { @@ -120,12 +135,12 @@ mod test { let mut key = ExtXKey::empty(); // it is expected, that all attributes will be ignored in an empty key! - key.set_key_format("hi"); + key.set_key_format(Some(KeyFormat::Identity)); key.set_iv([ 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("1/2/3"); + key.set_key_format_versions(vec![1, 2, 3]); assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); } @@ -133,7 +148,9 @@ mod test { #[test] fn test_parser() { assert_eq!( - r#"#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + "#EXT-X-KEY:\ + METHOD=AES-128,\ + URI=\"https://priv.example.com/key.php?r=52\"" .parse::() .unwrap(), ExtXKey::new( diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index d680f56..9b1ce94 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -6,9 +6,18 @@ use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; use crate::utils::{quote, tag, unquote}; use crate::Error; -/// [4.3.2.5. EXT-X-MAP] +/// # [4.4.2.5. EXT-X-MAP] +/// The [ExtXMap] tag specifies how to obtain the Media Initialization +/// Section, required to parse the applicable [Media Segment]s. /// -/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 +/// Its format is: +/// ```text +/// #EXT-X-MAP: +/// ``` +/// +/// [Media Segment]: crate::MediaSegment +/// [4.4.2.5. EXT-X-MAP]: +/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.5 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXMap { uri: String, diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 2f852f6..d6a68ce 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; use chrono::{DateTime, FixedOffset}; @@ -7,8 +8,11 @@ use crate::types::{ProtocolVersion, RequiredVersion}; use crate::utils::tag; use crate::Error; -/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] +/// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] +/// The [ExtXProgramDateTime] tag associates the first sample of a +/// [Media Segment] with an absolute date and/or time. /// +/// [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); @@ -17,13 +21,33 @@ impl ExtXProgramDateTime { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; /// Makes a new `ExtXProgramDateTime` tag. - pub fn new>>(date_time: T) -> Self { - Self(date_time.into()) + /// + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXProgramDateTime; + /// use chrono::{FixedOffset, TimeZone}; + /// + /// 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) + /// ); + /// ``` + 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 + 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 { + self.0 = value; + self } } @@ -51,6 +75,20 @@ impl FromStr for ExtXProgramDateTime { } } +impl Deref for ExtXProgramDateTime { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ExtXProgramDateTime { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + #[cfg(test)] mod test { use super::*; @@ -60,14 +98,13 @@ mod test { #[test] fn test_display() { - let date_time = "2010-02-19T14:54:23.031+08:00" - .parse::>() - .unwrap(); - - let program_date_time = ExtXProgramDateTime::new(date_time); - assert_eq!( - program_date_time.to_string(), + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31) + ) + .to_string(), "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string() ); } @@ -88,12 +125,14 @@ mod test { #[test] fn test_required_version() { - let program_date_time = ExtXProgramDateTime::new( - FixedOffset::east(8 * HOURS_IN_SECS) - .ymd(2010, 2, 19) - .and_hms_milli(14, 54, 23, 31), + assert_eq!( + ExtXProgramDateTime::new( + FixedOffset::east(8 * HOURS_IN_SECS) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ) + .required_version(), + ProtocolVersion::V1 ); - - assert_eq!(program_date_time.required_version(), ProtocolVersion::V1); } } diff --git a/src/tags/mod.rs b/src/tags/mod.rs index ebad0b7..35ae4c4 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -2,16 +2,6 @@ //! //! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3 -macro_rules! impl_from { - ($to:ident, $from:ident) => { - impl From<$from> for $to { - fn from(f: $from) -> Self { - $to::$from(f) - } - } - }; -} - mod basic; mod master_playlist; mod media_playlist; @@ -23,79 +13,3 @@ pub use master_playlist::*; pub use media_playlist::*; pub use media_segment::*; pub use shared::*; - -/// [4.3.4. Master Playlist Tags] -/// -/// See also [4.3.5. Media or Master Playlist Tags] -/// -/// [4.3.4. Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.4 -/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5 -#[allow(missing_docs)] -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MasterPlaylistTag { - ExtXMedia(ExtXMedia), - ExtXStreamInf(ExtXStreamInf), - ExtXIFrameStreamInf(ExtXIFrameStreamInf), - ExtXSessionData(ExtXSessionData), - ExtXSessionKey(ExtXSessionKey), - ExtXIndependentSegments(ExtXIndependentSegments), - ExtXStart(ExtXStart), -} -impl_from!(MasterPlaylistTag, ExtXMedia); -impl_from!(MasterPlaylistTag, ExtXStreamInf); -impl_from!(MasterPlaylistTag, ExtXIFrameStreamInf); -impl_from!(MasterPlaylistTag, ExtXSessionData); -impl_from!(MasterPlaylistTag, ExtXSessionKey); -impl_from!(MasterPlaylistTag, ExtXIndependentSegments); -impl_from!(MasterPlaylistTag, ExtXStart); - -/// [4.3.3. Media Playlist Tags] -/// -/// See also [4.3.5. Media or Master Playlist Tags] -/// -/// [4.3.3. Media Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.3 -/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MediaPlaylistTag { - ExtXTargetDuration(ExtXTargetDuration), - ExtXMediaSequence(ExtXMediaSequence), - ExtXDiscontinuitySequence(ExtXDiscontinuitySequence), - ExtXEndList(ExtXEndList), - ExtXPlaylistType(ExtXPlaylistType), - ExtXIFramesOnly(ExtXIFramesOnly), - ExtXIndependentSegments(ExtXIndependentSegments), - ExtXStart(ExtXStart), -} -impl_from!(MediaPlaylistTag, ExtXTargetDuration); -impl_from!(MediaPlaylistTag, ExtXMediaSequence); -impl_from!(MediaPlaylistTag, ExtXDiscontinuitySequence); -impl_from!(MediaPlaylistTag, ExtXEndList); -impl_from!(MediaPlaylistTag, ExtXPlaylistType); -impl_from!(MediaPlaylistTag, ExtXIFramesOnly); -impl_from!(MediaPlaylistTag, ExtXIndependentSegments); -impl_from!(MediaPlaylistTag, ExtXStart); - -/// [4.3.2. Media Segment Tags] -/// -/// [4.3.2. Media Segment Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.2 -#[allow(missing_docs)] -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MediaSegmentTag { - ExtInf(ExtInf), - ExtXByteRange(ExtXByteRange), - ExtXDateRange(ExtXDateRange), - ExtXDiscontinuity(ExtXDiscontinuity), - ExtXKey(ExtXKey), - ExtXMap(ExtXMap), - ExtXProgramDateTime(ExtXProgramDateTime), -} -impl_from!(MediaSegmentTag, ExtInf); -impl_from!(MediaSegmentTag, ExtXByteRange); -impl_from!(MediaSegmentTag, ExtXDateRange); -impl_from!(MediaSegmentTag, ExtXDiscontinuity); -impl_from!(MediaSegmentTag, ExtXKey); -impl_from!(MediaSegmentTag, ExtXMap); -impl_from!(MediaSegmentTag, ExtXProgramDateTime); diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 438eace..2e1ea20 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -4,39 +4,49 @@ use std::str::FromStr; use derive_builder::Builder; use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, RequiredVersion}; +use crate::types::{ + EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion, + RequiredVersion, +}; 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]. +/// +/// [ExtXSessionKey]: crate::tags::ExtXSessionKey +/// [ExtXKey]: crate::tags::ExtXKey pub struct DecryptionKey { + /// The [EncryptionMethod]. pub(crate) method: EncryptionMethod, #[builder(setter(into, strip_option), default)] + /// An `URI`, that specifies how to obtain the key. pub(crate) uri: Option, #[builder(setter(into, strip_option), default)] + /// The IV (Initialization Vector) attribute. pub(crate) iv: Option, #[builder(setter(into, strip_option), default)] - pub(crate) key_format: Option, - #[builder(setter(into, strip_option), default)] - pub(crate) key_format_versions: Option, + /// 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, } impl DecryptionKey { - /// Makes a new `DecryptionKey`. + /// Makes a new [DecryptionKey]. + /// /// # Example /// ``` - /// use hls_m3u8::types::{EncryptionMethod, DecryptionKey}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"https://www.example.com/\"" - /// ); /// ``` pub fn new(method: EncryptionMethod, uri: T) -> Self { Self { @@ -44,14 +54,16 @@ impl DecryptionKey { uri: Some(uri.to_string()), iv: None, key_format: None, - key_format_versions: None, + key_format_versions: KeyFormatVersions::new(), } } /// Returns the [EncryptionMethod]. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -67,15 +79,17 @@ impl DecryptionKey { self.method } - /// Returns a Builder to build a `DecryptionKey`. + /// Returns a Builder to build a [DecryptionKey]. pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } /// Sets the [EncryptionMethod]. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -93,12 +107,14 @@ impl DecryptionKey { self.method = value; } - /// Returns an `URI` that specifies how to obtain the key. + /// Returns an `URI`, that specifies how to obtain the key. /// /// This attribute is required, if the [EncryptionMethod] is not None. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -121,7 +137,8 @@ impl DecryptionKey { /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -142,9 +159,11 @@ impl DecryptionKey { /// Returns the IV (Initialization Vector) attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -171,9 +190,11 @@ impl DecryptionKey { /// Sets the `IV` attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, @@ -197,106 +218,110 @@ impl DecryptionKey { } /// Returns a string that specifies how the key is - /// represented in the resource identified by the URI. + /// represented in the resource identified by the `URI`. + /// + /// This attribute is optional. /// - //// This attribute is optional. /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{KeyFormat, EncryptionMethod}; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); /// - /// key.set_key_format("key_format_attribute"); + /// key.set_key_format(Some(KeyFormat::Identity)); /// /// assert_eq!( /// key.key_format(), - /// &Some("key_format_attribute".to_string()) + /// 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. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{KeyFormat, EncryptionMethod}; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); /// - /// key.set_key_format("key_format_attribute"); + /// key.set_key_format(Some(KeyFormat::Identity)); /// /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() + /// key.key_format(), + /// Some(KeyFormat::Identity) /// ); /// ``` - pub fn set_key_format(&mut self, value: T) { - self.key_format = Some(value.to_string()); + pub fn set_key_format>(&mut self, value: Option) { + self.key_format = value.map(|v| v.into()); } - /// Returns a string containing one or more positive - /// integers separated by the "/" character (for example, "1", "1/2", - /// or "1/2/5"). If more than one version of a particular `KEYFORMAT` - /// is defined, this attribute can be used to indicate which - /// version(s) this instance complies with. + /// Returns the [KeyFormatVersions] attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::{KeyFormatVersions, EncryptionMethod}; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); /// - /// key.set_key_format_versions("1/2/3/4/5"); + /// key.set_key_format_versions(vec![1, 2, 3, 4, 5]); /// /// assert_eq!( /// key.key_format_versions(), - /// &Some("1/2/3/4/5".to_string()) + /// &KeyFormatVersions::from(vec![1, 2, 3, 4, 5]) /// ); /// ``` - pub const fn key_format_versions(&self) -> &Option { + pub const fn key_format_versions(&self) -> &KeyFormatVersions { &self.key_format_versions } - /// Sets the `KEYFORMATVERSIONS` attribute. + /// Sets the [KeyFormatVersions] attribute. /// /// This attribute is optional. + /// /// # Example /// ``` - /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// # use hls_m3u8::types::DecryptionKey; + /// use hls_m3u8::types::EncryptionMethod; /// /// let mut key = DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/" /// ); /// - /// key.set_key_format_versions("1/2/3/4/5"); + /// key.set_key_format_versions(vec![1, 2, 3, 4, 5]); /// /// assert_eq!( /// key.to_string(), /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() /// ); /// ``` - pub fn set_key_format_versions(&mut self, value: T) { - self.key_format_versions = Some(value.to_string()); + pub fn set_key_format_versions>(&mut self, value: T) { + self.key_format_versions = value.into(); } } impl RequiredVersion for DecryptionKey { fn required_version(&self) -> ProtocolVersion { - if self.key_format.is_some() || self.key_format_versions.is_some() { + if self.key_format.is_some() || !self.key_format_versions.is_default() { ProtocolVersion::V5 } else if self.iv.is_some() { ProtocolVersion::V2 @@ -318,11 +343,11 @@ impl FromStr for DecryptionKey { for (key, value) in input.parse::()? { match key.as_str() { - "METHOD" => method = Some((value.parse())?), + "METHOD" => method = Some(value.parse()?), "URI" => uri = Some(unquote(value)), - "IV" => iv = Some((value.parse())?), - "KEYFORMAT" => key_format = Some(unquote(value)), - "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), + "IV" => iv = Some(value.parse()?), + "KEYFORMAT" => key_format = Some(value.parse()?), + "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse()?), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -340,7 +365,7 @@ impl FromStr for DecryptionKey { uri, iv, key_format, - key_format_versions, + key_format_versions: key_format_versions.unwrap_or(KeyFormatVersions::new()), }) } } @@ -361,8 +386,8 @@ impl fmt::Display for DecryptionKey { if let Some(value) = &self.key_format { write!(f, ",KEYFORMAT={}", quote(value))?; } - if let Some(value) = &self.key_format_versions { - write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; + if !self.key_format_versions.is_default() { + write!(f, ",KEYFORMATVERSIONS={}", &self.key_format_versions)?; } Ok(()) } @@ -381,13 +406,19 @@ mod test { .iv([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ]) - .key_format("ABC123") - .key_format_versions("1,2,3,4,5/12345") + .key_format(KeyFormat::Identity) + .key_format_versions(vec![1, 2, 3, 4, 5]) .build() .unwrap(); assert_eq!( key.to_string(), - "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT=\"ABC123\",KEYFORMATVERSIONS=\"1,2,3,4,5/12345\"".to_string() + "METHOD=AES-128,\ + URI=\"https://www.example.com/\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52,\ + KEYFORMAT=\"identity\",\ + KEYFORMATVERSIONS=\"1/2/3/4/5\"\ + " + .to_string() ) } @@ -413,7 +444,8 @@ mod test { #[test] fn test_parser() { assert_eq!( - r#"METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + "METHOD=AES-128,\ + URI=\"https://priv.example.com/key.php?r=52\"" .parse::() .unwrap(), DecryptionKey::new( @@ -443,11 +475,15 @@ mod test { key.set_iv([ 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, ]); - key.set_key_format("baz"); + key.set_key_format(Some(KeyFormat::Identity)); assert_eq!( - r#"METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# - .parse::().unwrap(), + "METHOD=AES-128,\ + URI=\"http://www.example.com\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52,\ + KEYFORMAT=\"identity\"" + .parse::() + .unwrap(), key ) } diff --git a/src/types/key_format.rs b/src/types/key_format.rs new file mode 100644 index 0000000..43ef6a7 --- /dev/null +++ b/src/types/key_format.rs @@ -0,0 +1,68 @@ +use std::fmt; +use std::str::FromStr; + +use crate::types::{ProtocolVersion, RequiredVersion}; +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 +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 + } +} + +impl FromStr for KeyFormat { + type Err = Error; + + fn from_str(input: &str) -> Result { + tag(&unquote(input), "identity")?; // currently only KeyFormat::Identity exists! + + Ok(Self::Identity) + } +} + +impl fmt::Display for KeyFormat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", quote("identity")) + } +} + +impl RequiredVersion for KeyFormat { + fn required_version(&self) -> ProtocolVersion { + ProtocolVersion::V5 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!(KeyFormat::Identity.to_string(), quote("identity")); + } + + #[test] + fn test_parser() { + assert_eq!(KeyFormat::Identity, quote("identity").parse().unwrap()); + + assert_eq!(KeyFormat::Identity, "identity".parse().unwrap()); + } + + #[test] + fn test_required_version() { + assert_eq!(KeyFormat::Identity.required_version(), ProtocolVersion::V5) + } + + #[test] + fn test_default() { + assert_eq!(KeyFormat::Identity, KeyFormat::default()); + } +} diff --git a/src/types/key_format_versions.rs b/src/types/key_format_versions.rs new file mode 100644 index 0000000..8448905 --- /dev/null +++ b/src/types/key_format_versions.rs @@ -0,0 +1,170 @@ +use std::fmt; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +use crate::types::{ProtocolVersion, RequiredVersion}; +use crate::utils::{quote, unquote}; +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]: 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]) + } +} + +impl KeyFormatVersions { + /// Makes a new [KeyFormatVersions]. + pub fn new() -> Self { + Self::default() + } + + /// Add a value to the [KeyFormatVersions]. + pub fn push(&mut self, value: usize) { + if self.is_default() { + self.0 = vec![value]; + } else { + self.0.push(value); + } + } + + /// 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() + } +} + +impl Deref for KeyFormatVersions { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for KeyFormatVersions { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl RequiredVersion for KeyFormatVersions { + fn required_version(&self) -> ProtocolVersion { + ProtocolVersion::V5 + } +} + +impl FromStr for KeyFormatVersions { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut result = unquote(input) + .split("/") + .filter_map(|v| v.parse().ok()) + .collect::>(); + + if result.is_empty() { + result.push(1); + } + + Ok(Self(result)) + } +} + +impl fmt::Display for KeyFormatVersions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.is_default() { + return write!(f, "{}", quote("1")); + } + + write!( + f, + "{}", + quote( + // vec![1, 2, 3] -> "1/2/3" + self.0 + .iter() + .map(|v| v.to_string()) + .collect::>() + .join("/") + ) + ) + } +} + +impl>> From for KeyFormatVersions { + fn from(value: T) -> Self { + Self(value.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!( + KeyFormatVersions::from(vec![1, 2, 3, 4, 5]).to_string(), + quote("1/2/3/4/5") + ); + + assert_eq!(KeyFormatVersions::from(vec![]).to_string(), quote("1")); + + assert_eq!(KeyFormatVersions::new().to_string(), quote("1")); + } + + #[test] + fn test_parser() { + assert_eq!( + KeyFormatVersions::from(vec![1, 2, 3, 4, 5]), + quote("1/2/3/4/5").parse().unwrap() + ); + + assert_eq!(KeyFormatVersions::from(vec![1]), "1".parse().unwrap()); + + assert_eq!(KeyFormatVersions::from(vec![1, 2]), "1/2".parse().unwrap()); + } + + #[test] + fn test_required_version() { + assert_eq!( + KeyFormatVersions::new().required_version(), + ProtocolVersion::V5 + ) + } + + #[test] + fn test_is_default() { + assert!(KeyFormatVersions::new().is_default()); + assert!(KeyFormatVersions::from(vec![]).is_default()); + assert!(!KeyFormatVersions::from(vec![1, 2, 3]).is_default()); + } + + #[test] + fn test_push() { + let mut key_format_versions = KeyFormatVersions::from(vec![]); + + key_format_versions.push(2); + assert_eq!(KeyFormatVersions::from(vec![2]), key_format_versions); + } + + #[test] + fn test_deref() { + assert!(!KeyFormatVersions::new().is_empty()); + } + + #[test] + fn test_deref_mut() { + let mut key_format_versions = KeyFormatVersions::from(vec![1, 2, 3]); + key_format_versions.pop(); + assert_eq!(key_format_versions, KeyFormatVersions::from(vec![1, 2])); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index f3c751f..70c891f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -8,6 +8,8 @@ mod encryption_method; mod hdcp_level; mod in_stream_id; mod initialization_vector; +mod key_format; +mod key_format_versions; mod media_type; mod protocol_version; mod signed_decimal_floating_point; @@ -22,6 +24,8 @@ pub use encryption_method::*; pub use hdcp_level::*; pub use in_stream_id::*; pub use initialization_vector::*; +pub use key_format::*; +pub use key_format_versions::*; pub use media_type::*; pub use protocol_version::*; pub(crate) use signed_decimal_floating_point::*; diff --git a/src/utils.rs b/src/utils.rs index eeda232..bb3662c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -46,7 +46,7 @@ pub(crate) fn tag(input: &str, tag: T) -> crate::Result<&str> where T: AsRef, { - if !input.starts_with(tag.as_ref()) { + if !input.trim().starts_with(tag.as_ref()) { return Err(Error::missing_tag(tag.as_ref(), input)); } let result = input.split_at(tag.as_ref().len()).1;