From 7025114e363dd24f535d1f821ff48bf27196d942 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 23 Mar 2020 13:34:26 +0100 Subject: [PATCH] rewrite keys (ExtXKey, ExtXSessionKey) and Encrypted trait --- src/media_playlist.rs | 6 +- src/media_segment.rs | 15 +- src/tags/master_playlist/session_key.rs | 186 ++----- src/tags/media_segment/key.rs | 703 +++++++++--------------- src/tags/media_segment/map.rs | 46 +- src/traits.rs | 136 ++--- src/types/decryption_key.rs | 317 +++++++++++ src/types/encryption_method.rs | 10 - tests/rfc8216.rs | 18 +- 9 files changed, 708 insertions(+), 729 deletions(-) create mode 100644 src/types/decryption_key.rs diff --git a/src/media_playlist.rs b/src/media_playlist.rs index fdd167d..bd58c9f 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -10,11 +10,11 @@ use crate::line::{Line, Lines, Tag}; use crate::media_segment::MediaSegment; use crate::tags::{ ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, - ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, + ExtXKey, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, }; -use crate::types::ProtocolVersion; +use crate::types::{EncryptionMethod, ProtocolVersion}; use crate::utils::tag; -use crate::{Encrypted, Error, RequiredVersion}; +use crate::{Error, RequiredVersion}; /// Media playlist. #[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)] diff --git a/src/media_segment.rs b/src/media_segment.rs index e23de4e..4f21171 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -6,8 +6,8 @@ use shorthand::ShortHand; use crate::tags::{ ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, }; -use crate::types::ProtocolVersion; -use crate::{Encrypted, RequiredVersion}; +use crate::types::{DecryptionKey, ProtocolVersion}; +use crate::{Decryptable, RequiredVersion}; /// Media segment. #[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)] @@ -104,10 +104,11 @@ impl RequiredVersion for MediaSegment { } } -impl Encrypted for MediaSegment { - fn keys(&self) -> &Vec { &self.keys } - - fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } +impl Decryptable for MediaSegment { + fn keys(&self) -> Vec<&DecryptionKey> { + // + self.keys.iter().filter_map(ExtXKey::as_ref).collect() + } } #[cfg(test)] @@ -120,7 +121,6 @@ mod tests { fn test_display() { assert_eq!( MediaSegment::builder() - //.keys(vec![ExtXKey::empty()]) .map(ExtXMap::new("https://www.example.com/")) .byte_range(ExtXByteRange::from(5..25)) //.date_range() // TODO! @@ -131,7 +131,6 @@ mod tests { .unwrap() .to_string(), concat!( - //"#EXT-X-KEY:METHOD=NONE\n", "#EXT-X-MAP:URI=\"https://www.example.com/\"\n", "#EXT-X-BYTERANGE:20@5\n", "#EXT-X-DISCONTINUITY\n", diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index a3db6a9..d3ecb8d 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -2,10 +2,10 @@ use core::convert::TryFrom; use std::fmt; use std::str::FromStr; -use derive_more::{Deref, DerefMut}; +use derive_more::{AsMut, AsRef, From}; use crate::tags::ExtXKey; -use crate::types::{EncryptionMethod, ProtocolVersion}; +use crate::types::{DecryptionKey, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -20,72 +20,52 @@ use crate::{Error, RequiredVersion}; /// /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist -/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 -#[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXSessionKey(ExtXKey); +#[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[from(forward)] +pub struct ExtXSessionKey(pub DecryptionKey); impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; /// 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`]. - /// /// # Example /// /// ``` - /// # use hls_m3u8::tags::{ExtXSessionKey, ExtXKey}; - /// use hls_m3u8::types::EncryptionMethod; + /// # use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// - /// ExtXSessionKey::new(ExtXKey::new( + /// let session_key = ExtXSessionKey::new(DecryptionKey::new( /// EncryptionMethod::Aes128, /// "https://www.example.com/", /// )); /// ``` #[must_use] - pub fn new(inner: ExtXKey) -> Self { - if inner.method() == EncryptionMethod::None { - panic!("the encryption method should never be `None`"); - } - - Self(inner) - } + #[inline] + pub const fn new(inner: DecryptionKey) -> Self { Self(inner) } } impl TryFrom for ExtXSessionKey { type Error = Error; fn try_from(value: ExtXKey) -> Result { - if value.method() == EncryptionMethod::None { - return Err(Error::custom( - "the encryption method should never be `None`", - )); + if let ExtXKey(Some(inner)) = value { + Ok(Self(inner)) + } else { + Err(Error::custom("missing decryption key")) } - - Ok(Self(value)) } } /// This tag requires the same [`ProtocolVersion`] that is returned by -/// `ExtXKey::required_version`. +/// `DecryptionKey::required_version`. impl RequiredVersion for ExtXSessionKey { fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // TODO: this is not the most elegant solution - write!( - f, - "{}{}", - Self::PREFIX, - self.0.to_string().replacen(ExtXKey::PREFIX, "", 1) - ) + write!(f, "{}{}", Self::PREFIX, self.0.to_string()) } } @@ -93,7 +73,7 @@ impl FromStr for ExtXSessionKey { type Err = Error; fn from_str(input: &str) -> Result { - Ok(Self(ExtXKey::parse_from_str(tag(input, Self::PREFIX)?)?)) + Ok(Self(DecryptionKey::from_str(tag(input, Self::PREFIX)?)?)) } } @@ -103,68 +83,55 @@ mod test { use crate::types::{EncryptionMethod, KeyFormat}; use pretty_assertions::assert_eq; - #[test] - fn test_display() { - let mut key = ExtXSessionKey::new(ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin", - )); + macro_rules! generate_tests { + ( $( { $struct:expr, $str:expr } ),+ $(,)* ) => { + #[test] + fn test_display() { + $( + assert_eq!($struct.to_string(), $str.to_string()); + )+ + } - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); + #[test] + fn test_parser() { + $( + assert_eq!($struct, $str.parse().unwrap()); + )+ + } + } + } - assert_eq!( - key.to_string(), + generate_tests! { + { + ExtXSessionKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]) + .build() + .unwrap(), + ), concat!( "#EXT-X-SESSION-KEY:", "METHOD=AES-128,", "URI=\"https://www.example.com/hls-key/key.bin\",", "IV=0x10ef8f758ca555115584bb5b3c687f52" ) - .to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - concat!( - "#EXT-X-SESSION-KEY:", - "METHOD=AES-128,", - "URI=\"https://priv.example.com/key.php?r=52\"" - ) - .parse::() - .unwrap(), - ExtXSessionKey::new(ExtXKey::new( - EncryptionMethod::Aes128, - "https://priv.example.com/key.php?r=52" - )) - ); - - let mut key = ExtXSessionKey::new(ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin", - )); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); - - assert_eq!( - concat!( - "#EXT-X-SESSION-KEY:", - "METHOD=AES-128,", - "URI=\"https://www.example.com/hls-key/key.bin\",", - "IV=0X10ef8f758ca555115584bb5b3c687f52" - ) - .parse::() - .unwrap(), - key - ); - - key.set_key_format(Some(KeyFormat::Identity)); - - assert_eq!( + }, + { + ExtXSessionKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]) + .format(KeyFormat::Identity) + .build() + .unwrap(), + ), concat!( "#EXT-X-SESSION-KEY:", "METHOD=AES-128,", @@ -172,16 +139,13 @@ mod test { "IV=0x10ef8f758ca555115584bb5b3c687f52,", "KEYFORMAT=\"identity\"", ) - .parse::() - .unwrap(), - key - ) + } } #[test] fn test_required_version() { assert_eq!( - ExtXSessionKey::new(ExtXKey::new( + ExtXSessionKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://www.example.com/" )) @@ -189,34 +153,4 @@ mod test { ProtocolVersion::V1 ); } - - // ExtXSessionKey::new should panic, if the provided - // EncryptionMethod is None! - #[test] - #[should_panic = "the encryption method should never be `None`"] - fn test_new_panic() { let _ = ExtXSessionKey::new(ExtXKey::new(EncryptionMethod::None, "")); } - - #[test] - fn test_deref() { - let key = ExtXSessionKey::new(ExtXKey::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(ExtXKey::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/media_segment/key.rs b/src/tags/media_segment/key.rs index d1c5590..a3f2989 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,254 +1,166 @@ use std::fmt; use std::str::FromStr; -use derive_builder::Builder; -use shorthand::ShortHand; - -use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; -use crate::utils::{parse_iv_from_str, quote, tag, unquote}; +use crate::types::{DecryptionKey, ProtocolVersion}; +use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.3.2.4. EXT-X-KEY] +/// Specifies how to decrypt encrypted data from the server. /// -/// [`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). -/// -/// # 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.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[builder(setter(into), build_fn(validate = "Self::validate"))] -#[shorthand(enable(must_use, into))] -pub struct ExtXKey { - /// HLS supports multiple [`EncryptionMethod`]s for a [`MediaSegment`]. - /// - /// For example `AES-128`. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_method(EncryptionMethod::SampleAes); - /// - /// assert_eq!(key.method(), EncryptionMethod::SampleAes); - /// ``` - /// - /// # Note - /// - /// This attribute is required. - /// - /// [`MediaSegment`]: crate::MediaSegment - #[shorthand(enable(copy))] - pub(crate) method: EncryptionMethod, - /// An `URI` that specifies how to obtain the key. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_uri(Some("http://www.google.com/")); - /// - /// assert_eq!(key.uri(), Some(&"http://www.google.com/".to_string())); - /// ``` - /// - /// # Note - /// - /// This attribute is required, if the [`EncryptionMethod`] is not `None`. - #[builder(setter(into, strip_option), default)] - pub(crate) uri: Option, - /// An IV (initialization vector) is used to prevent repetitions between - /// segments of encrypted data. - /// - /// - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// # 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(), - /// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 - #[shorthand(enable(copy), disable(option_as_ref))] - pub(crate) iv: Option<[u8; 0x10]>, - /// The [`KeyFormat`] specifies how the key is - /// represented in the resource identified by the `URI`. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format(Some(KeyFormat::Identity)); - /// - /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - #[shorthand(enable(copy))] - pub(crate) key_format: Option, - /// The [`KeyFormatVersions`] attribute. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions}; - /// - /// let mut key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5])); - /// - /// assert_eq!( - /// key.key_format_versions(), - /// Some(&KeyFormatVersions::from(vec![1, 2, 3, 4, 5])) - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is optional. - #[builder(setter(into, strip_option), default)] - pub(crate) key_format_versions: Option, -} - -impl ExtXKeyBuilder { - fn validate(&self) -> Result<(), String> { - if self.method != Some(EncryptionMethod::None) && self.uri.is_none() { - return Err(Error::missing_value("URL").to_string()); - } - - Ok(()) - } -} +/// An unencrypted segment should be marked with [`ExtXKey::empty`]. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub struct ExtXKey(pub Option); impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; - /// Makes a new [`ExtXKey`] tag. + /// Constructs an [`ExtXKey`] tag. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod, KeyFormat}; /// - /// let key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// let key = ExtXKey::new( + /// DecryptionKey::builder() + /// .method(EncryptionMethod::Aes128) + /// .uri("https://www.example.com/") + /// .iv([ + /// 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + /// ]) + /// .format(KeyFormat::Identity) + /// .versions(vec![1, 2, 3, 4, 5]) + /// .build()?, + /// ); + /// # Ok::<(), String>(()) + /// ``` + #[must_use] + #[inline] + pub const fn new(inner: DecryptionKey) -> Self { Self(Some(inner)) } + + /// Constructs an empty [`ExtXKey`], which signals that a segment is + /// unencrypted. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// assert_eq!(ExtXKey::empty(), ExtXKey(None)); + /// ``` + #[must_use] + #[inline] + pub const fn empty() -> Self { Self(None) } + + /// Returns `true` if the key is not empty. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let k = ExtXKey::new(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url", + /// )); + /// assert_eq!(k.is_some(), true); + /// + /// let k = ExtXKey::empty(); + /// assert_eq!(k.is_some(), false); + /// ``` + #[must_use] + #[inline] + pub fn is_some(&self) -> bool { self.0.is_some() } + + /// Returns `true` if the key is empty. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let k = ExtXKey::new(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url", + /// )); + /// assert_eq!(k.is_none(), false); + /// + /// let k = ExtXKey::empty(); + /// assert_eq!(k.is_none(), true); + /// ``` + #[must_use] + #[inline] + pub fn is_none(&self) -> bool { self.0.is_none() } + + /// Returns the underlying [`DecryptionKey`], if there is one. + /// + /// # Panics + /// + /// Panics if there is no underlying decryption key. + /// + /// # Examples + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let k = ExtXKey::new(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url", + /// )); /// /// assert_eq!( - /// key.to_string(), - /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\"" + /// k.unwrap(), + /// DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.url") + /// ); + /// ``` + /// + /// ```{.should_panic} + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::DecryptionKey; + /// + /// let decryption_key: DecryptionKey = ExtXKey::empty().unwrap(); // panics + /// ``` + #[must_use] + pub fn unwrap(self) -> DecryptionKey { + match self.0 { + Some(v) => v, + None => panic!("called `ExtXKey::unwrap()` on an empty key"), + } + } + + /// Returns a reference to the underlying [`DecryptionKey`]. + #[must_use] + #[inline] + pub fn as_ref(&self) -> Option<&DecryptionKey> { self.0.as_ref() } + + /// Converts an [`ExtXKey`] into an `Option`. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// assert_eq!(ExtXKey::empty().into_option(), None); + /// + /// assert_eq!( + /// ExtXKey::new(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url" + /// )) + /// .into_option(), + /// Some(DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.url" + /// )) /// ); /// ``` #[must_use] - pub fn new>(method: EncryptionMethod, uri: T) -> Self { - Self { - method, - uri: Some(uri.into()), - iv: None, - key_format: None, - key_format_versions: None, - } - } - - /// Returns a Builder to build an [`ExtXKey`]. - /// - /// # Example - /// - /// ``` - /// use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; - /// - /// ExtXKey::builder() - /// .method(EncryptionMethod::Aes128) - /// .uri("https://www.example.com/") - /// .iv([ - /// 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - /// ]) - /// .key_format(KeyFormat::Identity) - /// .key_format_versions(vec![1, 2, 3, 4, 5]) - /// .build()?; - /// # Ok::<(), Box>(()) - /// ``` - #[must_use] - pub fn builder() -> ExtXKeyBuilder { ExtXKeyBuilder::default() } - - /// 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"); - /// ``` - #[must_use] - pub const fn empty() -> Self { - Self { - method: EncryptionMethod::None, - uri: None, - iv: None, - key_format: None, - key_format_versions: None, - } - } - - /// Returns whether the [`EncryptionMethod`] is - /// [`None`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = ExtXKey::empty(); - /// - /// assert_eq!(key.method() == EncryptionMethod::None, key.is_empty()); - /// ``` - /// - /// [`None`]: EncryptionMethod::None - #[must_use] - pub fn is_empty(&self) -> bool { self.method() == EncryptionMethod::None } + #[inline] + pub fn into_option(self) -> Option { self.0 } } /// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or @@ -258,60 +170,9 @@ impl ExtXKey { /// Otherwise [`ProtocolVersion::V1`] is required. impl RequiredVersion for ExtXKey { fn required_version(&self) -> ProtocolVersion { - if self.key_format.is_some() || self.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } - } -} - -impl ExtXKey { - /// Parses a String without verifying the starting tag - pub(crate) fn parse_from_str(input: &str) -> crate::Result { - let mut method = None; - let mut uri = None; - let mut iv = None; - let mut key_format = None; - let mut key_format_versions = None; - - for (key, value) in AttributePairs::new(input) { - match key { - "METHOD" => method = Some(value.parse().map_err(Error::strum)?), - "URI" => { - let unquoted_uri = unquote(value); - - if unquoted_uri.trim().is_empty() { - uri = None; - } else { - uri = Some(unquoted_uri); - } - } - "IV" => iv = Some(parse_iv_from_str(value)?), - "KEYFORMAT" => key_format = Some(value.parse()?), - "KEYFORMATVERSIONS" => key_format_versions = Some(value.parse().unwrap()), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized - // AttributeName. - } - } - } - - let method = method.ok_or_else(|| Error::missing_value("METHOD"))?; - if method != EncryptionMethod::None && uri.is_none() { - return Err(Error::missing_value("URI")); - } - - Ok(Self { - method, - uri, - iv, - key_format, - key_format_versions, - }) + self.0 + .as_ref() + .map_or(ProtocolVersion::V1, |i| i.required_version()) } } @@ -320,42 +181,36 @@ impl FromStr for ExtXKey { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - Self::parse_from_str(input) + + if input.trim() == "METHOD=NONE" { + Ok(Self(None)) + } else { + Ok(DecryptionKey::from_str(input)?.into()) + } } } +impl From> for ExtXKey { + fn from(value: Option) -> Self { Self(value) } +} + +impl From for ExtXKey { + fn from(value: DecryptionKey) -> Self { Self(Some(value)) } +} + +impl From for ExtXKey { + fn from(value: crate::tags::ExtXSessionKey) -> Self { Self(Some(value.0)) } +} + impl fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - write!(f, "METHOD={}", self.method)?; - - if self.method == EncryptionMethod::None { - return Ok(()); + if let Some(value) = &self.0 { + write!(f, "{}", value) + } else { + write!(f, "METHOD=NONE") } - - if let Some(uri) = &self.uri { - write!(f, ",URI={}", quote(uri))?; - } - - if let Some(value) = &self.iv { - let mut result = [0; 0x10 * 2]; - hex::encode_to_slice(value, &mut result).unwrap(); - - write!(f, ",IV=0x{}", ::core::str::from_utf8(&result).unwrap())?; - } - - if let Some(value) = &self.key_format { - write!(f, ",KEYFORMAT={}", quote(value))?; - } - - if let Some(key_format_versions) = &self.key_format_versions { - if !key_format_versions.is_default() { - write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?; - } - } - - Ok(()) } } @@ -365,168 +220,130 @@ mod test { use crate::types::{EncryptionMethod, KeyFormat}; use pretty_assertions::assert_eq; - #[test] - fn test_builder() { - assert_eq!( - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,]) - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3, 4, 5]) - .build() - .unwrap() - .to_string(), + macro_rules! generate_tests { + ( $( { $struct:expr, $str:expr } ),+ $(,)* ) => { + #[test] + fn test_display() { + $( + assert_eq!($struct.to_string(), $str.to_string()); + )+ + } + + #[test] + fn test_parser() { + $( + assert_eq!($struct, $str.parse().unwrap()); + )+ + + assert_eq!( + ExtXKey::new( + DecryptionKey::new( + EncryptionMethod::Aes128, + "http://www.example.com" + ) + ), + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"http://www.example.com\",", + "UNKNOWNTAG=abcd" + ).parse().unwrap(), + ); + assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::().is_err()); + assert!("garbage".parse::().is_err()); + } + } + } + + generate_tests! { + { + ExtXKey::empty(), + "#EXT-X-KEY:METHOD=NONE" + }, + { + ExtXKey::new(DecryptionKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52" + )), concat!( "#EXT-X-KEY:", "METHOD=AES-128,", - "URI=\"https://www.example.com/\",", - "IV=0x10ef8f758ca555115584bb5b3c687f52,", - "KEYFORMAT=\"identity\",", - "KEYFORMATVERSIONS=\"1/2/3/4/5\"", + "URI=\"https://priv.example.com/key.php?r=52\"" ) - .to_string() - ); - - assert!(ExtXKey::builder().build().is_err()); - assert!(ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .build() - .is_err()); - } - - #[test] - fn test_display() { - assert_eq!( - ExtXKey::empty().to_string(), - "#EXT-X-KEY:METHOD=NONE".to_string() - ); - - let mut key = ExtXKey::empty(); - // it is expected, that all attributes will be ignored for an empty key! - key.set_key_format(Some(KeyFormat::Identity)); - 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(Some(vec![1, 2, 3])); - - assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); - - assert_eq!( - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/hls-key/key.bin") - .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) - .build() - .unwrap() - .to_string(), + }, + { + ExtXKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .build() + .unwrap() + ), concat!( "#EXT-X-KEY:", "METHOD=AES-128,", "URI=\"https://www.example.com/hls-key/key.bin\",", "IV=0x10ef8f758ca555115584bb5b3c687f52" ) - .to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - concat!( - "#EXT-X-KEY:", - "METHOD=AES-128,", - "URI=\"https://priv.example.com/key.php?r=52\"" - ) - .parse::() - .unwrap(), + }, + { ExtXKey::new( - EncryptionMethod::Aes128, - "https://priv.example.com/key.php?r=52" - ) - ); - - assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3]) + .build() + .unwrap() + ), concat!( "#EXT-X-KEY:", "METHOD=AES-128,", "URI=\"https://www.example.com/hls-key/key.bin\",", - "IV=0X10ef8f758ca555115584bb5b3c687f52" - ) - .parse::() - .unwrap(), - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/hls-key/key.bin") - .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) - .build() - .unwrap() - ); - - assert_eq!( - concat!( - "#EXT-X-KEY:", - "METHOD=AES-128,", - "URI=\"https://www.example.com/hls-key/key.bin\",", - "IV=0X10ef8f758ca555115584bb5b3c687f52,", + "IV=0x10ef8f758ca555115584bb5b3c687f52,", "KEYFORMAT=\"identity\",", "KEYFORMATVERSIONS=\"1/2/3\"" ) - .parse::() - .unwrap(), - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/hls-key/key.bin") - .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3]) - .build() - .unwrap() - ); - - assert_eq!( - concat!( - "#EXT-X-KEY:", - "METHOD=AES-128,", - "URI=\"http://www.example.com\",", - "UNKNOWNTAG=abcd" - ) - .parse::() - .unwrap(), - ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com") - ); - assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::().is_err()); - assert!("garbage".parse::().is_err()); + }, } #[test] fn test_required_version() { assert_eq!( - ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/").required_version(), + ExtXKey::new(DecryptionKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/" + )) + .required_version(), ProtocolVersion::V1 ); assert_eq!( - ExtXKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3]) - .build() - .unwrap() - .required_version(), + ExtXKey::new( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3]) + .build() + .unwrap() + ) + .required_version(), ProtocolVersion::V5 ); assert_eq!( - ExtXKey::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(), + ExtXKey::new( + 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/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 1161289..abc7803 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -5,9 +5,9 @@ use shorthand::ShortHand; use crate::attribute::AttributePairs; use crate::tags::ExtXKey; -use crate::types::{ByteRange, ProtocolVersion}; +use crate::types::{ByteRange, DecryptionKey, ProtocolVersion}; use crate::utils::{quote, tag, unquote}; -use crate::{Encrypted, Error, RequiredVersion}; +use crate::{Decryptable, Error, RequiredVersion}; /// The [`ExtXMap`] tag specifies how to obtain the [Media Initialization /// Section], required to parse the applicable [`MediaSegment`]s. @@ -36,38 +36,12 @@ use crate::{Encrypted, Error, RequiredVersion}; pub struct ExtXMap { /// The `URI` that identifies a resource, that contains the media /// initialization section. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMap; - /// let mut map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); - /// # assert_eq!( - /// # map.uri(), - /// # &"https://prod.mediaspace.com/init.bin".to_string() - /// # ); - /// map.set_uri("https://www.google.com/init.bin"); - /// - /// assert_eq!(map.uri(), &"https://www.google.com/init.bin".to_string()); - /// ``` uri: String, /// The range of the media initialization section. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMap; - /// use hls_m3u8::types::ByteRange; - /// - /// let mut map = ExtXMap::with_range("https://prod.mediaspace.com/init.bin", ..9); - /// - /// map.set_range(Some(2..5)); - /// assert_eq!(map.range(), Some(ByteRange::from(2..5))); - /// ``` #[shorthand(enable(copy))] range: Option, #[shorthand(enable(skip))] - keys: Vec, + pub(crate) keys: Vec, } impl ExtXMap { @@ -108,10 +82,11 @@ impl ExtXMap { } } -impl Encrypted for ExtXMap { - fn keys(&self) -> &Vec { &self.keys } - - fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } +impl Decryptable for ExtXMap { + fn keys(&self) -> Vec<&DecryptionKey> { + // + self.keys.iter().filter_map(ExtXKey::as_ref).collect() + } } /// Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that contains the @@ -224,8 +199,7 @@ mod test { } #[test] - fn test_encrypted() { - assert_eq!(ExtXMap::new("foo").keys(), &vec![]); - assert_eq!(ExtXMap::new("foo").keys_mut(), &mut vec![]); + fn test_decryptable() { + assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey>::new()); } } diff --git a/src/traits.rs b/src/traits.rs index a92a9af..4175588 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,106 +1,54 @@ -use crate::tags::ExtXKey; -use crate::types::{EncryptionMethod, ProtocolVersion}; +use std::collections::{BTreeMap, HashMap}; -/// A trait, that is implemented on all tags, that could be encrypted. -/// -/// # Example -/// -/// ``` -/// use hls_m3u8::tags::ExtXKey; -/// use hls_m3u8::types::EncryptionMethod; -/// use hls_m3u8::Encrypted; -/// -/// struct ExampleTag { -/// keys: Vec, -/// } -/// -/// // Implementing the trait is very simple: -/// // Simply expose the internal buffer, that contains all the keys. -/// impl Encrypted for ExampleTag { -/// fn keys(&self) -> &Vec { &self.keys } -/// -/// fn keys_mut(&mut self) -> &mut Vec { &mut self.keys } -/// } -/// -/// let mut example_tag = ExampleTag { keys: vec![] }; -/// -/// // adding new keys: -/// example_tag.set_keys(vec![ExtXKey::empty()]); -/// example_tag.push_key(ExtXKey::new( -/// EncryptionMethod::Aes128, -/// "http://www.example.com/data.bin", -/// )); -/// -/// // getting the keys: -/// assert_eq!( -/// example_tag.keys(), -/// &vec![ -/// ExtXKey::empty(), -/// ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com/data.bin",) -/// ] -/// ); -/// -/// assert_eq!( -/// example_tag.keys_mut(), -/// &mut vec![ -/// ExtXKey::empty(), -/// ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com/data.bin",) -/// ] -/// ); -/// -/// assert!(example_tag.is_encrypted()); -/// assert!(!example_tag.is_not_encrypted()); -/// ``` -pub trait Encrypted { - /// Returns a shared reference to all keys, that can be used to decrypt this - /// tag. - fn keys(&self) -> &Vec; +use crate::types::{DecryptionKey, ProtocolVersion}; - /// Returns an exclusive reference to all keys, that can be used to decrypt - /// this tag. - fn keys_mut(&mut self) -> &mut Vec; +mod private { + pub trait Sealed {} + impl Sealed for crate::MediaSegment {} + impl Sealed for crate::tags::ExtXMap {} +} - /// Sets all keys, that can be used to decrypt this tag. - fn set_keys(&mut self, value: Vec) -> &mut Self { - let keys = self.keys_mut(); - *keys = value; - self - } - - /// Add a single key to the list of keys, that can be used to decrypt this - /// tag. - fn push_key(&mut self, value: ExtXKey) -> &mut Self { - self.keys_mut().push(value); - self - } - - /// Returns `true`, if the tag is encrypted. +/// Signals that a type or some of the asssociated data might need to be +/// decrypted. +/// +/// # Note +/// +/// You are not supposed to implement this trait, therefore it is "sealed". +pub trait Decryptable: private::Sealed { + /// Returns all keys, associated with the type. /// - /// # Note + /// # Example /// - /// This will return `true`, if any of the keys satisfies - /// - /// ```text - /// key.method() != EncryptionMethod::None /// ``` - fn is_encrypted(&self) -> bool { - if self.keys().is_empty() { - return false; - } + /// use hls_m3u8::tags::ExtXMap; + /// use hls_m3u8::types::{ByteRange, EncryptionMethod}; + /// use hls_m3u8::Decryptable; + /// + /// let map = ExtXMap::with_range("https://www.example.url/", ByteRange::from(2..11)); + /// + /// for key in map.keys() { + /// if key.method == EncryptionMethod::Aes128 { + /// // fetch content with the uri and decrypt the result + /// break; + /// } + /// } + /// ``` + #[must_use] + fn keys(&self) -> Vec<&DecryptionKey>; - self.keys() - .iter() - .any(|k| k.method() != EncryptionMethod::None) + /// Most of the time only a single key is provided, so instead of iterating + /// through all keys, one might as well just get the first key. + #[must_use] + fn first_key(&self) -> Option<&DecryptionKey> { + ::keys(self).first().copied() } - /// Returns `false`, if the tag is not encrypted. - /// - /// # Note - /// - /// This is the inverse of [`is_encrypted`]. - /// - /// [`is_encrypted`]: #method.is_encrypted - fn is_not_encrypted(&self) -> bool { !self.is_encrypted() } + /// Returns the number of keys. + #[must_use] + fn len(&self) -> usize { ::keys(self).len() } + + #[must_use] + fn is_empty(&self) -> bool { ::len(self) == 0 } } /// # Example diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs new file mode 100644 index 0000000..68396a3 --- /dev/null +++ b/src/types/decryption_key.rs @@ -0,0 +1,317 @@ +use std::fmt; +use std::str::FromStr; + +use derive_builder::Builder; +use shorthand::ShortHand; + +use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; +use crate::utils::{parse_iv_from_str, quote, unquote}; +use crate::{Error, RequiredVersion}; + +#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[builder(setter(into), build_fn(validate = "Self::validate"))] +#[shorthand(enable(must_use, into))] +pub struct DecryptionKey { + /// HLS supports multiple [`EncryptionMethod`]s for a [`MediaSegment`]. + /// + /// For example `AES-128`. + /// + /// ## Note + /// + /// This field is required. + /// + /// [`MediaSegment`]: crate::MediaSegment + //#[shorthand(enable(skip))] + #[shorthand(enable(copy))] + pub method: EncryptionMethod, + /// An `URI` that specifies how to obtain the key. + /// + /// ## Note + /// + /// This attribute is required, if the [`EncryptionMethod`] is not `None`. + #[builder(setter(into, strip_option), default)] + pub(crate) uri: String, + /// An IV (initialization vector) is used to prevent repetitions between + /// segments of encrypted data. + /// + /// ## Note + /// + /// This field is optional. + #[builder(setter(into, strip_option), default)] + // TODO: workaround for https://github.com/Luro02/shorthand/issues/20 + #[shorthand(enable(copy), disable(option_as_ref))] + pub(crate) iv: Option<[u8; 0x10]>, + /// The [`KeyFormat`] specifies how the key is + /// represented in the resource identified by the `URI`. + /// + /// ## Note + /// + /// This field is optional. + #[builder(setter(into, strip_option), default)] + #[shorthand(enable(copy))] + pub format: Option, + /// The [`KeyFormatVersions`] attribute. + /// + /// ## Note + /// + /// This field is optional. + #[builder(setter(into, strip_option), default)] + pub versions: Option, +} + +impl DecryptionKey { + #[must_use] + #[inline] + pub fn new>(method: EncryptionMethod, uri: I) -> Self { + Self { + method, + uri: uri.into(), + iv: None, + format: None, + versions: None, + } + } + + #[must_use] + #[inline] + pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } +} + +/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or +/// [`KeyFormatVersions`] is specified and [`ProtocolVersion::V2`] if an iv is +/// specified. +/// +/// Otherwise [`ProtocolVersion::V1`] is required. +impl RequiredVersion for DecryptionKey { + fn required_version(&self) -> ProtocolVersion { + if self.format.is_some() || self.versions.is_some() { + ProtocolVersion::V5 + } else if self.iv.is_some() { + ProtocolVersion::V2 + } else { + ProtocolVersion::V1 + } + } +} + +impl FromStr for DecryptionKey { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut method = None; + let mut uri = None; + let mut iv = None; + let mut format = None; + let mut versions = None; + + for (key, value) in AttributePairs::new(input) { + match key { + "METHOD" => method = Some(value.parse().map_err(Error::strum)?), + "URI" => { + let unquoted_uri = unquote(value); + + if !unquoted_uri.trim().is_empty() { + uri = Some(unquoted_uri); + } + } + "IV" => iv = Some(parse_iv_from_str(value)?), + "KEYFORMAT" => format = Some(value.parse()?), + "KEYFORMATVERSIONS" => versions = Some(value.parse()?), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized + // AttributeName. + } + } + } + + let method = method.ok_or_else(|| Error::missing_value("METHOD"))?; + let uri = uri.ok_or_else(|| Error::missing_value("URI"))?; + + Ok(Self { + method, + uri, + iv, + format, + versions, + }) + } +} + +impl fmt::Display for DecryptionKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?; + + if let Some(value) = &self.iv { + let mut result = [0; 0x10 * 2]; + ::hex::encode_to_slice(value, &mut result).unwrap(); + + write!(f, ",IV=0x{}", ::core::str::from_utf8(&result).unwrap())?; + } + + if let Some(value) = &self.format { + write!(f, ",KEYFORMAT={}", quote(value))?; + } + + if let Some(value) = &self.versions { + if !value.is_default() { + write!(f, ",KEYFORMATVERSIONS={}", value)?; + } + } + + Ok(()) + } +} + +impl DecryptionKeyBuilder { + fn validate(&self) -> Result<(), String> { + // a decryption key must contain a uri and a method + if self.method.is_none() { + return Err(Error::missing_field("DecryptionKey", "method").to_string()); + } else if self.uri.is_none() { + return Err(Error::missing_field("DecryptionKey", "uri").to_string()); + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::{EncryptionMethod, KeyFormat}; + use pretty_assertions::assert_eq; + + macro_rules! generate_tests { + ( $( { $struct:expr, $str:expr } ),+ $(,)* ) => { + #[test] + fn test_display() { + $( + assert_eq!($struct.to_string(), $str.to_string()); + )+ + } + + #[test] + fn test_parser() { + $( + assert_eq!($struct, $str.parse().unwrap()); + )+ + + assert_eq!( + DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"), + concat!( + "METHOD=AES-128,", + "URI=\"http://www.example.com\",", + "UNKNOWNTAG=abcd" + ).parse().unwrap(), + ); + assert!("METHOD=AES-128,URI=".parse::().is_err()); + assert!("garbage".parse::().is_err()); + } + } + } + + #[test] + fn test_builder() { + let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + key.set_iv(Some([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ])); + key.format = Some(KeyFormat::Identity); + key.versions = Some(vec![1, 2, 3, 4, 5].into()); + + assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3, 4, 5]) + .build() + .unwrap(), + key + ); + + assert!(DecryptionKey::builder().build().is_err()); + assert!(DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .build() + .is_err()); + } + + generate_tests! { + { + DecryptionKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52" + ), + concat!( + "METHOD=AES-128,", + "URI=\"https://priv.example.com/key.php?r=52\"" + ) + }, + { + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .build() + .unwrap(), + concat!( + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52" + ) + }, + { + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/hls-key/key.bin") + .iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82]) + .format(KeyFormat::Identity) + .versions(vec![1, 2, 3]) + .build() + .unwrap(), + concat!( + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52,", + "KEYFORMAT=\"identity\",", + "KEYFORMATVERSIONS=\"1/2/3\"" + ) + }, + } + + #[test] + fn test_required_version() { + assert_eq!( + DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") + .required_version(), + ProtocolVersion::V1 + ); + + assert_eq!( + DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com/") + .format(KeyFormat::Identity) + .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 3f8cfa0..cedb5d1 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -6,10 +6,6 @@ use strum::{Display, EnumString}; #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)] #[strum(serialize_all = "SCREAMING-KEBAB-CASE")] pub enum EncryptionMethod { - /// The [`MediaSegment`]s are not encrypted. - /// - /// [`MediaSegment`]: crate::MediaSegment - None, /// The [`MediaSegment`]s are completely encrypted using the Advanced /// Encryption Standard ([AES-128]) with a 128-bit key, Cipher Block /// Chaining (CBC), and [Public-Key Cryptography Standards #7 (PKCS7)] @@ -65,7 +61,6 @@ mod tests { EncryptionMethod::SampleAes.to_string(), "SAMPLE-AES".to_string() ); - assert_eq!(EncryptionMethod::None.to_string(), "NONE".to_string()); } #[test] @@ -80,11 +75,6 @@ mod tests { "SAMPLE-AES".parse::().unwrap() ); - assert_eq!( - EncryptionMethod::None, - "NONE".parse::().unwrap() - ); - assert!("unknown".parse::().is_err()); } } diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index cae2c2c..9d16727 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -4,7 +4,7 @@ use std::time::Duration; use hls_m3u8::tags::{ ExtInf, ExtXEndList, ExtXKey, ExtXMedia, ExtXMediaSequence, ExtXTargetDuration, VariantStream, }; -use hls_m3u8::types::{EncryptionMethod, MediaType, StreamData}; +use hls_m3u8::types::{DecryptionKey, EncryptionMethod, MediaType, StreamData}; use hls_m3u8::{MasterPlaylist, MediaPlaylist, MediaSegment}; use pretty_assertions::assert_eq; @@ -102,10 +102,10 @@ generate_tests! { MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(2.833))) .keys(vec![ - ExtXKey::new( + ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=52" - ) + )) ]) .uri("http://media.example.com/fileSequence52-A.ts") .build() @@ -113,10 +113,10 @@ generate_tests! { MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(15.0))) .keys(vec![ - ExtXKey::new( + ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=52" - ) + )) ]) .uri("http://media.example.com/fileSequence52-B.ts") .build() @@ -124,10 +124,10 @@ generate_tests! { MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(13.333))) .keys(vec![ - ExtXKey::new( + ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=52" - ) + )) ]) .uri("http://media.example.com/fileSequence52-C.ts") .build() @@ -135,10 +135,10 @@ generate_tests! { MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(15.0))) .keys(vec![ - ExtXKey::new( + ExtXKey::new(DecryptionKey::new( EncryptionMethod::Aes128, "https://priv.example.com/key.php?r=53" - ) + )) ]) .uri("http://media.example.com/fileSequence53-A.ts") .build()