From 1d614d580aac6674fa086d49ba5ed4a984e79c1e Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Sun, 15 Sep 2019 16:45:43 +0200 Subject: [PATCH] updated ExtXKey + ExtXSessionKey #9 --- src/attribute.rs | 10 +- src/master_playlist.rs | 14 - src/tags/master_playlist/session_data.rs | 14 +- src/tags/master_playlist/session_key.rs | 433 ++++++++++++++++-- src/tags/media_segment/key.rs | 536 ++++++++++++++++++++--- src/types/decryption_key.rs | 87 ---- src/types/encryption_method.rs | 57 ++- src/types/initialization_vector.rs | 14 +- src/types/mod.rs | 6 +- src/types/session_data.rs | 11 - 10 files changed, 947 insertions(+), 235 deletions(-) delete mode 100644 src/types/session_data.rs diff --git a/src/attribute.rs b/src/attribute.rs index 338fed5..f2645e0 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -51,8 +51,8 @@ impl FromStr for AttributePairs { fn from_str(input: &str) -> Result { let mut result = AttributePairs::new(); - for line in split(input) { - let pair = line.trim().split("=").collect::>(); + for line in split(input, ',') { + let pair = split(line.trim(), '='); if pair.len() < 2 { return Err(Error::invalid_input()); @@ -63,11 +63,13 @@ impl FromStr for AttributePairs { result.insert(key.to_string(), value.to_string()); } + + dbg!(&result); Ok(result) } } -fn split(value: &str) -> Vec { +fn split(value: &str, terminator: char) -> Vec { let mut result = vec![]; let mut inside_quotes = false; @@ -83,7 +85,7 @@ fn split(value: &str) -> Vec { } temp_string.push(c); } - ',' => { + k if (k == terminator) => { if !inside_quotes { result.push(temp_string); temp_string = String::new(); diff --git a/src/master_playlist.rs b/src/master_playlist.rs index 4d1d2a5..ec11a86 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -109,8 +109,6 @@ impl MasterPlaylistBuilder { .map_err(|e| e.to_string())?; self.validate_session_data_tags() .map_err(|e| e.to_string())?; - self.validate_session_key_tags() - .map_err(|e| e.to_string())?; Ok(()) } @@ -232,18 +230,6 @@ impl MasterPlaylistBuilder { Ok(()) } - fn validate_session_key_tags(&self) -> crate::Result<()> { - let mut set = HashSet::new(); - if let Some(value) = &self.session_key_tags { - for t in value { - if !set.insert(t.key()) { - return Err(Error::custom(format!("Conflict: {}", t))); - } - } - } - Ok(()) - } - fn check_media_group(&self, media_type: MediaType, group_id: T) -> bool { if let Some(value) = &self.media_tags { value diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 183318a..0c7d1aa 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -4,10 +4,22 @@ use std::str::FromStr; use getset::{Getters, MutGetters, Setters}; use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, SessionData}; +use crate::types::ProtocolVersion; use crate::utils::{quote, tag, unquote}; use crate::Error; +/// Session data. +/// +/// See: [4.3.4.4. EXT-X-SESSION-DATA] +/// +/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SessionData { + Value(String), + Uri(String), +} + /// [4.3.4.4. EXT-X-SESSION-DATA] /// /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index e2d29ba..ac19110 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,71 +1,444 @@ use std::fmt; use std::str::FromStr; -use crate::types::{DecryptionKey, ProtocolVersion}; -use crate::utils::tag; +use url::Url; + +use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; /// [4.3.4.5. EXT-X-SESSION-KEY] /// /// [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 { - key: DecryptionKey, + method: EncryptionMethod, + uri: Option, + iv: Option, + key_format: Option, + key_format_versions: Option, } impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; - /// Makes a new `ExtXSessionKey` tag. - pub const fn new(key: DecryptionKey) -> Self { - ExtXSessionKey { key } + /// Makes a new [ExtXSessionKey] tag. + /// # Panic + /// This method will panic, if the [EncryptionMethod] is None. + pub fn new(method: EncryptionMethod, uri: Url) -> Self { + if method == EncryptionMethod::None { + panic!("The EncryptionMethod is not allowed to be None"); + } + + Self { + method, + uri: Some(uri), + iv: None, + key_format: None, + key_format_versions: None, + } } - /// Returns a decryption key for the playlist. - pub const fn key(&self) -> &DecryptionKey { - &self.key + /// Returns the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.method(), + /// EncryptionMethod::Aes128 + /// ); + /// ``` + pub const fn method(&self) -> EncryptionMethod { + self.method + } + + /// Sets the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_method(EncryptionMethod::SampleAes); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() + /// ); + /// ``` + pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self { + self.method = value; + self + } + + /// Returns an `URI` that specifies how to obtain the key. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.uri(), + /// &Some("https://www.example.com".parse().unwrap()) + /// ); + /// ``` + pub const fn uri(&self) -> &Option { + &self.uri + } + + /// Sets the `URI` attribute. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_uri("http://www.google.com".parse().unwrap()); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() + /// ); + /// ``` + pub fn set_uri(&mut self, value: Url) -> &mut Self { + self.uri = Some(value); + self + } + + /// Returns the IV (Initialization Vector) attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 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]) + /// ); + /// ``` + pub fn iv(&self) -> Option<[u8; 16]> { + if let Some(iv) = &self.iv { + Some(iv.to_slice()) + } else { + None + } + } + + /// Sets the `IV` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 + /// ]); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() + /// ); + /// ``` + pub fn set_iv(&mut self, value: T) -> &mut Self + where + T: Into<[u8; 16]>, + { + self.iv = Some(InitializationVector(value.into())); + self + } + + /// Returns a string that specifies how the key is + /// represented in the resource identified by the URI. + /// + //// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.key_format(), + /// &Some("key_format_attribute".to_string()) + /// ); + /// ``` + pub const fn key_format(&self) -> &Option { + &self.key_format + } + + /// Sets the `KEYFORMAT` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() + /// ); + /// ``` + pub fn set_key_format(&mut self, value: T) -> &mut Self { + self.key_format = Some(value.to_string()); + self + } + + /// 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. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.key_format_versions(), + /// &Some("1/2/3/4/5".to_string()) + /// ); + /// ``` + pub const fn key_format_versions(&self) -> &Option { + &self.key_format_versions + } + + /// Sets the `KEYFORMATVERSIONS` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-SESSION-KEY: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) -> &mut Self { + self.key_format_versions = Some(value.to_string()); + self } /// Returns the protocol compatibility version that this tag requires. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXSessionKey; + /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion}; + /// + /// let mut key = ExtXSessionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.requires_version(), + /// ProtocolVersion::V1 + /// ); + /// ``` pub fn requires_version(&self) -> ProtocolVersion { - self.key.requires_version() + 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 fmt::Display for ExtXSessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.key) + write!(f, "{}METHOD={}", Self::PREFIX, self.method)?; + if let Some(uri) = &self.uri { + write!(f, ",URI={}", quote(uri))?; + } + if let Some(value) = &self.iv { + write!(f, ",IV={}", value)?; + } + 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))?; + } + Ok(()) } } impl FromStr for ExtXSessionKey { - type Err = crate::Error; + type Err = Error; fn from_str(input: &str) -> Result { - let key = tag(input, Self::PREFIX)?.parse()?; - Ok(Self::new(key)) + let input = tag(input, Self::PREFIX)?; + 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 input.parse::()? { + match key.as_str() { + "METHOD" => method = Some((value.parse())?), + "URI" => uri = Some(unquote(value).parse()?), + "IV" => iv = Some((value.parse())?), + "KEYFORMAT" => key_format = Some(unquote(value)), + "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + + let method = method.ok_or(Error::missing_value("EXT-X-METHOD"))?; + if method == EncryptionMethod::None { + return Err(Error::custom( + "EXT-X-SESSION-KEY: Encryption Method must not be NONE", + )); + } + Ok(ExtXSessionKey { + method, + uri, + iv, + key_format, + key_format_versions, + }) } } #[cfg(test)] mod test { use super::*; - use crate::types::{EncryptionMethod, InitializationVector}; + use crate::types::EncryptionMethod; #[test] - fn ext_x_session_key() { - let tag = ExtXSessionKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: "foo".to_string(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: None, - key_format_versions: None, - }); - let text = - r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V2); + fn test_display() { + let mut key = ExtXSessionKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 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() + ); + } + + #[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( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52".parse().unwrap() + ) + ); + + let mut key = ExtXSessionKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 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(), + key + ); + + key.set_key_format("baz"); + + 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(), + key + ) } } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index cb7844e..bfa7df9 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,52 +1,399 @@ use std::fmt; use std::str::FromStr; +use url::Url; + use crate::attribute::AttributePairs; -use crate::types::{DecryptionKey, ProtocolVersion}; -use crate::utils::tag; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; +use crate::utils::{quote, tag, unquote}; use crate::Error; /// [4.3.2.4. 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. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ExtXKey(Option); +pub struct ExtXKey { + method: EncryptionMethod, + uri: Option, + iv: Option, + key_format: Option, + key_format_versions: Option, +} impl ExtXKey { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; /// Makes a new `ExtXKey` tag. - pub const fn new(key: DecryptionKey) -> Self { - Self(Some(key)) + /// # Example + /// ``` + /// use url::Url; + /// + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\"" + /// ); + /// ``` + pub const fn new(method: EncryptionMethod, uri: Url) -> Self { + Self { + method, + uri: Some(uri), + iv: None, + key_format: None, + key_format_versions: None, + } } /// Makes a new `ExtXKey` tag without a decryption key. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; /// - /// This tag has the `METHDO=NONE` attribute. - pub const fn new_without_key() -> Self { - Self(None) + /// let key = ExtXKey::empty(); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=NONE" + /// ); + /// ``` + pub const fn empty() -> Self { + Self { + method: EncryptionMethod::None, + uri: None, + iv: None, + key_format: None, + key_format_versions: None, + } } - /// Returns the decryption key for the following media segments and media initialization sections. - pub fn key(&self) -> Option<&DecryptionKey> { - self.0.as_ref() + /// 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() + /// ); + /// ``` + pub fn is_empty(&self) -> bool { + if self.method == EncryptionMethod::None { + true + } else { + false + } + } + + /// Returns the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.method(), + /// EncryptionMethod::Aes128 + /// ); + /// ``` + pub const fn method(&self) -> EncryptionMethod { + self.method + } + + /// Sets the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_method(EncryptionMethod::SampleAes); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() + /// ); + /// ``` + pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self { + self.method = value; + self + } + + /// Returns an `URI` that specifies how to obtain the key. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.uri(), + /// &Some("https://www.example.com".parse().unwrap()) + /// ); + /// ``` + pub const fn uri(&self) -> &Option { + &self.uri + } + + /// Sets the `URI` attribute. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_uri("http://www.google.com".parse().unwrap()); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() + /// ); + /// ``` + pub fn set_uri(&mut self, value: Url) -> &mut Self { + self.uri = Some(value); + self + } + + /// Returns the IV (Initialization Vector) attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 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]) + /// ); + /// ``` + pub fn iv(&self) -> Option<[u8; 16]> { + if let Some(iv) = &self.iv { + Some(iv.to_slice()) + } else { + None + } + } + + /// Sets the `IV` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_iv([ + /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 + /// ]); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() + /// ); + /// ``` + pub fn set_iv(&mut self, value: T) -> &mut Self + where + T: Into<[u8; 16]>, + { + self.iv = Some(InitializationVector(value.into())); + self + } + + /// Returns a string that specifies how the key is + /// represented in the resource identified by the URI. + /// + //// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.key_format(), + /// &Some("key_format_attribute".to_string()) + /// ); + /// ``` + pub const fn key_format(&self) -> &Option { + &self.key_format + } + + /// Sets the `KEYFORMAT` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() + /// ); + /// ``` + pub fn set_key_format(&mut self, value: T) -> &mut Self { + self.key_format = Some(value.to_string()); + self + } + + /// 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. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.key_format_versions(), + /// &Some("1/2/3/4/5".to_string()) + /// ); + /// ``` + pub const fn key_format_versions(&self) -> &Option { + &self.key_format_versions + } + + /// Sets the `KEYFORMATVERSIONS` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::EncryptionMethod; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY: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) -> &mut Self { + self.key_format_versions = Some(value.to_string()); + self } /// Returns the protocol compatibility version that this tag requires. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; + /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion}; + /// + /// let mut key = ExtXKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.requires_version(), + /// ProtocolVersion::V1 + /// ); + /// ``` pub fn requires_version(&self) -> ProtocolVersion { - self.0 - .as_ref() - .map_or(ProtocolVersion::V1, |k| k.requires_version()) + 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 fmt::Display for ExtXKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", Self::PREFIX)?; - if let Some(value) = &self.0 { - write!(f, "{}", value)?; - } else { - write!(f, "METHOD=NONE")?; + write!(f, "{}METHOD={}", Self::PREFIX, 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 { + write!(f, ",IV={}", value)?; + } + 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))?; } Ok(()) } @@ -57,74 +404,117 @@ impl FromStr for ExtXKey { fn from_str(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; + let mut method = None; + let mut uri = None; + let mut iv = None; + let mut key_format = None; + let mut key_format_versions = None; - let pairs = input.parse::()?; - - if pairs.iter().any(|(k, v)| k == "METHOD" && v == "NONE") { - for (key, _) in pairs { - if key == "URI" || key == "IV" || key == "KEYFORMAT" || key == "KEYFORMATVERSIONS" { - return Err(Error::invalid_input()); + for (key, value) in input.parse::()? { + match key.as_str() { + "METHOD" => method = Some((value.parse())?), + "URI" => uri = Some(unquote(value).parse()?), + "IV" => iv = Some((value.parse())?), + "KEYFORMAT" => key_format = Some(unquote(value)), + "KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. } } - - Ok(Self(None)) - } else { - Ok(Self(Some(input.parse()?))) } + + let method = method.ok_or(Error::missing_value("EXT-X-KEY:METHOD"))?; + if method != EncryptionMethod::None && uri.is_none() { + return Err(Error::missing_value("EXT-X-KEY:URI")); + } + + Ok(ExtXKey { + method, + uri, + iv, + key_format, + key_format_versions, + }) } } #[cfg(test)] mod test { use super::*; - use crate::types::{EncryptionMethod, InitializationVector}; + use crate::types::EncryptionMethod; #[test] - fn ext_x_key() { - let tag = ExtXKey::new_without_key(); - let text = "#EXT-X-KEY:METHOD=NONE"; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_display() { + assert_eq!( + ExtXKey::empty().to_string(), + "#EXT-X-KEY:METHOD=NONE".to_string() + ); - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: "foo".to_string(), - iv: None, - key_format: None, - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + assert_eq!( + ExtXKey::empty().set_key_format("hi").to_string(), + "#EXT-X-KEY:METHOD=NONE".to_string() + ); - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: "foo".to_string(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: None, - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V2); + let mut key = ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: "foo".to_string(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: Some("baz".to_string()), - key_format_versions: None, - }); - let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V5); + assert_eq!( + key.to_string(), + "#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!( + r#"#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".parse().unwrap() + ) + ); + + let mut key = ExtXKey::new( + EncryptionMethod::Aes128, + "https://www.example.com/hls-key/key.bin".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); + + assert_eq!( + "#EXT-X-KEY:METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0X10ef8f758ca555115584bb5b3c687f52" + .parse::() + .unwrap(), + key + ); + + let mut key = ExtXKey::new( + EncryptionMethod::Aes128, + "http://www.example.com".parse().unwrap(), + ); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]) + .set_key_format("baz"); + + assert_eq!( + r#"#EXT-X-KEY:METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# + .parse::().unwrap(), + key + ) } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index 571a588..8b13789 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -1,88 +1 @@ -use std::fmt; -use std::str::{self, FromStr}; -use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; -use crate::utils::{quote, unquote}; -use crate::Error; - -/// Decryption key. -/// -/// See: [4.3.2.4. EXT-X-KEY] -/// -/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct DecryptionKey { - pub method: EncryptionMethod, - pub uri: String, - pub iv: Option, - pub key_format: Option, - pub key_format_versions: Option, -} - -impl DecryptionKey { - pub(crate) fn requires_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 fmt::Display for DecryptionKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "METHOD={}", self.method)?; - write!(f, ",URI={}", quote(&self.uri))?; - if let Some(value) = &self.iv { - write!(f, ",IV={}", value)?; - } - 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))?; - } - Ok(()) - } -} - -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 input.parse::()? { - match key.as_str() { - "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)), - _ => { - // [6.3.1. General Client Responsibilities] - // > ignore any attribute/value pair with an unrecognized AttributeName. - } - } - } - - let method = method.ok_or(Error::missing_value("EXT-X-METHOD"))?; - let uri = uri.ok_or(Error::missing_value("EXT-X-URI"))?; - - Ok(DecryptionKey { - method, - uri, - iv, - key_format, - key_format_versions, - }) - } -} diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 8754ba1..a5389f8 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -11,7 +11,40 @@ use crate::Error; #[allow(missing_docs)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum EncryptionMethod { + /// `None` means that [MediaSegment]s are not encrypted. + /// + /// [MediaSegment]: crate::MediaSegment + None, + /// `Aes128` signals that 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)] padding. + /// CBC is restarted on each segment boundary, using either the + /// Initialization Vector (IV) attribute value or the Media Sequence + /// Number as the IV. + /// + /// [MediaSegment]: crate::MediaSegment + /// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf + /// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652 Aes128, + /// `SampleAes` means that the [MediaSegment]s + /// contain media samples, such as audio or video, that are encrypted + /// using the Advanced Encryption Standard ([AES_128]). How these media + /// streams are encrypted and encapsulated in a segment depends on the + /// media encoding and the media format of the segment. fMP4 Media + /// Segments are encrypted using the 'cbcs' scheme of + /// [Common Encryption]. Encryption of other Media Segment + /// formats containing [H.264], [AAC], [AC-3], + /// and Enhanced [AC-3] media streams is described in the HTTP + /// Live Streaming (HLS) [SampleEncryption specification]. + /// + /// [MediaSegment]: crate::MediaSegment + /// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf + /// [Common Encryption]: https://tools.ietf.org/html/rfc8216#ref-COMMON_ENC + /// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264 + /// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496 + /// [AC-3]: https://tools.ietf.org/html/rfc8216#ref-AC_3 + /// [SampleEncryption specification]: https://tools.ietf.org/html/rfc8216#ref-SampleEnc SampleAes, } @@ -20,6 +53,7 @@ impl fmt::Display for EncryptionMethod { match &self { EncryptionMethod::Aes128 => "AES-128".fmt(f), EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), + EncryptionMethod::None => "NONE".fmt(f), } } } @@ -31,6 +65,7 @@ impl FromStr for EncryptionMethod { match input { "AES-128" => Ok(EncryptionMethod::Aes128), "SAMPLE-AES" => Ok(EncryptionMethod::SampleAes), + "NONE" => Ok(EncryptionMethod::None), _ => Err(Error::custom(format!( "Unknown encryption method: {:?}", input @@ -45,25 +80,29 @@ mod tests { #[test] fn test_display() { - let encryption_method = EncryptionMethod::Aes128; - assert_eq!(encryption_method.to_string(), "AES-128".to_string()); - - let encryption_method = EncryptionMethod::SampleAes; - assert_eq!(encryption_method.to_string(), "SAMPLE-AES".to_string()); + assert_eq!(EncryptionMethod::Aes128.to_string(), "AES-128".to_string()); + assert_eq!( + EncryptionMethod::SampleAes.to_string(), + "SAMPLE-AES".to_string() + ); + assert_eq!(EncryptionMethod::None.to_string(), "NONE".to_string()); } #[test] fn test_parse() { - let encryption_method = EncryptionMethod::Aes128; assert_eq!( - encryption_method, + EncryptionMethod::Aes128, "AES-128".parse::().unwrap() ); - let encryption_method = EncryptionMethod::SampleAes; assert_eq!( - encryption_method, + EncryptionMethod::SampleAes, "SAMPLE-AES".parse::().unwrap() ); + + assert_eq!( + EncryptionMethod::None, + "NONE".parse::().unwrap() + ); } } diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 9743f34..2b66bd2 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -10,7 +10,19 @@ use crate::Error; /// /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InitializationVector(pub [u8; 16]); +pub(crate) struct InitializationVector(pub [u8; 16]); + +impl InitializationVector { + pub const fn to_slice(&self) -> [u8; 16] { + self.0 + } +} + +impl From<[u8; 16]> for InitializationVector { + fn from(value: [u8; 16]) -> Self { + Self(value) + } +} impl Deref for InitializationVector { type Target = [u8]; diff --git a/src/types/mod.rs b/src/types/mod.rs index 5a78297..83390b7 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,7 +3,6 @@ mod byte_range; mod closed_captions; mod decimal_floating_point; mod decimal_resolution; -mod decryption_key; mod encryption_method; mod hdcp_level; mod hexadecimal_sequence; @@ -11,20 +10,17 @@ mod in_stream_id; mod initialization_vector; mod media_type; mod protocol_version; -mod session_data; mod signed_decimal_floating_point; pub use byte_range::*; pub use closed_captions::*; pub use decimal_floating_point::*; pub(crate) use decimal_resolution::*; -pub use decryption_key::*; pub use encryption_method::*; pub use hdcp_level::*; pub use hexadecimal_sequence::*; pub use in_stream_id::*; -pub use initialization_vector::*; +pub(crate) use initialization_vector::*; pub use media_type::*; pub use protocol_version::*; -pub use session_data::*; pub use signed_decimal_floating_point::*; diff --git a/src/types/session_data.rs b/src/types/session_data.rs deleted file mode 100644 index 4875b35..0000000 --- a/src/types/session_data.rs +++ /dev/null @@ -1,11 +0,0 @@ -/// Session data. -/// -/// See: [4.3.4.4. EXT-X-SESSION-DATA] -/// -/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum SessionData { - Value(String), - Uri(String), -}