diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 9f0e1d8..ce93168 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,9 +1,11 @@ +use core::convert::TryFrom; use std::fmt; use std::str::FromStr; use derive_more::{Deref, DerefMut}; -use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; +use crate::tags::ExtXKey; +use crate::types::{EncryptionMethod, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; @@ -18,7 +20,7 @@ use crate::{Error, RequiredVersion}; /// [`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(DecryptionKey); +pub struct ExtXSessionKey(ExtXKey); impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; @@ -35,17 +37,34 @@ impl ExtXSessionKey { /// # Example /// /// ``` - /// # use hls_m3u8::tags::ExtXSessionKey; + /// # use hls_m3u8::tags::{ExtXSessionKey, ExtXKey}; /// use hls_m3u8::types::EncryptionMethod; /// - /// let session_key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + /// ExtXSessionKey::new(ExtXKey::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"); + pub fn new(inner: ExtXKey) -> Self { + if inner.method() == EncryptionMethod::None { + panic!("the encryption method should never be `None`"); } - Self(DecryptionKey::new(method, uri)) + 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`", + )); + } + + Ok(Self(value)) } } @@ -57,12 +76,13 @@ impl RequiredVersion for ExtXSessionKey { impl fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.0.method == EncryptionMethod::None { - // TODO: this is bad practice, this function should never fail! - return Err(fmt::Error); - } - - write!(f, "{}{}", Self::PREFIX, self.0) + // TODO: this is not the most elegant solution + write!( + f, + "{}{}", + Self::PREFIX, + self.0.to_string().replacen(ExtXKey::PREFIX, "", 1) + ) } } @@ -70,8 +90,7 @@ impl FromStr for ExtXSessionKey { type Err = Error; fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?; - Ok(Self(input.parse()?)) + Ok(Self(ExtXKey::parse_from_str(tag(input, Self::PREFIX)?)?)) } } @@ -83,62 +102,75 @@ mod test { #[test] fn test_display() { - let mut key = ExtXSessionKey::new( + 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!( key.to_string(), - "#EXT-X-SESSION-KEY:METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52" - .to_string() + 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!( - r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# - .parse::() - .unwrap(), - ExtXSessionKey::new( + 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( + 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!( - "#EXT-X-SESSION-KEY:METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0X10ef8f758ca555115584bb5b3c687f52" - .parse::() - .unwrap(), + 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!( - "#EXT-X-SESSION-KEY:\ - METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52,\ - KEYFORMAT=\"identity\"" - .parse::() - .unwrap(), + concat!( + "#EXT-X-SESSION-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/hls-key/key.bin\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52,", + "KEYFORMAT=\"identity\"", + ) + .parse::() + .unwrap(), key ) } @@ -146,8 +178,11 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") - .required_version(), + ExtXSessionKey::new(ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/" + )) + .required_version(), ProtocolVersion::V1 ); } @@ -155,18 +190,15 @@ mod test { // ExtXSessionKey::new should panic, if the provided // EncryptionMethod is None! #[test] - #[should_panic] - fn test_new_panic() { ExtXSessionKey::new(EncryptionMethod::None, ""); } - - #[test] - #[should_panic] - fn test_display_err() { - ExtXSessionKey(DecryptionKey::new(EncryptionMethod::None, "")).to_string(); - } + #[should_panic = "the encryption method should never be `None`"] + fn test_new_panic() { ExtXSessionKey::new(ExtXKey::new(EncryptionMethod::None, "")); } #[test] fn test_deref() { - let key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + 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())); @@ -174,7 +206,10 @@ mod test { #[test] fn test_deref_mut() { - let mut key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); + let mut key = ExtXSessionKey::new(ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/", + )); key.set_method(EncryptionMethod::None); assert_eq!(key.method(), EncryptionMethod::None); diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index b10266c..fab983e 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -64,7 +64,7 @@ use crate::Error; /// [`ExtXProgramDateTime`]: crate::tags::ExtXProgramDateTime /// [`ExtXPlaylistType`]: crate::tags::ExtXPlaylistType /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly -#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] pub enum VariantStream { /// The [`VariantStream::ExtXIFrame`] variant identifies a [`MediaPlaylist`] /// file containing the I-frames of a multimedia presentation. diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index d44dec2..87d12fc 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,10 +1,12 @@ use std::fmt; use std::str::FromStr; -use derive_more::{Deref, DerefMut}; +use derive_builder::Builder; +use shorthand::ShortHand; -use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; -use crate::utils::tag; +use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, KeyFormat, KeyFormatVersions, ProtocolVersion}; +use crate::utils::{parse_iv_from_str, quote, tag, unquote}; use crate::{Error, RequiredVersion}; /// # [4.3.2.4. EXT-X-KEY] @@ -24,8 +26,136 @@ use crate::{Error, RequiredVersion}; /// [`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(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ExtXKey(DecryptionKey); +#[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. + #[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; 16]>, + /// 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::custom("missing URL").to_string()); + } + Ok(()) + } +} impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; @@ -46,9 +176,36 @@ impl ExtXKey { /// ); /// ``` pub fn new>(method: EncryptionMethod, uri: T) -> Self { - Self(DecryptionKey::new(method, uri)) + 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>(()) + /// ``` + pub fn builder() -> ExtXKeyBuilder { ExtXKeyBuilder::default() } + /// Makes a new [`ExtXKey`] tag without a decryption key. /// /// # Example @@ -60,13 +217,13 @@ impl ExtXKey { /// assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE"); /// ``` pub const fn empty() -> Self { - Self(DecryptionKey { + Self { method: EncryptionMethod::None, uri: None, iv: None, key_format: None, key_format_versions: None, - }) + } } /// Returns whether the [`EncryptionMethod`] is @@ -84,13 +241,71 @@ impl ExtXKey { /// ``` /// /// [`None`]: EncryptionMethod::None - pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None } + pub fn is_empty(&self) -> bool { self.method() == EncryptionMethod::None } } -/// This tag requires the same [`ProtocolVersion`] that is returned by -/// `DecryptionKey::required_version`. +/// 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 ExtXKey { - fn required_version(&self) -> ProtocolVersion { self.0.required_version() } + 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, + }) + } } impl FromStr for ExtXKey { @@ -98,14 +313,40 @@ impl FromStr for ExtXKey { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - Ok(Self(input.parse()?)) + Self::parse_from_str(input) } } impl fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // - write!(f, "{}{}", Self::PREFIX, self.0) + write!(f, "{}", Self::PREFIX)?; + + write!(f, "METHOD={}", self.method)?; + + if self.method == EncryptionMethod::None { + return Ok(()); + } + + if let Some(uri) = &self.uri { + write!(f, ",URI={}", quote(uri))?; + } + + if let Some(value) = &self.iv { + // TODO: use hex::encode_to_slice + write!(f, ",IV=0x{}", hex::encode(&value))?; + } + + 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(()) } } @@ -115,6 +356,36 @@ 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(), + concat!( + "#EXT-X-KEY:", + "METHOD=AES-128,", + "URI=\"https://www.example.com/\",", + "IV=0x10ef8f758ca555115584bb5b3c687f52,", + "KEYFORMAT=\"identity\",", + "KEYFORMATVERSIONS=\"1/2/3/4/5\"", + ) + .to_string() + ); + + assert!(ExtXKey::builder().build().is_err()); + assert!(ExtXKey::builder() + .method(EncryptionMethod::Aes128) + .build() + .is_err()); + } + #[test] fn test_display() { assert_eq!( @@ -132,28 +403,122 @@ mod test { 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(), + 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!( - "#EXT-X-KEY:\ - METHOD=AES-128,\ - URI=\"https://priv.example.com/key.php?r=52\"" - .parse::() - .unwrap(), + 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" ) ); - let mut key = ExtXKey::new( - EncryptionMethod::Aes128, - "https://www.example.com/hls-key/key.bin", + assert_eq!( + 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,", + "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(), + 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(), + 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(), + ProtocolVersion::V2 ); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs deleted file mode 100644 index 275899a..0000000 --- a/src/types/decryption_key.rs +++ /dev/null @@ -1,434 +0,0 @@ -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}; - -/// A [`DecryptionKey`] contains data, that is shared between [`ExtXSessionKey`] -/// and [`ExtXKey`]. -/// -/// [`ExtXSessionKey`]: crate::tags::ExtXSessionKey -/// [`ExtXKey`]: crate::tags::ExtXKey -#[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 encryption methods for a segment. - /// - /// For example `AES-128`. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_method(EncryptionMethod::SampleAes); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() - /// ); - /// ``` - /// - /// # Note - /// - /// This attribute is required. - #[shorthand(enable(copy))] - pub(crate) method: EncryptionMethod, - /// An `URI` that specifies how to obtain the key. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_uri(Some("http://www.google.com/")); - /// - /// assert_eq!( - /// key.to_string(), - /// "METHOD=AES-128,URI=\"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::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let mut key = DecryptionKey::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; 16]>, - /// The [`KeyFormat`] specifies how the key is - /// represented in the resource identified by the `URI`. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormat}; - /// - /// let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// - /// key.set_key_format(Some(KeyFormat::Identity)); - /// - /// assert_eq!(key.key_format(), Some(KeyFormat::Identity)); - /// ``` - /// - /// # 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::types::DecryptionKey; - /// use hls_m3u8::types::{EncryptionMethod, KeyFormatVersions}; - /// - /// let mut key = DecryptionKey::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 DecryptionKeyBuilder { - fn validate(&self) -> Result<(), String> { - if self.method != Some(EncryptionMethod::None) && self.uri.is_none() { - return Err(Error::custom("Missing URL").to_string()); - } - Ok(()) - } -} - -impl DecryptionKey { - /// Makes a new [`DecryptionKey`]. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::DecryptionKey; - /// use hls_m3u8::types::EncryptionMethod; - /// - /// let key = DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/"); - /// ``` - #[doc(hidden)] - 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 a [`DecryptionKey`]. - 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.key_format.is_some() || self.key_format_versions.is_some() { - ProtocolVersion::V5 - } else if self.iv.is_some() { - ProtocolVersion::V2 - } else { - ProtocolVersion::V1 - } - } -} - -#[doc(hidden)] -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 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, - }) - } -} - -impl fmt::Display for DecryptionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "METHOD={}", self.method)?; - - if self.method == EncryptionMethod::None { - return Ok(()); - } - - if let Some(uri) = &self.uri { - write!(f, ",URI={}", quote(uri))?; - } - - if let Some(value) = &self.iv { - // TODO: use hex::encode_to_slice - write!(f, ",IV=0x{}", hex::encode(&value))?; - } - - 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(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::types::EncryptionMethod; - use pretty_assertions::assert_eq; - - #[test] - fn test_builder() { - let key = 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, - ]) - .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=\"identity\",\ - KEYFORMATVERSIONS=\"1/2/3/4/5\"\ - " - .to_string() - ); - - assert!(DecryptionKey::builder().build().is_err()); - assert!(DecryptionKey::builder() - .method(EncryptionMethod::Aes128) - .build() - .is_err()); - } - - #[test] - fn test_display() { - let mut key = DecryptionKey::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!( - key.to_string(), - "METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52" - .to_string() - ); - } - - #[test] - fn test_parser() { - assert_eq!( - "METHOD=AES-128,\ - URI=\"https://priv.example.com/key.php?r=52\"" - .parse::() - .unwrap(), - DecryptionKey::new( - EncryptionMethod::Aes128, - "https://priv.example.com/key.php?r=52" - ) - ); - - let mut key = DecryptionKey::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!( - "METHOD=AES-128,\ - URI=\"https://www.example.com/hls-key/key.bin\",\ - IV=0X10ef8f758ca555115584bb5b3c687f52" - .parse::() - .unwrap(), - key - ); - - let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"); - key.set_iv(Some([ - 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, - ])); - key.set_key_format(Some(KeyFormat::Identity)); - - assert_eq!( - "METHOD=AES-128,\ - URI=\"http://www.example.com\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52,\ - KEYFORMAT=\"identity\"" - .parse::() - .unwrap(), - key - ); - - key.set_key_format_versions(Some(vec![1, 2, 3])); - assert_eq!( - "METHOD=AES-128,\ - URI=\"http://www.example.com\",\ - IV=0x10ef8f758ca555115584bb5b3c687f52,\ - KEYFORMAT=\"identity\",\ - KEYFORMATVERSIONS=\"1/2/3\"" - .parse::() - .unwrap(), - key - ); - - assert_eq!( - "METHOD=AES-128,\ - URI=\"http://www.example.com\",\ - UNKNOWNTAG=abcd" - .parse::() - .unwrap(), - DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com") - ); - assert!("METHOD=AES-128,URI=".parse::().is_err()); - assert!("garbage".parse::().is_err()); - } - - #[test] - 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/") - .key_format(KeyFormat::Identity) - .key_format_versions(vec![1, 2, 3]) - .build() - .unwrap() - .required_version(), - ProtocolVersion::V5 - ); - - assert_eq!( - DecryptionKey::builder() - .method(EncryptionMethod::Aes128) - .uri("https://www.example.com/") - .iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7]) - .build() - .unwrap() - .required_version(), - ProtocolVersion::V2 - ); - } -} diff --git a/src/types/mod.rs b/src/types/mod.rs index 2f00d48..5bb0b04 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,7 +2,6 @@ mod byte_range; mod channels; mod closed_captions; -mod decryption_key; mod encryption_method; mod hdcp_level; mod in_stream_id; @@ -20,7 +19,6 @@ mod ufloat; pub use byte_range::*; pub use channels::*; pub use closed_captions::*; -pub use decryption_key::*; pub use encryption_method::*; pub use hdcp_level::*; pub use in_stream_id::*;