mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-21 23:01:00 +00:00
rewrite keys (ExtXKey, ExtXSessionKey) and Encrypted trait
This commit is contained in:
parent
02d363daa1
commit
7025114e36
9 changed files with 708 additions and 729 deletions
|
@ -10,11 +10,11 @@ use crate::line::{Line, Lines, Tag};
|
||||||
use crate::media_segment::MediaSegment;
|
use crate::media_segment::MediaSegment;
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments,
|
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::utils::tag;
|
||||||
use crate::{Encrypted, Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// Media playlist.
|
/// Media playlist.
|
||||||
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)]
|
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)]
|
||||||
|
|
|
@ -6,8 +6,8 @@ use shorthand::ShortHand;
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
||||||
};
|
};
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
use crate::{Encrypted, RequiredVersion};
|
use crate::{Decryptable, RequiredVersion};
|
||||||
|
|
||||||
/// Media segment.
|
/// Media segment.
|
||||||
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)]
|
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)]
|
||||||
|
@ -104,10 +104,11 @@ impl RequiredVersion for MediaSegment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encrypted for MediaSegment {
|
impl Decryptable for MediaSegment {
|
||||||
fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
fn keys(&self) -> Vec<&DecryptionKey> {
|
||||||
|
//
|
||||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys }
|
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -120,7 +121,6 @@ mod tests {
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
//.keys(vec![ExtXKey::empty()])
|
|
||||||
.map(ExtXMap::new("https://www.example.com/"))
|
.map(ExtXMap::new("https://www.example.com/"))
|
||||||
.byte_range(ExtXByteRange::from(5..25))
|
.byte_range(ExtXByteRange::from(5..25))
|
||||||
//.date_range() // TODO!
|
//.date_range() // TODO!
|
||||||
|
@ -131,7 +131,6 @@ mod tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
//"#EXT-X-KEY:METHOD=NONE\n",
|
|
||||||
"#EXT-X-MAP:URI=\"https://www.example.com/\"\n",
|
"#EXT-X-MAP:URI=\"https://www.example.com/\"\n",
|
||||||
"#EXT-X-BYTERANGE:20@5\n",
|
"#EXT-X-BYTERANGE:20@5\n",
|
||||||
"#EXT-X-DISCONTINUITY\n",
|
"#EXT-X-DISCONTINUITY\n",
|
||||||
|
|
|
@ -2,10 +2,10 @@ use core::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{AsMut, AsRef, From};
|
||||||
|
|
||||||
use crate::tags::ExtXKey;
|
use crate::tags::ExtXKey;
|
||||||
use crate::types::{EncryptionMethod, ProtocolVersion};
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
use crate::{Error, RequiredVersion};
|
use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
|
@ -20,72 +20,52 @@ use crate::{Error, RequiredVersion};
|
||||||
///
|
///
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
#[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
#[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash)]
|
#[from(forward)]
|
||||||
pub struct ExtXSessionKey(ExtXKey);
|
pub struct ExtXSessionKey(pub DecryptionKey);
|
||||||
|
|
||||||
impl ExtXSessionKey {
|
impl ExtXSessionKey {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXSessionKey`] tag.
|
/// Makes a new [`ExtXSessionKey`] tag.
|
||||||
///
|
///
|
||||||
/// # Panic
|
|
||||||
///
|
|
||||||
/// An [`ExtXSessionKey`] should only be used,
|
|
||||||
/// if the segments of the stream are encrypted.
|
|
||||||
/// Therefore this function will panic,
|
|
||||||
/// if the `method` is [`EncryptionMethod::None`].
|
|
||||||
///
|
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::{ExtXSessionKey, ExtXKey};
|
/// # use hls_m3u8::tags::ExtXSessionKey;
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
///
|
///
|
||||||
/// ExtXSessionKey::new(ExtXKey::new(
|
/// let session_key = ExtXSessionKey::new(DecryptionKey::new(
|
||||||
/// EncryptionMethod::Aes128,
|
/// EncryptionMethod::Aes128,
|
||||||
/// "https://www.example.com/",
|
/// "https://www.example.com/",
|
||||||
/// ));
|
/// ));
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(inner: ExtXKey) -> Self {
|
#[inline]
|
||||||
if inner.method() == EncryptionMethod::None {
|
pub const fn new(inner: DecryptionKey) -> Self { Self(inner) }
|
||||||
panic!("the encryption method should never be `None`");
|
|
||||||
}
|
|
||||||
|
|
||||||
Self(inner)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<ExtXKey> for ExtXSessionKey {
|
impl TryFrom<ExtXKey> for ExtXSessionKey {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: ExtXKey) -> Result<Self, Self::Error> {
|
fn try_from(value: ExtXKey) -> Result<Self, Self::Error> {
|
||||||
if value.method() == EncryptionMethod::None {
|
if let ExtXKey(Some(inner)) = value {
|
||||||
return Err(Error::custom(
|
Ok(Self(inner))
|
||||||
"the encryption method should never be `None`",
|
} else {
|
||||||
));
|
Err(Error::custom("missing decryption key"))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(value))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires the same [`ProtocolVersion`] that is returned by
|
/// This tag requires the same [`ProtocolVersion`] that is returned by
|
||||||
/// `ExtXKey::required_version`.
|
/// `DecryptionKey::required_version`.
|
||||||
impl RequiredVersion for ExtXSessionKey {
|
impl RequiredVersion for ExtXSessionKey {
|
||||||
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXSessionKey {
|
impl fmt::Display for ExtXSessionKey {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
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())
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}{}",
|
|
||||||
Self::PREFIX,
|
|
||||||
self.0.to_string().replacen(ExtXKey::PREFIX, "", 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +73,7 @@ impl FromStr for ExtXSessionKey {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
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 crate::types::{EncryptionMethod, KeyFormat};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
macro_rules! generate_tests {
|
||||||
fn test_display() {
|
( $( { $struct:expr, $str:expr } ),+ $(,)* ) => {
|
||||||
let mut key = ExtXSessionKey::new(ExtXKey::new(
|
#[test]
|
||||||
EncryptionMethod::Aes128,
|
fn test_display() {
|
||||||
"https://www.example.com/hls-key/key.bin",
|
$(
|
||||||
));
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
|
||||||
key.set_iv(Some([
|
#[test]
|
||||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
fn test_parser() {
|
||||||
]));
|
$(
|
||||||
|
assert_eq!($struct, $str.parse().unwrap());
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(
|
generate_tests! {
|
||||||
key.to_string(),
|
{
|
||||||
|
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!(
|
concat!(
|
||||||
"#EXT-X-SESSION-KEY:",
|
"#EXT-X-SESSION-KEY:",
|
||||||
"METHOD=AES-128,",
|
"METHOD=AES-128,",
|
||||||
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
"IV=0x10ef8f758ca555115584bb5b3c687f52"
|
"IV=0x10ef8f758ca555115584bb5b3c687f52"
|
||||||
)
|
)
|
||||||
.to_string()
|
},
|
||||||
);
|
{
|
||||||
}
|
ExtXSessionKey::new(
|
||||||
|
DecryptionKey::builder()
|
||||||
#[test]
|
.method(EncryptionMethod::Aes128)
|
||||||
fn test_parser() {
|
.uri("https://www.example.com/hls-key/key.bin")
|
||||||
assert_eq!(
|
.iv([
|
||||||
concat!(
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
"#EXT-X-SESSION-KEY:",
|
])
|
||||||
"METHOD=AES-128,",
|
.format(KeyFormat::Identity)
|
||||||
"URI=\"https://priv.example.com/key.php?r=52\""
|
.build()
|
||||||
)
|
.unwrap(),
|
||||||
.parse::<ExtXSessionKey>()
|
),
|
||||||
.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::<ExtXSessionKey>()
|
|
||||||
.unwrap(),
|
|
||||||
key
|
|
||||||
);
|
|
||||||
|
|
||||||
key.set_key_format(Some(KeyFormat::Identity));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
concat!(
|
concat!(
|
||||||
"#EXT-X-SESSION-KEY:",
|
"#EXT-X-SESSION-KEY:",
|
||||||
"METHOD=AES-128,",
|
"METHOD=AES-128,",
|
||||||
|
@ -172,16 +139,13 @@ mod test {
|
||||||
"IV=0x10ef8f758ca555115584bb5b3c687f52,",
|
"IV=0x10ef8f758ca555115584bb5b3c687f52,",
|
||||||
"KEYFORMAT=\"identity\"",
|
"KEYFORMAT=\"identity\"",
|
||||||
)
|
)
|
||||||
.parse::<ExtXSessionKey>()
|
}
|
||||||
.unwrap(),
|
|
||||||
key
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXSessionKey::new(ExtXKey::new(
|
ExtXSessionKey::new(DecryptionKey::new(
|
||||||
EncryptionMethod::Aes128,
|
EncryptionMethod::Aes128,
|
||||||
"https://www.example.com/"
|
"https://www.example.com/"
|
||||||
))
|
))
|
||||||
|
@ -189,34 +153,4 @@ mod test {
|
||||||
ProtocolVersion::V1
|
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,254 +1,166 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
use shorthand::ShortHand;
|
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};
|
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
|
/// An unencrypted segment should be marked with [`ExtXKey::empty`].
|
||||||
/// decrypt them. It applies to every [`Media Segment`] and to every Media
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||||
/// Initialization Section declared by an [`ExtXMap`] tag, that appears
|
pub struct ExtXKey(pub Option<DecryptionKey>);
|
||||||
/// 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<String>,
|
|
||||||
/// An IV (initialization vector) is used to prevent repetitions between
|
|
||||||
/// segments of encrypted data.
|
|
||||||
///
|
|
||||||
/// <https://en.wikipedia.org/wiki/Initialization_vector>
|
|
||||||
///
|
|
||||||
/// # 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<KeyFormat>,
|
|
||||||
/// 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<KeyFormatVersions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXKey {
|
impl ExtXKey {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXKey`] tag.
|
/// Constructs an [`ExtXKey`] tag.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use hls_m3u8::tags::ExtXKey;
|
/// # 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!(
|
/// assert_eq!(
|
||||||
/// key.to_string(),
|
/// k.unwrap(),
|
||||||
/// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\""
|
/// 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<DecryptionKey>`.
|
||||||
|
///
|
||||||
|
/// # 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]
|
#[must_use]
|
||||||
pub fn new<T: Into<String>>(method: EncryptionMethod, uri: T) -> Self {
|
#[inline]
|
||||||
Self {
|
pub fn into_option(self) -> Option<DecryptionKey> { self.0 }
|
||||||
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<dyn ::std::error::Error>>(())
|
|
||||||
/// ```
|
|
||||||
#[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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or
|
/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or
|
||||||
|
@ -258,60 +170,9 @@ impl ExtXKey {
|
||||||
/// Otherwise [`ProtocolVersion::V1`] is required.
|
/// Otherwise [`ProtocolVersion::V1`] is required.
|
||||||
impl RequiredVersion for ExtXKey {
|
impl RequiredVersion for ExtXKey {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
if self.key_format.is_some() || self.key_format_versions.is_some() {
|
self.0
|
||||||
ProtocolVersion::V5
|
.as_ref()
|
||||||
} else if self.iv.is_some() {
|
.map_or(ProtocolVersion::V1, |i| i.required_version())
|
||||||
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<Self> {
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,42 +181,36 @@ impl FromStr for ExtXKey {
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
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<Option<DecryptionKey>> for ExtXKey {
|
||||||
|
fn from(value: Option<DecryptionKey>) -> Self { Self(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DecryptionKey> for ExtXKey {
|
||||||
|
fn from(value: DecryptionKey) -> Self { Self(Some(value)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::tags::ExtXSessionKey> for ExtXKey {
|
||||||
|
fn from(value: crate::tags::ExtXSessionKey) -> Self { Self(Some(value.0)) }
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXKey {
|
impl fmt::Display for ExtXKey {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
|
||||||
write!(f, "METHOD={}", self.method)?;
|
if let Some(value) = &self.0 {
|
||||||
|
write!(f, "{}", value)
|
||||||
if self.method == EncryptionMethod::None {
|
} else {
|
||||||
return Ok(());
|
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 crate::types::{EncryptionMethod, KeyFormat};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
macro_rules! generate_tests {
|
||||||
fn test_builder() {
|
( $( { $struct:expr, $str:expr } ),+ $(,)* ) => {
|
||||||
assert_eq!(
|
#[test]
|
||||||
ExtXKey::builder()
|
fn test_display() {
|
||||||
.method(EncryptionMethod::Aes128)
|
$(
|
||||||
.uri("https://www.example.com/")
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
.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()
|
#[test]
|
||||||
.unwrap()
|
fn test_parser() {
|
||||||
.to_string(),
|
$(
|
||||||
|
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::<ExtXKey>().is_err());
|
||||||
|
assert!("garbage".parse::<ExtXKey>().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!(
|
concat!(
|
||||||
"#EXT-X-KEY:",
|
"#EXT-X-KEY:",
|
||||||
"METHOD=AES-128,",
|
"METHOD=AES-128,",
|
||||||
"URI=\"https://www.example.com/\",",
|
"URI=\"https://priv.example.com/key.php?r=52\""
|
||||||
"IV=0x10ef8f758ca555115584bb5b3c687f52,",
|
|
||||||
"KEYFORMAT=\"identity\",",
|
|
||||||
"KEYFORMATVERSIONS=\"1/2/3/4/5\"",
|
|
||||||
)
|
)
|
||||||
.to_string()
|
},
|
||||||
);
|
{
|
||||||
|
ExtXKey::new(
|
||||||
assert!(ExtXKey::builder().build().is_err());
|
DecryptionKey::builder()
|
||||||
assert!(ExtXKey::builder()
|
.method(EncryptionMethod::Aes128)
|
||||||
.method(EncryptionMethod::Aes128)
|
.uri("https://www.example.com/hls-key/key.bin")
|
||||||
.build()
|
.iv([16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82])
|
||||||
.is_err());
|
.build()
|
||||||
}
|
.unwrap()
|
||||||
|
),
|
||||||
#[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(),
|
|
||||||
concat!(
|
concat!(
|
||||||
"#EXT-X-KEY:",
|
"#EXT-X-KEY:",
|
||||||
"METHOD=AES-128,",
|
"METHOD=AES-128,",
|
||||||
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
"IV=0x10ef8f758ca555115584bb5b3c687f52"
|
"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::<ExtXKey>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXKey::new(
|
ExtXKey::new(
|
||||||
EncryptionMethod::Aes128,
|
DecryptionKey::builder()
|
||||||
"https://priv.example.com/key.php?r=52"
|
.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)
|
||||||
assert_eq!(
|
.versions(vec![1, 2, 3])
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
),
|
||||||
concat!(
|
concat!(
|
||||||
"#EXT-X-KEY:",
|
"#EXT-X-KEY:",
|
||||||
"METHOD=AES-128,",
|
"METHOD=AES-128,",
|
||||||
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
"URI=\"https://www.example.com/hls-key/key.bin\",",
|
||||||
"IV=0X10ef8f758ca555115584bb5b3c687f52"
|
"IV=0x10ef8f758ca555115584bb5b3c687f52,",
|
||||||
)
|
|
||||||
.parse::<ExtXKey>()
|
|
||||||
.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\",",
|
"KEYFORMAT=\"identity\",",
|
||||||
"KEYFORMATVERSIONS=\"1/2/3\""
|
"KEYFORMATVERSIONS=\"1/2/3\""
|
||||||
)
|
)
|
||||||
.parse::<ExtXKey>()
|
},
|
||||||
.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::<ExtXKey>()
|
|
||||||
.unwrap(),
|
|
||||||
ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com")
|
|
||||||
);
|
|
||||||
assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::<ExtXKey>().is_err());
|
|
||||||
assert!("garbage".parse::<ExtXKey>().is_err());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
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
|
ProtocolVersion::V1
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXKey::builder()
|
ExtXKey::new(
|
||||||
.method(EncryptionMethod::Aes128)
|
DecryptionKey::builder()
|
||||||
.uri("https://www.example.com/")
|
.method(EncryptionMethod::Aes128)
|
||||||
.key_format(KeyFormat::Identity)
|
.uri("https://www.example.com/")
|
||||||
.key_format_versions(vec![1, 2, 3])
|
.format(KeyFormat::Identity)
|
||||||
.build()
|
.versions(vec![1, 2, 3])
|
||||||
.unwrap()
|
.build()
|
||||||
.required_version(),
|
.unwrap()
|
||||||
|
)
|
||||||
|
.required_version(),
|
||||||
ProtocolVersion::V5
|
ProtocolVersion::V5
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXKey::builder()
|
ExtXKey::new(
|
||||||
.method(EncryptionMethod::Aes128)
|
DecryptionKey::builder()
|
||||||
.uri("https://www.example.com/")
|
.method(EncryptionMethod::Aes128)
|
||||||
.iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])
|
.uri("https://www.example.com/")
|
||||||
.build()
|
.iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])
|
||||||
.unwrap()
|
.build()
|
||||||
.required_version(),
|
.unwrap()
|
||||||
|
)
|
||||||
|
.required_version(),
|
||||||
ProtocolVersion::V2
|
ProtocolVersion::V2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ use shorthand::ShortHand;
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::tags::ExtXKey;
|
use crate::tags::ExtXKey;
|
||||||
use crate::types::{ByteRange, ProtocolVersion};
|
use crate::types::{ByteRange, DecryptionKey, ProtocolVersion};
|
||||||
use crate::utils::{quote, tag, unquote};
|
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
|
/// The [`ExtXMap`] tag specifies how to obtain the [Media Initialization
|
||||||
/// Section], required to parse the applicable [`MediaSegment`]s.
|
/// Section], required to parse the applicable [`MediaSegment`]s.
|
||||||
|
@ -36,38 +36,12 @@ use crate::{Encrypted, Error, RequiredVersion};
|
||||||
pub struct ExtXMap {
|
pub struct ExtXMap {
|
||||||
/// The `URI` that identifies a resource, that contains the media
|
/// The `URI` that identifies a resource, that contains the media
|
||||||
/// initialization section.
|
/// 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,
|
uri: String,
|
||||||
/// The range of the media initialization section.
|
/// 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))]
|
#[shorthand(enable(copy))]
|
||||||
range: Option<ByteRange>,
|
range: Option<ByteRange>,
|
||||||
#[shorthand(enable(skip))]
|
#[shorthand(enable(skip))]
|
||||||
keys: Vec<ExtXKey>,
|
pub(crate) keys: Vec<ExtXKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXMap {
|
impl ExtXMap {
|
||||||
|
@ -108,10 +82,11 @@ impl ExtXMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encrypted for ExtXMap {
|
impl Decryptable for ExtXMap {
|
||||||
fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
fn keys(&self) -> Vec<&DecryptionKey> {
|
||||||
|
//
|
||||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys }
|
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that contains the
|
/// Use of the [`ExtXMap`] tag in a [`MediaPlaylist`] that contains the
|
||||||
|
@ -224,8 +199,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypted() {
|
fn test_decryptable() {
|
||||||
assert_eq!(ExtXMap::new("foo").keys(), &vec![]);
|
assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey>::new());
|
||||||
assert_eq!(ExtXMap::new("foo").keys_mut(), &mut vec![]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
136
src/traits.rs
136
src/traits.rs
|
@ -1,106 +1,54 @@
|
||||||
use crate::tags::ExtXKey;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use crate::types::{EncryptionMethod, ProtocolVersion};
|
|
||||||
|
|
||||||
/// A trait, that is implemented on all tags, that could be encrypted.
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use hls_m3u8::tags::ExtXKey;
|
|
||||||
/// use hls_m3u8::types::EncryptionMethod;
|
|
||||||
/// use hls_m3u8::Encrypted;
|
|
||||||
///
|
|
||||||
/// struct ExampleTag {
|
|
||||||
/// keys: Vec<ExtXKey>,
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// // Implementing the trait is very simple:
|
|
||||||
/// // Simply expose the internal buffer, that contains all the keys.
|
|
||||||
/// impl Encrypted for ExampleTag {
|
|
||||||
/// fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
|
||||||
///
|
|
||||||
/// fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &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<ExtXKey>;
|
|
||||||
|
|
||||||
/// Returns an exclusive reference to all keys, that can be used to decrypt
|
mod private {
|
||||||
/// this tag.
|
pub trait Sealed {}
|
||||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey>;
|
impl Sealed for crate::MediaSegment {}
|
||||||
|
impl Sealed for crate::tags::ExtXMap {}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets all keys, that can be used to decrypt this tag.
|
/// Signals that a type or some of the asssociated data might need to be
|
||||||
fn set_keys(&mut self, value: Vec<ExtXKey>) -> &mut Self {
|
/// decrypted.
|
||||||
let keys = self.keys_mut();
|
///
|
||||||
*keys = value;
|
/// # Note
|
||||||
self
|
///
|
||||||
}
|
/// You are not supposed to implement this trait, therefore it is "sealed".
|
||||||
|
pub trait Decryptable: private::Sealed {
|
||||||
/// Add a single key to the list of keys, that can be used to decrypt this
|
/// Returns all keys, associated with the type.
|
||||||
/// tag.
|
|
||||||
fn push_key(&mut self, value: ExtXKey) -> &mut Self {
|
|
||||||
self.keys_mut().push(value);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true`, if the tag is encrypted.
|
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Example
|
||||||
///
|
///
|
||||||
/// This will return `true`, if any of the keys satisfies
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// key.method() != EncryptionMethod::None
|
|
||||||
/// ```
|
/// ```
|
||||||
fn is_encrypted(&self) -> bool {
|
/// use hls_m3u8::tags::ExtXMap;
|
||||||
if self.keys().is_empty() {
|
/// use hls_m3u8::types::{ByteRange, EncryptionMethod};
|
||||||
return false;
|
/// 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()
|
/// Most of the time only a single key is provided, so instead of iterating
|
||||||
.iter()
|
/// through all keys, one might as well just get the first key.
|
||||||
.any(|k| k.method() != EncryptionMethod::None)
|
#[must_use]
|
||||||
|
fn first_key(&self) -> Option<&DecryptionKey> {
|
||||||
|
<Self as Decryptable>::keys(self).first().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `false`, if the tag is not encrypted.
|
/// Returns the number of keys.
|
||||||
///
|
#[must_use]
|
||||||
/// # Note
|
fn len(&self) -> usize { <Self as Decryptable>::keys(self).len() }
|
||||||
///
|
|
||||||
/// This is the inverse of [`is_encrypted`].
|
#[must_use]
|
||||||
///
|
fn is_empty(&self) -> bool { <Self as Decryptable>::len(self) == 0 }
|
||||||
/// [`is_encrypted`]: #method.is_encrypted
|
|
||||||
fn is_not_encrypted(&self) -> bool { !self.is_encrypted() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Example
|
/// # Example
|
||||||
|
|
317
src/types/decryption_key.rs
Normal file
317
src/types/decryption_key.rs
Normal file
|
@ -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<KeyFormat>,
|
||||||
|
/// The [`KeyFormatVersions`] attribute.
|
||||||
|
///
|
||||||
|
/// ## Note
|
||||||
|
///
|
||||||
|
/// This field is optional.
|
||||||
|
#[builder(setter(into, strip_option), default)]
|
||||||
|
pub versions: Option<KeyFormatVersions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecryptionKey {
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub fn new<I: Into<String>>(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<Self, Self::Err> {
|
||||||
|
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::<DecryptionKey>().is_err());
|
||||||
|
assert!("garbage".parse::<DecryptionKey>().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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,6 @@ use strum::{Display, EnumString};
|
||||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||||
pub enum EncryptionMethod {
|
pub enum EncryptionMethod {
|
||||||
/// The [`MediaSegment`]s are not encrypted.
|
|
||||||
///
|
|
||||||
/// [`MediaSegment`]: crate::MediaSegment
|
|
||||||
None,
|
|
||||||
/// The [`MediaSegment`]s are completely encrypted using the Advanced
|
/// The [`MediaSegment`]s are completely encrypted using the Advanced
|
||||||
/// Encryption Standard ([AES-128]) with a 128-bit key, Cipher Block
|
/// Encryption Standard ([AES-128]) with a 128-bit key, Cipher Block
|
||||||
/// Chaining (CBC), and [Public-Key Cryptography Standards #7 (PKCS7)]
|
/// Chaining (CBC), and [Public-Key Cryptography Standards #7 (PKCS7)]
|
||||||
|
@ -65,7 +61,6 @@ mod tests {
|
||||||
EncryptionMethod::SampleAes.to_string(),
|
EncryptionMethod::SampleAes.to_string(),
|
||||||
"SAMPLE-AES".to_string()
|
"SAMPLE-AES".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(EncryptionMethod::None.to_string(), "NONE".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -80,11 +75,6 @@ mod tests {
|
||||||
"SAMPLE-AES".parse::<EncryptionMethod>().unwrap()
|
"SAMPLE-AES".parse::<EncryptionMethod>().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
EncryptionMethod::None,
|
|
||||||
"NONE".parse::<EncryptionMethod>().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!("unknown".parse::<EncryptionMethod>().is_err());
|
assert!("unknown".parse::<EncryptionMethod>().is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::time::Duration;
|
||||||
use hls_m3u8::tags::{
|
use hls_m3u8::tags::{
|
||||||
ExtInf, ExtXEndList, ExtXKey, ExtXMedia, ExtXMediaSequence, ExtXTargetDuration, VariantStream,
|
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 hls_m3u8::{MasterPlaylist, MediaPlaylist, MediaSegment};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
@ -102,10 +102,10 @@ generate_tests! {
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
.inf(ExtInf::new(Duration::from_secs_f64(2.833)))
|
.inf(ExtInf::new(Duration::from_secs_f64(2.833)))
|
||||||
.keys(vec![
|
.keys(vec![
|
||||||
ExtXKey::new(
|
ExtXKey::new(DecryptionKey::new(
|
||||||
EncryptionMethod::Aes128,
|
EncryptionMethod::Aes128,
|
||||||
"https://priv.example.com/key.php?r=52"
|
"https://priv.example.com/key.php?r=52"
|
||||||
)
|
))
|
||||||
])
|
])
|
||||||
.uri("http://media.example.com/fileSequence52-A.ts")
|
.uri("http://media.example.com/fileSequence52-A.ts")
|
||||||
.build()
|
.build()
|
||||||
|
@ -113,10 +113,10 @@ generate_tests! {
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
.inf(ExtInf::new(Duration::from_secs_f64(15.0)))
|
.inf(ExtInf::new(Duration::from_secs_f64(15.0)))
|
||||||
.keys(vec![
|
.keys(vec![
|
||||||
ExtXKey::new(
|
ExtXKey::new(DecryptionKey::new(
|
||||||
EncryptionMethod::Aes128,
|
EncryptionMethod::Aes128,
|
||||||
"https://priv.example.com/key.php?r=52"
|
"https://priv.example.com/key.php?r=52"
|
||||||
)
|
))
|
||||||
])
|
])
|
||||||
.uri("http://media.example.com/fileSequence52-B.ts")
|
.uri("http://media.example.com/fileSequence52-B.ts")
|
||||||
.build()
|
.build()
|
||||||
|
@ -124,10 +124,10 @@ generate_tests! {
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
.inf(ExtInf::new(Duration::from_secs_f64(13.333)))
|
.inf(ExtInf::new(Duration::from_secs_f64(13.333)))
|
||||||
.keys(vec![
|
.keys(vec![
|
||||||
ExtXKey::new(
|
ExtXKey::new(DecryptionKey::new(
|
||||||
EncryptionMethod::Aes128,
|
EncryptionMethod::Aes128,
|
||||||
"https://priv.example.com/key.php?r=52"
|
"https://priv.example.com/key.php?r=52"
|
||||||
)
|
))
|
||||||
])
|
])
|
||||||
.uri("http://media.example.com/fileSequence52-C.ts")
|
.uri("http://media.example.com/fileSequence52-C.ts")
|
||||||
.build()
|
.build()
|
||||||
|
@ -135,10 +135,10 @@ generate_tests! {
|
||||||
MediaSegment::builder()
|
MediaSegment::builder()
|
||||||
.inf(ExtInf::new(Duration::from_secs_f64(15.0)))
|
.inf(ExtInf::new(Duration::from_secs_f64(15.0)))
|
||||||
.keys(vec![
|
.keys(vec![
|
||||||
ExtXKey::new(
|
ExtXKey::new(DecryptionKey::new(
|
||||||
EncryptionMethod::Aes128,
|
EncryptionMethod::Aes128,
|
||||||
"https://priv.example.com/key.php?r=53"
|
"https://priv.example.com/key.php?r=53"
|
||||||
)
|
))
|
||||||
])
|
])
|
||||||
.uri("http://media.example.com/fileSequence53-A.ts")
|
.uri("http://media.example.com/fileSequence53-A.ts")
|
||||||
.build()
|
.build()
|
||||||
|
|
Loading…
Reference in a new issue