diff --git a/Cargo.toml b/Cargo.toml index 2c4e602..4701571 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Takeru Ohta "] [dependencies] +chrono = "0.4" trackable = "0.2" [dev-dependencies] diff --git a/src/attribute.rs b/src/attribute.rs index a84df1f..f97f5d1 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -68,8 +68,18 @@ impl<'a> Iterator for AttributePairs<'a> { } } +// TODO: export and rename #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct QuotedString(String); +impl QuotedString { + pub fn new>(s: T) -> Result { + // TODO: validate + Ok(QuotedString(s.into())) + } + pub fn as_str(&self) -> &str { + &self.0 + } +} impl fmt::Display for QuotedString { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self.0) @@ -97,6 +107,11 @@ impl FromStr for QuotedString { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct HexadecimalSequence(Vec); +impl HexadecimalSequence { + pub fn new>>(v: T) -> Self { + HexadecimalSequence(v.into()) + } +} impl fmt::Display for HexadecimalSequence { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "0x")?; diff --git a/src/lib.rs b/src/lib.rs index 63c42d5..aa62fab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ //! //! TODO #![warn(missing_docs)] +extern crate chrono; #[macro_use] extern crate trackable; diff --git a/src/media_playlist.rs b/src/media_playlist.rs index bdc023e..3ca9f23 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -107,6 +107,10 @@ impl FromStr for MediaPlaylist { segment.tag(t); } Tag::ExtXByteRange(t) => { + // TODO: If o is not present, a previous Media Segment MUST appear in the + // Playlist file and MUST be a sub-range of the same media resource, or + // the Media Segment is undefined and the client MUST fail to parse the + // Playlist. segment.tag(t); } Tag::ExtXDiscontinuity(t) => { diff --git a/src/media_segment.rs b/src/media_segment.rs index 77c4c03..881b81d 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -12,7 +12,7 @@ pub struct MediaSegmentBuilder { ext_x_byterange: Option, ext_x_daterange: Option, ext_x_discontinuity: Option, - ext_x_key: Option, + ext_x_key: Option, // TODO: vec ext_x_map: Option, ext_x_program_date_time: Option, } diff --git a/src/tag/media_segment.rs b/src/tag/media_segment.rs new file mode 100644 index 0000000..317bf69 --- /dev/null +++ b/src/tag/media_segment.rs @@ -0,0 +1,638 @@ +use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; +use std::time::Duration; +use chrono::{DateTime, FixedOffset, NaiveDate}; +use trackable::error::ErrorKindExt; + +use {Error, ErrorKind, Result}; +use attribute::{AttributePairs, DecimalFloatingPoint, QuotedString}; +use types::{ByteRange, DecryptionKey, M3u8String, ProtocolVersion, Yes}; + +/// [4.3.2.1. EXTINF] +/// +/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtInf { + duration: Duration, + title: Option, +} +impl ExtInf { + pub(crate) const PREFIX: &'static str = "#EXTINF:"; + + /// Makes a new `ExtInf` tag. + pub fn new(duration: Duration) -> Self { + ExtInf { + duration, + title: None, + } + } + + /// Makes a new `ExtInf` tag with the given title. + pub fn with_title(duration: Duration, title: M3u8String) -> Self { + ExtInf { + duration, + title: Some(title), + } + } + + /// Returns the duration of the associated media segment. + pub fn duration(&self) -> Duration { + self.duration + } + + /// Returns the title of the associated media segment. + pub fn title(&self) -> Option<&M3u8String> { + self.title.as_ref() + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + if self.duration.subsec_nanos() == 0 { + ProtocolVersion::V1 + } else { + ProtocolVersion::V3 + } + } +} +impl fmt::Display for ExtInf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + + let duration = (self.duration.as_secs() as f64) + + (self.duration.subsec_nanos() as f64 / 1_000_000_000.0); + write!(f, "{}", duration)?; + + if let Some(ref title) = self.title { + write!(f, ",{}", title)?; + } + Ok(()) + } +} +impl FromStr for ExtInf { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ','); + + let seconds: DecimalFloatingPoint = + may_invalid!(tokens.next().expect("Never fails").parse())?; + let duration = seconds.to_duration(); + + let title = if let Some(title) = tokens.next() { + Some(track!(M3u8String::new(title))?) + } else { + None + }; + Ok(ExtInf { duration, title }) + } +} + +/// [4.3.2.2. EXT-X-BYTERANGE] +/// +/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXByteRange { + range: ByteRange, +} +impl ExtXByteRange { + pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; + + /// Makes a new `ExtXByteRange` tag. + pub fn new(range: ByteRange) -> Self { + ExtXByteRange { range } + } + + /// Returns the range of the associated media segment. + pub fn range(&self) -> ByteRange { + self.range + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V4 + } +} +impl fmt::Display for ExtXByteRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.range) + } +} +impl FromStr for ExtXByteRange { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; + Ok(ExtXByteRange { range }) + } +} + +/// [4.3.2.3. EXT-X-DISCONTINUITY] +/// +/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXDiscontinuity; +impl ExtXDiscontinuity { + pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY"; + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} +impl fmt::Display for ExtXDiscontinuity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Self::PREFIX.fmt(f) + } +} +impl FromStr for ExtXDiscontinuity { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + Ok(ExtXDiscontinuity) + } +} + +/// [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 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXKey { + key: Option, +} +impl ExtXKey { + pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; + + /// Makes a new `ExtXKey` tag. + pub fn new(key: DecryptionKey) -> Self { + ExtXKey { key: Some(key) } + } + + /// Makes a new `ExtXKey` tag without a decryption key. + /// + /// This tag has the `METHDO=NONE` attribute. + pub fn new_without_key() -> Self { + ExtXKey { key: None } + } + + /// Returns the decryption key for the following media segments and media initialization sections. + pub fn key(&self) -> Option<&DecryptionKey> { + self.key.as_ref() + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + self.key + .as_ref() + .map_or(ProtocolVersion::V1, |k| k.requires_version()) + } +} +impl fmt::Display for ExtXKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + if let Some(ref key) = self.key { + write!(f, "{}", key)?; + } else { + write!(f, "METHOD=NONE")?; + } + Ok(()) + } +} +impl FromStr for ExtXKey { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let suffix = s.split_at(Self::PREFIX.len()).1; + + if AttributePairs::parse(suffix) + .find(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) + .is_some() + { + for attr in AttributePairs::parse(suffix) { + let (key, _) = track!(attr)?; + track_assert_ne!(key, "URI", ErrorKind::InvalidInput); + track_assert_ne!(key, "IV", ErrorKind::InvalidInput); + track_assert_ne!(key, "KEYFORMAT", ErrorKind::InvalidInput); + track_assert_ne!(key, "KEYFORMATVERSIONS", ErrorKind::InvalidInput); + } + Ok(ExtXKey { key: None }) + } else { + let key = track!(suffix.parse())?; + Ok(ExtXKey { key: Some(key) }) + } + } +} + +/// [4.3.2.5. EXT-X-MAP] +/// +/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXMap { + uri: QuotedString, + range: Option, +} +impl ExtXMap { + pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:"; + + /// Makes a new `ExtXMap` tag. + pub fn new(uri: QuotedString) -> Self { + ExtXMap { uri, range: None } + } + + /// Makes a new `ExtXMap` tag with the given range. + pub fn with_range(uri: QuotedString, range: ByteRange) -> Self { + ExtXMap { + uri, + range: Some(range), + } + } + + /// Returns the URI that identifies a resource that contains the media initialization section. + pub fn uri(&self) -> &QuotedString { + &self.uri + } + + /// Returns the range of the media initialization section. + pub fn range(&self) -> Option { + self.range + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V6 + } +} +impl fmt::Display for ExtXMap { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "URI={}", self.uri)?; + if let Some(ref x) = self.range { + write!(f, ",BYTERANGE={}", x)?; + } + Ok(()) + } +} +impl FromStr for ExtXMap { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + let mut uri = None; + let mut range = None; + let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "URI" => { + track_assert_eq!(uri, None, ErrorKind::InvalidInput); + uri = Some(track!(value.parse())?); + } + "BYTERANGE" => { + track_assert_eq!(range, None, ErrorKind::InvalidInput); + range = Some(track!(value.parse())?); + } + _ => { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + + let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + Ok(ExtXMap { uri, range }) + } +} + +/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] +/// +/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtXProgramDateTime { + date_time: DateTime, +} +impl ExtXProgramDateTime { + pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; + + /// Makes a new `ExtXProgramDateTime` tag. + pub fn new(date_time: DateTime) -> Self { + ExtXProgramDateTime { date_time } + } + + /// Returns the `DateTime` of the first sample of the associated media segment. + pub fn date_time(&self) -> DateTime { + self.date_time + } + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} +impl fmt::Display for ExtXProgramDateTime { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.date_time.to_rfc3339()) + } +} +impl FromStr for ExtXProgramDateTime { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + let suffix = s.split_at(Self::PREFIX.len()).1; + Ok(ExtXProgramDateTime { + date_time: track!(suffix.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, + }) + } +} + +/// [4.3.2.7. EXT-X-DATERANGE] +/// +/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7 +/// +/// TODO: Implement properly +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExtXDateRange { + pub id: QuotedString, + pub class: Option, + pub start_date: NaiveDate, + pub end_date: Option, + pub duration: Option, + pub planned_duration: Option, + pub scte35_cmd: Option, + pub scte35_out: Option, + pub scte35_in: Option, + pub end_on_next: Option, + pub client_attributes: BTreeMap, +} +impl ExtXDateRange { + pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; + + /// Returns the protocol compatibility version that this tag requires. + pub fn requires_version(&self) -> ProtocolVersion { + ProtocolVersion::V1 + } +} +impl fmt::Display for ExtXDateRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", Self::PREFIX)?; + write!(f, "ID={}", self.id)?; + if let Some(ref x) = self.class { + write!(f, ",CLASS={}", x)?; + } + write!( + f, + ",START_DATE={:?}", + self.start_date.format("%Y-%m-%d").to_string() + )?; + if let Some(ref x) = self.end_date { + write!(f, ",END_DATE={:?}", x.format("%Y-%m-%d").to_string())?; + } + if let Some(x) = self.duration { + write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?; + } + if let Some(x) = self.planned_duration { + write!( + f, + ",PLANNED_DURATION={}", + DecimalFloatingPoint::from_duration(x) + )?; + } + if let Some(ref x) = self.scte35_cmd { + write!(f, ",SCTE35_CMD={}", x)?; + } + if let Some(ref x) = self.scte35_out { + write!(f, ",SCTE35_OUT={}", x)?; + } + if let Some(ref x) = self.scte35_in { + write!(f, ",SCTE35_IN={}", x)?; + } + if let Some(ref x) = self.end_on_next { + write!(f, ",END_ON_NEXT={}", x)?; + } + for (k, v) in &self.client_attributes { + write!(f, ",{}={}", k, v)?; + } + Ok(()) + } +} +impl FromStr for ExtXDateRange { + type Err = Error; + fn from_str(s: &str) -> Result { + track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + let mut id = None; + let mut class = None; + let mut start_date = None; + let mut end_date = None; + let mut duration = None; + let mut planned_duration = None; + let mut scte35_cmd = None; + let mut scte35_out = None; + let mut scte35_in = None; + let mut end_on_next = None; + let mut client_attributes = BTreeMap::new(); + let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "ID" => { + id = Some(track!(value.parse())?); + } + "CLASS" => { + class = Some(track!(value.parse())?); + } + "START-DATE" => { + let s: QuotedString = track!(value.parse())?; + start_date = Some(track!( + NaiveDate::parse_from_str(s.as_str(), "%Y-%m-%d") + .map_err(|e| ErrorKind::InvalidInput.cause(e)) + )?); + } + "END-DATE" => { + let s: QuotedString = track!(value.parse())?; + end_date = Some(track!( + NaiveDate::parse_from_str(s.as_str(), "%Y-%m-%d") + .map_err(|e| ErrorKind::InvalidInput.cause(e)) + )?); + } + "DURATION" => { + let seconds: DecimalFloatingPoint = track!(value.parse())?; + duration = Some(seconds.to_duration()); + } + "PLANNED-DURATION" => { + let seconds: DecimalFloatingPoint = track!(value.parse())?; + planned_duration = Some(seconds.to_duration()); + } + "SCTE35-CMD" => { + scte35_cmd = Some(track!(value.parse())?); + } + "SCTE35-OUT" => { + scte35_out = Some(track!(value.parse())?); + } + "SCTE35-IN" => { + scte35_in = Some(track!(value.parse())?); + } + "END-ON-NEXT" => { + end_on_next = Some(track!(value.parse())?); + } + _ => { + if key.starts_with("X-") { + client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned()); + } else { + // [6.3.1. General Client Responsibilities] + // > ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + } + + let id = track_assert_some!(id, ErrorKind::InvalidInput); + let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput); + if end_on_next.is_some() { + track_assert!(class.is_some(), ErrorKind::InvalidInput); + } + Ok(ExtXDateRange { + id, + class, + start_date, + end_date, + duration, + planned_duration, + scte35_cmd, + scte35_out, + scte35_in, + end_on_next, + client_attributes, + }) + } +} + +#[cfg(test)] +mod test { + use std::time::Duration; + + use attribute::HexadecimalSequence; + use types::EncryptionMethod; + use super::*; + + #[test] + fn extinf() { + let tag = ExtInf::new(Duration::from_secs(5)); + assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), "#EXTINF:5"); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + + let tag = ExtInf::with_title(Duration::from_secs(5), M3u8String::new("foo").unwrap()); + assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), "#EXTINF:5,foo"); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + + let tag = ExtInf::new(Duration::from_millis(1234)); + assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), "#EXTINF:1.234"); + assert_eq!(tag.requires_version(), ProtocolVersion::V3); + } + + #[test] + fn ext_x_byterange() { + let tag = ExtXByteRange::new(ByteRange { + length: 3, + start: None, + }); + assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3"); + assert_eq!(tag.requires_version(), ProtocolVersion::V4); + + let tag = ExtXByteRange::new(ByteRange { + length: 3, + start: Some(5), + }); + assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5"); + assert_eq!(tag.requires_version(), ProtocolVersion::V4); + } + + #[test] + fn ext_x_discontinuity() { + let tag = ExtXDiscontinuity; + assert_eq!("#EXT-X-DISCONTINUITY".parse().ok(), Some(tag)); + assert_eq!(tag.to_string(), "#EXT-X-DISCONTINUITY"); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } + + #[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); + + let tag = ExtXKey::new(DecryptionKey { + method: EncryptionMethod::Aes128, + uri: QuotedString::new("foo").unwrap(), + 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); + + let tag = ExtXKey::new(DecryptionKey { + method: EncryptionMethod::Aes128, + uri: QuotedString::new("foo").unwrap(), + iv: Some(HexadecimalSequence::new(vec![0, 1, 2])), + key_format: None, + key_format_versions: None, + }); + let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102"#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V2); + + let tag = ExtXKey::new(DecryptionKey { + method: EncryptionMethod::Aes128, + uri: QuotedString::new("foo").unwrap(), + iv: Some(HexadecimalSequence::new(vec![0, 1, 2])), + key_format: Some(QuotedString::new("baz").unwrap()), + key_format_versions: None, + }); + let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102,KEYFORMAT="baz""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V5); + } + + #[test] + fn ext_x_map() { + let tag = ExtXMap::new(QuotedString::new("foo").unwrap()); + let text = r#"#EXT-X-MAP:URI="foo""#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V6); + + let tag = ExtXMap::with_range( + QuotedString::new("foo").unwrap(), + ByteRange { + length: 9, + start: Some(2), + }, + ); + let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE=9@2"#; + assert_eq!(text.parse().ok(), Some(tag.clone())); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V6); + } + + #[test] + fn ext_x_program_date_time() { + let text = "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"; + assert!(text.parse::().is_ok()); + + let tag = text.parse::().unwrap(); + assert_eq!(tag.to_string(), text); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); + } +} diff --git a/src/tag/mod.rs b/src/tag/mod.rs index 5ae7bd8..57f0028 100644 --- a/src/tag/mod.rs +++ b/src/tag/mod.rs @@ -33,18 +33,7 @@ pub use self::media_segment::{ExtInf, ExtXByteRange, ExtXDateRange, ExtXDisconti mod basic; mod media_segment; -/// [4.3.1. Basic Tags] -/// -/// [4.3.1. Basic Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.1 #[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum BasicTag { - ExtM3u(ExtM3u), - ExtXVersion(ExtXVersion), -} -impl_from!(BasicTag, ExtM3u); -impl_from!(BasicTag, ExtXVersion); - #[derive(Debug, Clone, PartialEq, Eq)] pub enum MediaSegmentTag { ExtInf(ExtInf), @@ -56,6 +45,7 @@ pub enum MediaSegmentTag { ExtXProgramDateTime(ExtXProgramDateTime), } // TODO: delete +#[allow(missing_docs)] impl MediaSegmentTag { pub fn as_inf(&self) -> Option<&ExtInf> { if let MediaSegmentTag::ExtInf(ref t) = *self { diff --git a/src/types.rs b/src/types.rs index eb011dd..08b4ac3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -5,6 +5,7 @@ use std::str::FromStr; use trackable::error::ErrorKindExt; use {Error, ErrorKind, Result}; +use attribute::{AttributePairs, HexadecimalSequence, QuotedString}; // TODO: rename #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -123,3 +124,116 @@ impl FromStr for ByteRange { }) } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DecryptionKey { + pub method: EncryptionMethod, + pub uri: QuotedString, + pub iv: Option, + pub key_format: Option, + pub key_format_versions: Option, +} +impl DecryptionKey { + pub 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={}", self.uri)?; + if let Some(ref x) = self.iv { + write!(f, ",IV={}", x)?; + } + if let Some(ref x) = self.key_format { + write!(f, ",KEYFORMAT={}", x)?; + } + if let Some(ref x) = self.key_format_versions { + write!(f, ",KEYFORMATVERSIONS={}", x)?; + } + Ok(()) + } +} +impl FromStr for DecryptionKey { + type Err = Error; + fn from_str(s: &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; + let attrs = AttributePairs::parse(s); + for attr in attrs { + let (key, value) = track!(attr)?; + match key { + "METHOD" => { + track_assert_eq!(method, None, ErrorKind::InvalidInput); + method = Some(track!(value.parse())?); + } + "URI" => { + track_assert_eq!(uri, None, ErrorKind::InvalidInput); + uri = Some(track!(value.parse())?); + } + "IV" => { + // TODO: validate length(128-bit) + track_assert_eq!(iv, None, ErrorKind::InvalidInput); + iv = Some(track!(value.parse())?); + } + "KEYFORMAT" => { + track_assert_eq!(key_format, None, ErrorKind::InvalidInput); + key_format = Some(track!(value.parse())?); + } + "KEYFORMATVERSIONS" => { + track_assert_eq!(key_format_versions, None, ErrorKind::InvalidInput); + key_format_versions = Some(track!(value.parse())?); + } + _ => { + // [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName. + } + } + } + let method = track_assert_some!(method, ErrorKind::InvalidInput); + let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + Ok(DecryptionKey { + method, + uri, + iv, + key_format, + key_format_versions, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EncryptionMethod { + Aes128, + SampleAes, +} +impl fmt::Display for EncryptionMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + EncryptionMethod::Aes128 => "AES-128".fmt(f), + EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), + } + } +} +impl FromStr for EncryptionMethod { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "AES-128" => Ok(EncryptionMethod::Aes128), + "SAMPLE-AES" => Ok(EncryptionMethod::SampleAes), + _ => track_panic!( + ErrorKind::InvalidInput, + "Unknown encryption method: {:?}", + s + ), + } + } +}