diff --git a/Cargo.toml b/Cargo.toml index 37efbc5..fdb392b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,18 @@ readme = "README.md" license = "MIT OR Apache-2.0" keywords = ["hls", "m3u8"] edition = "2018" +categories = ["parser"] [badges] travis-ci = {repository = "sile/hls_m3u8"} codecov = {repository = "sile/hls_m3u8"} [dependencies] -trackable = "0.2" +getset = "0.0.8" +failure = "0.1.5" +derive_builder = "0.7.2" +url = "2.1.0" +chrono = "0.4.9" [dev-dependencies] -clap = "2" \ No newline at end of file +clap = "2" diff --git a/examples/parse.rs b/examples/parse.rs index 44a8d16..226a22d 100644 --- a/examples/parse.rs +++ b/examples/parse.rs @@ -1,12 +1,9 @@ extern crate clap; extern crate hls_m3u8; -#[macro_use] -extern crate trackable; use clap::{App, Arg}; use hls_m3u8::{MasterPlaylist, MediaPlaylist}; use std::io::{self, Read}; -use trackable::error::Failure; fn main() { let matches = App::new("parse") @@ -19,17 +16,15 @@ fn main() { ) .get_matches(); let mut m3u8 = String::new(); - track_try_unwrap!(io::stdin() - .read_to_string(&mut m3u8) - .map_err(Failure::from_error)); + io::stdin().read_to_string(&mut m3u8).unwrap(); match matches.value_of("M3U8_TYPE").unwrap() { "media" => { - let playlist: MediaPlaylist = track_try_unwrap!(m3u8.parse()); + let playlist: MediaPlaylist = m3u8.parse().unwrap(); println!("{}", playlist); } "master" => { - let playlist: MasterPlaylist = track_try_unwrap!(m3u8.parse()); + let playlist: MasterPlaylist = m3u8.parse().unwrap(); println!("{}", playlist); } _ => unreachable!(), diff --git a/src/attribute.rs b/src/attribute.rs index 8ef7450..f2645e0 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,103 +1,138 @@ -use crate::{ErrorKind, Result}; -use std::collections::HashSet; -use std::str; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; -#[derive(Debug)] -pub struct AttributePairs<'a> { - input: &'a str, - visited_keys: HashSet<&'a str>, -} -impl<'a> AttributePairs<'a> { - pub fn parse(input: &'a str) -> Self { - AttributePairs { - input, - visited_keys: HashSet::new(), - } - } +use crate::Error; - fn parse_name(&mut self) -> Result<&'a str> { - for i in 0..self.input.len() { - match self.input.as_bytes()[i] { - b'=' => { - let (key, _) = self.input.split_at(i); - let (_, rest) = self.input.split_at(i + 1); - self.input = rest; - return Ok(key); - } - b'A'..=b'Z' | b'0'..=b'9' | b'-' => {} - _ => track_panic!( - ErrorKind::InvalidInput, - "Malformed attribute name: {:?}", - self.input - ), - } - } - track_panic!( - ErrorKind::InvalidInput, - "No attribute value: {:?}", - self.input - ); - } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AttributePairs(HashMap); - fn parse_raw_value(&mut self) -> &'a str { - let mut in_quote = false; - let mut value_end = self.input.len(); - let mut next = self.input.len(); - for (i, c) in self.input.bytes().enumerate() { - match c { - b'"' => { - in_quote = !in_quote; - } - b',' if !in_quote => { - value_end = i; - next = i + 1; - break; - } - _ => {} - } - } - let (value, _) = self.input.split_at(value_end); - let (_, rest) = self.input.split_at(next); - self.input = rest; - value +impl AttributePairs { + pub fn new() -> Self { + Self::default() } } -impl<'a> Iterator for AttributePairs<'a> { - type Item = Result<(&'a str, &'a str)>; - fn next(&mut self) -> Option { - if self.input.is_empty() { - return None; + +impl Deref for AttributePairs { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for AttributePairs { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl IntoIterator for AttributePairs { + type Item = (String, String); + type IntoIter = ::std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a AttributePairs { + type Item = (&'a String, &'a String); + type IntoIter = ::std::collections::hash_map::Iter<'a, String, String>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl FromStr for AttributePairs { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut result = AttributePairs::new(); + + for line in split(input, ',') { + let pair = split(line.trim(), '='); + + if pair.len() < 2 { + return Err(Error::invalid_input()); + } + + let key = pair[0].to_uppercase(); + let value = pair[1].to_string(); + + result.insert(key.to_string(), value.to_string()); } - let result = || -> Result<(&'a str, &'a str)> { - let key = track!(self.parse_name())?; - track_assert!( - self.visited_keys.insert(key), - ErrorKind::InvalidInput, - "Duplicate attribute key: {:?}", - key - ); - - let value = self.parse_raw_value(); - Ok((key, value)) - }(); - Some(result) + dbg!(&result); + Ok(result) } } +fn split(value: &str, terminator: char) -> Vec { + let mut result = vec![]; + + let mut inside_quotes = false; + let mut temp_string = String::new(); + + for c in value.chars() { + match c { + '"' => { + if inside_quotes { + inside_quotes = false; + } else { + inside_quotes = true; + } + temp_string.push(c); + } + k if (k == terminator) => { + if !inside_quotes { + result.push(temp_string); + temp_string = String::new(); + } else { + temp_string.push(c); + } + } + _ => { + temp_string.push(c); + } + } + } + result.push(temp_string); + + result +} + #[cfg(test)] mod test { use super::*; #[test] - fn it_works() { - let mut pairs = AttributePairs::parse("FOO=BAR,BAR=\"baz,qux\",ABC=12.3"); - assert_eq!(pairs.next().map(|x| x.ok()), Some(Some(("FOO", "BAR")))); - assert_eq!( - pairs.next().map(|x| x.ok()), - Some(Some(("BAR", "\"baz,qux\""))) - ); - assert_eq!(pairs.next().map(|x| x.ok()), Some(Some(("ABC", "12.3")))); - assert_eq!(pairs.next().map(|x| x.ok()), None) + fn test_parser() { + let pairs = ("FOO=BAR,BAR=\"baz,qux\",ABC=12.3") + .parse::() + .unwrap(); + + let mut iterator = pairs.iter(); + assert!(iterator.any(|(k, v)| k == "FOO" && "BAR" == v)); + + let mut iterator = pairs.iter(); + assert!(iterator.any(|(k, v)| k == "BAR" && v == "\"baz,qux\"")); + + let mut iterator = pairs.iter(); + assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3")); + } + + #[test] + fn test_iterator() { + let mut attrs = AttributePairs::new(); + attrs.insert("key_01".to_string(), "value_01".to_string()); + attrs.insert("key_02".to_string(), "value_02".to_string()); + + let mut iterator = attrs.iter(); + assert!(iterator.any(|(k, v)| k == "key_01" && v == "value_01")); + + let mut iterator = attrs.iter(); + assert!(iterator.any(|(k, v)| k == "key_02" && v == "value_02")); } } diff --git a/src/error.rs b/src/error.rs index b803bd1..7acb888 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,13 +1,224 @@ -use trackable::error::{ErrorKind as TrackableErrorKind, TrackableError}; +use std::error; +use std::fmt; -/// This crate specific `Error` type. -#[derive(Debug, Clone, TrackableError)] -pub struct Error(TrackableError); +use failure::{Backtrace, Context, Fail}; -/// Possible error kinds. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[allow(missing_docs)] +/// This crate specific `Result` type. +pub type Result = std::result::Result; + +/// The ErrorKind. +#[derive(Debug, Fail, Clone, PartialEq, Eq)] pub enum ErrorKind { + #[fail(display = "ChronoParseError: {}", _0)] + /// An error from the [Chrono](chrono) crate. + ChronoParseError(String), + #[fail(display = "UrlParseError: {}", _0)] + /// An error from the [Url](url) crate. + UrlParseError(String), + #[fail(display = "UnknownError: {}", _0)] + /// An unknown error occured. + UnknownError(String), + + #[fail(display = "A value is missing for the attribute {}", _0)] + /// A required value is missing. + MissingValue(String), + + #[fail(display = "Invalid Input")] + /// Error for anything. InvalidInput, + + #[fail(display = "ParseIntError: {}", _0)] + /// Failed to parse a String to int. + ParseIntError(String), + + #[fail(display = "ParseFloatError: {}", _0)] + /// Failed to parse a String to float. + ParseFloatError(String), + + #[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)] + /// A tag is missing, that is required at the start of the input. + MissingTag { + /// The required tag. + tag: String, + /// The unparsed input data. + input: String, + }, + + #[fail(display = "CustomError: {}", _0)] + /// A custom error. + Custom(String), + + #[fail(display = "Unmatched Group: {:?}", _0)] + /// Unmatched Group + UnmatchedGroup(String), + + #[fail(display = "Unknown Protocol version: {:?}", _0)] + /// Unknown m3u8 version. This library supports up to ProtocolVersion 7. + UnknownProtocolVersion(String), + + #[fail(display = "IoError: {}", _0)] + /// Some io error + Io(String), + + #[fail( + display = "VersionError: required_version: {:?}, specified_version: {:?}", + _0, _1 + )] + /// This error occurs, if there is a ProtocolVersion mismatch. + VersionError(String, String), + + #[fail(display = "BuilderError: {}", _0)] + /// An Error from a Builder. + BuilderError(String), + + /// Hints that destructuring should not be exhaustive. + /// + /// This enum may grow additional variants, so this makes sure clients + /// don't count on exhaustive matching. (Otherwise, adding a new variant + /// could break existing code.) + #[doc(hidden)] + #[fail(display = "Invalid error")] + __Nonexhaustive, +} + +#[derive(Debug)] +/// The Error type of this library. +pub struct Error { + inner: Context, +} + +impl Fail for Error { + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error::from(Context::new(kind)) + } +} + +impl From> for Error { + fn from(inner: Context) -> Error { + Error { inner } + } +} + +impl Error { + pub(crate) fn unknown(value: T) -> Self + where + T: error::Error, + { + Self::from(ErrorKind::UnknownError(value.to_string())) + } + + pub(crate) fn missing_value(value: T) -> Self { + Self::from(ErrorKind::MissingValue(value.to_string())) + } + + pub(crate) fn invalid_input() -> Self { + Self::from(ErrorKind::InvalidInput) + } + + pub(crate) fn parse_int_error(value: T) -> Self { + Self::from(ErrorKind::ParseIntError(value.to_string())) + } + + pub(crate) fn parse_float_error(value: T) -> Self { + Self::from(ErrorKind::ParseFloatError(value.to_string())) + } + + pub(crate) fn missing_tag(tag: T, input: U) -> Self + where + T: ToString, + U: ToString, + { + Self::from(ErrorKind::MissingTag { + tag: tag.to_string(), + input: input.to_string(), + }) + } + + pub(crate) fn unmatched_group(value: T) -> Self { + Self::from(ErrorKind::UnmatchedGroup(value.to_string())) + } + + pub(crate) fn custom(value: T) -> Self + where + T: fmt::Display, + { + Self::from(ErrorKind::Custom(value.to_string())) + } + + pub(crate) fn unknown_protocol_version(value: T) -> Self { + Self::from(ErrorKind::UnknownProtocolVersion(value.to_string())) + } + + pub(crate) fn io(value: T) -> Self { + Self::from(ErrorKind::Io(value.to_string())) + } + + pub(crate) fn required_version(required_version: T, specified_version: U) -> Self + where + T: ToString, + U: ToString, + { + Self::from(ErrorKind::VersionError( + required_version.to_string(), + specified_version.to_string(), + )) + } + + pub(crate) fn builder_error(value: T) -> Self { + Self::from(ErrorKind::BuilderError(value.to_string())) + } + + pub(crate) fn url(value: T) -> Self { + Self::from(ErrorKind::UrlParseError(value.to_string())) + } + + pub(crate) fn chrono(value: T) -> Self { + Self::from(ErrorKind::ChronoParseError(value.to_string())) + } +} + +impl From<::std::num::ParseIntError> for Error { + fn from(value: ::std::num::ParseIntError) -> Self { + Error::parse_int_error(value) + } +} + +impl From<::std::num::ParseFloatError> for Error { + fn from(value: ::std::num::ParseFloatError) -> Self { + Error::parse_float_error(value) + } +} + +impl From<::std::io::Error> for Error { + fn from(value: ::std::io::Error) -> Self { + Error::io(value) + } +} + +impl From<::url::ParseError> for Error { + fn from(value: ::url::ParseError) -> Self { + Error::url(value) + } +} + +impl From<::chrono::ParseError> for Error { + fn from(value: ::chrono::ParseError) -> Self { + Error::chrono(value) + } } -impl TrackableErrorKind for ErrorKind {} diff --git a/src/lib.rs b/src/lib.rs index 889d757..75e8bd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,9 @@ +#![warn( + //clippy::pedantic, + clippy::nursery, + clippy::cargo +)] +#![warn(missing_docs)] //! [HLS] m3u8 parser/generator. //! //! [HLS]: https://tools.ietf.org/html/rfc8216 @@ -20,13 +26,10 @@ //! //! assert!(m3u8.parse::().is_ok()); //! ``` -#![warn(missing_docs)] -#[macro_use] -extern crate trackable; pub use error::{Error, ErrorKind}; pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder}; -pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder, MediaPlaylistOptions}; +pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder}; pub use media_segment::{MediaSegment, MediaSegmentBuilder}; pub mod tags; @@ -40,5 +43,4 @@ mod media_playlist; mod media_segment; mod utils; -/// This crate specific `Result` type. -pub type Result = std::result::Result; +pub use error::Result; diff --git a/src/line.rs b/src/line.rs index 6b1b1f6..28b3a32 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,81 +1,99 @@ -use crate::tags; -use crate::types::SingleLineString; -use crate::{Error, ErrorKind, Result}; use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; -#[derive(Debug)] -pub struct Lines<'a> { - input: &'a str, -} -impl<'a> Lines<'a> { - pub fn new(input: &'a str) -> Self { - Lines { input } - } +use url::Url; - fn read_line(&mut self) -> Result> { - let mut end = self.input.len(); - let mut next_start = self.input.len(); - let mut adjust = 0; - let mut next_line_of_ext_x_stream_inf = false; - for (i, c) in self.input.char_indices() { - match c { - '\n' => { - if !next_line_of_ext_x_stream_inf - && self.input.starts_with(tags::ExtXStreamInf::PREFIX) - { - next_line_of_ext_x_stream_inf = true; - adjust = 0; - continue; - } - next_start = i + 1; - end = i - adjust; - break; - } - '\r' => { - adjust = 1; - } - _ => { - track_assert!(!c.is_control(), ErrorKind::InvalidInput); - adjust = 0; - } +use crate::tags; +use crate::Error; + +#[derive(Debug, Default)] +pub struct Lines(Vec); + +impl Lines { + pub fn new() -> Self { + Self::default() + } +} + +impl FromStr for Lines { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut result = Lines::new(); + + let mut stream_inf = false; + let mut stream_inf_line = None; + + for l in input.lines() { + let line = l.trim(); + + // ignore empty lines + if line.len() == 0 { + continue; } + + let pline = { + if line.starts_with(tags::ExtXStreamInf::PREFIX) { + stream_inf = true; + stream_inf_line = Some(line); + + continue; + } else if line.starts_with("#EXT") { + Line::Tag(line.parse()?) + } else if line.starts_with("#") { + continue; // ignore comments + } else { + // stream inf line needs special treatment + if stream_inf { + stream_inf = false; + if let Some(first_line) = stream_inf_line { + let res = Line::Tag(format!("{}\n{}", first_line, line).parse()?); + stream_inf_line = None; + res + } else { + continue; + } + } else { + Line::Uri(line.trim().parse()?) + } + } + }; + + result.push(pline); } - let raw_line = &self.input[..end]; - let line = if raw_line.is_empty() { - Line::Blank - } else if raw_line.starts_with("#EXT") { - Line::Tag(track!(raw_line.parse())?) - } else if raw_line.starts_with('#') { - Line::Comment(raw_line) - } else { - let uri = track!(SingleLineString::new(raw_line))?; - Line::Uri(uri) - }; - self.input = &self.input[next_start..]; - Ok(line) - } -} -impl<'a> Iterator for Lines<'a> { - type Item = Result>; - fn next(&mut self) -> Option { - if self.input.is_empty() { - return None; - } - match track!(self.read_line()) { - Err(e) => Some(Err(e)), - Ok(line) => Some(Ok(line)), - } + + Ok(result) + } +} + +impl IntoIterator for Lines { + type Item = Line; + type IntoIter = ::std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Deref for Lines { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Lines { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } -#[allow(clippy::large_enum_variant)] #[derive(Debug, PartialEq, Eq)] -pub enum Line<'a> { - Blank, - Comment(&'a str), +pub enum Line { Tag(Tag), - Uri(SingleLineString), + Uri(Url), } #[allow(clippy::large_enum_variant)] @@ -103,86 +121,89 @@ pub enum Tag { ExtXSessionKey(tags::ExtXSessionKey), ExtXIndependentSegments(tags::ExtXIndependentSegments), ExtXStart(tags::ExtXStart), - Unknown(SingleLineString), + Unknown(String), } + impl fmt::Display for Tag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Tag::ExtM3u(ref t) => t.fmt(f), - Tag::ExtXVersion(ref t) => t.fmt(f), - Tag::ExtInf(ref t) => t.fmt(f), - Tag::ExtXByteRange(ref t) => t.fmt(f), - Tag::ExtXDiscontinuity(ref t) => t.fmt(f), - Tag::ExtXKey(ref t) => t.fmt(f), - Tag::ExtXMap(ref t) => t.fmt(f), - Tag::ExtXProgramDateTime(ref t) => t.fmt(f), - Tag::ExtXDateRange(ref t) => t.fmt(f), - Tag::ExtXTargetDuration(ref t) => t.fmt(f), - Tag::ExtXMediaSequence(ref t) => t.fmt(f), - Tag::ExtXDiscontinuitySequence(ref t) => t.fmt(f), - Tag::ExtXEndList(ref t) => t.fmt(f), - Tag::ExtXPlaylistType(ref t) => t.fmt(f), - Tag::ExtXIFramesOnly(ref t) => t.fmt(f), - Tag::ExtXMedia(ref t) => t.fmt(f), - Tag::ExtXStreamInf(ref t) => t.fmt(f), - Tag::ExtXIFrameStreamInf(ref t) => t.fmt(f), - Tag::ExtXSessionData(ref t) => t.fmt(f), - Tag::ExtXSessionKey(ref t) => t.fmt(f), - Tag::ExtXIndependentSegments(ref t) => t.fmt(f), - Tag::ExtXStart(ref t) => t.fmt(f), - Tag::Unknown(ref t) => t.fmt(f), + match &self { + Tag::ExtM3u(value) => value.fmt(f), + Tag::ExtXVersion(value) => value.fmt(f), + Tag::ExtInf(value) => value.fmt(f), + Tag::ExtXByteRange(value) => value.fmt(f), + Tag::ExtXDiscontinuity(value) => value.fmt(f), + Tag::ExtXKey(value) => value.fmt(f), + Tag::ExtXMap(value) => value.fmt(f), + Tag::ExtXProgramDateTime(value) => value.fmt(f), + Tag::ExtXDateRange(value) => value.fmt(f), + Tag::ExtXTargetDuration(value) => value.fmt(f), + Tag::ExtXMediaSequence(value) => value.fmt(f), + Tag::ExtXDiscontinuitySequence(value) => value.fmt(f), + Tag::ExtXEndList(value) => value.fmt(f), + Tag::ExtXPlaylistType(value) => value.fmt(f), + Tag::ExtXIFramesOnly(value) => value.fmt(f), + Tag::ExtXMedia(value) => value.fmt(f), + Tag::ExtXStreamInf(value) => value.fmt(f), + Tag::ExtXIFrameStreamInf(value) => value.fmt(f), + Tag::ExtXSessionData(value) => value.fmt(f), + Tag::ExtXSessionKey(value) => value.fmt(f), + Tag::ExtXIndependentSegments(value) => value.fmt(f), + Tag::ExtXStart(value) => value.fmt(f), + Tag::Unknown(value) => value.fmt(f), } } } + impl FromStr for Tag { type Err = Error; - fn from_str(s: &str) -> Result { + + fn from_str(s: &str) -> Result { if s.starts_with(tags::ExtM3u::PREFIX) { - track!(s.parse().map(Tag::ExtM3u)) + s.parse().map(Tag::ExtM3u) } else if s.starts_with(tags::ExtXVersion::PREFIX) { - track!(s.parse().map(Tag::ExtXVersion)) + s.parse().map(Tag::ExtXVersion) } else if s.starts_with(tags::ExtInf::PREFIX) { - track!(s.parse().map(Tag::ExtInf)) + s.parse().map(Tag::ExtInf) } else if s.starts_with(tags::ExtXByteRange::PREFIX) { - track!(s.parse().map(Tag::ExtXByteRange)) + s.parse().map(Tag::ExtXByteRange) } else if s.starts_with(tags::ExtXDiscontinuity::PREFIX) { - track!(s.parse().map(Tag::ExtXDiscontinuity)) + s.parse().map(Tag::ExtXDiscontinuity) } else if s.starts_with(tags::ExtXKey::PREFIX) { - track!(s.parse().map(Tag::ExtXKey)) + s.parse().map(Tag::ExtXKey) } else if s.starts_with(tags::ExtXMap::PREFIX) { - track!(s.parse().map(Tag::ExtXMap)) + s.parse().map(Tag::ExtXMap) } else if s.starts_with(tags::ExtXProgramDateTime::PREFIX) { - track!(s.parse().map(Tag::ExtXProgramDateTime)) + s.parse().map(Tag::ExtXProgramDateTime) } else if s.starts_with(tags::ExtXTargetDuration::PREFIX) { - track!(s.parse().map(Tag::ExtXTargetDuration)) + s.parse().map(Tag::ExtXTargetDuration) } else if s.starts_with(tags::ExtXDateRange::PREFIX) { - track!(s.parse().map(Tag::ExtXDateRange)) + s.parse().map(Tag::ExtXDateRange) } else if s.starts_with(tags::ExtXMediaSequence::PREFIX) { - track!(s.parse().map(Tag::ExtXMediaSequence)) + s.parse().map(Tag::ExtXMediaSequence) } else if s.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) { - track!(s.parse().map(Tag::ExtXDiscontinuitySequence)) + s.parse().map(Tag::ExtXDiscontinuitySequence) } else if s.starts_with(tags::ExtXEndList::PREFIX) { - track!(s.parse().map(Tag::ExtXEndList)) + s.parse().map(Tag::ExtXEndList) } else if s.starts_with(tags::ExtXPlaylistType::PREFIX) { - track!(s.parse().map(Tag::ExtXPlaylistType)) + s.parse().map(Tag::ExtXPlaylistType) } else if s.starts_with(tags::ExtXIFramesOnly::PREFIX) { - track!(s.parse().map(Tag::ExtXIFramesOnly)) + s.parse().map(Tag::ExtXIFramesOnly) } else if s.starts_with(tags::ExtXMedia::PREFIX) { - track!(s.parse().map(Tag::ExtXMedia)) + s.parse().map(Tag::ExtXMedia) } else if s.starts_with(tags::ExtXStreamInf::PREFIX) { - track!(s.parse().map(Tag::ExtXStreamInf)) + s.parse().map(Tag::ExtXStreamInf) } else if s.starts_with(tags::ExtXIFrameStreamInf::PREFIX) { - track!(s.parse().map(Tag::ExtXIFrameStreamInf)) + s.parse().map(Tag::ExtXIFrameStreamInf) } else if s.starts_with(tags::ExtXSessionData::PREFIX) { - track!(s.parse().map(Tag::ExtXSessionData)) + s.parse().map(Tag::ExtXSessionData) } else if s.starts_with(tags::ExtXSessionKey::PREFIX) { - track!(s.parse().map(Tag::ExtXSessionKey)) + s.parse().map(Tag::ExtXSessionKey) } else if s.starts_with(tags::ExtXIndependentSegments::PREFIX) { - track!(s.parse().map(Tag::ExtXIndependentSegments)) + s.parse().map(Tag::ExtXIndependentSegments) } else if s.starts_with(tags::ExtXStart::PREFIX) { - track!(s.parse().map(Tag::ExtXStart)) + s.parse().map(Tag::ExtXStart) } else { - track!(SingleLineString::new(s)).map(Tag::Unknown) + Ok(Tag::Unknown(s.to_string())) } } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index d33e0ff..418cdb6 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,251 +1,68 @@ -use crate::line::{Line, Lines, Tag}; -use crate::tags::{ - ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, - ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag, -}; -use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, QuotedString}; -use crate::{Error, ErrorKind, Result}; use std::collections::HashSet; use std::fmt; use std::iter; use std::str::FromStr; -/// Master playlist builder. -#[derive(Debug, Clone)] -pub struct MasterPlaylistBuilder { - version: Option, - independent_segments_tag: Option, - start_tag: Option, - media_tags: Vec, - stream_inf_tags: Vec, - i_frame_stream_inf_tags: Vec, - session_data_tags: Vec, - session_key_tags: Vec, -} -impl MasterPlaylistBuilder { - /// Makes a new `MasterPlaylistBuilder` instance. - pub fn new() -> Self { - MasterPlaylistBuilder { - version: None, - independent_segments_tag: None, - start_tag: None, - media_tags: Vec::new(), - stream_inf_tags: Vec::new(), - i_frame_stream_inf_tags: Vec::new(), - session_data_tags: Vec::new(), - session_key_tags: Vec::new(), - } - } +use derive_builder::Builder; - /// Sets the protocol compatibility version of the resulting playlist. - /// - /// If the resulting playlist has tags which requires a compatibility version greater than `version`, - /// `finish()` method will fail with an `ErrorKind::InvalidInput` error. - /// - /// The default is the maximum version among the tags in the playlist. - pub fn version(&mut self, version: ProtocolVersion) -> &mut Self { - self.version = Some(version); - self - } - - /// Adds the given tag to the resulting playlist. - /// - /// If it is forbidden to have multiple instance of the tag, the existing one will be overwritten. - pub fn tag>(&mut self, tag: T) -> &mut Self { - match tag.into() { - MasterPlaylistTag::ExtXIndependentSegments(t) => { - self.independent_segments_tag = Some(t); - } - MasterPlaylistTag::ExtXStart(t) => self.start_tag = Some(t), - MasterPlaylistTag::ExtXMedia(t) => self.media_tags.push(t), - MasterPlaylistTag::ExtXStreamInf(t) => self.stream_inf_tags.push(t), - MasterPlaylistTag::ExtXIFrameStreamInf(t) => self.i_frame_stream_inf_tags.push(t), - MasterPlaylistTag::ExtXSessionData(t) => self.session_data_tags.push(t), - MasterPlaylistTag::ExtXSessionKey(t) => self.session_key_tags.push(t), - } - self - } - - /// Builds a `MasterPlaylist` instance. - pub fn finish(self) -> Result { - let required_version = self.required_version(); - let specified_version = self.version.unwrap_or(required_version); - track_assert!( - required_version <= specified_version, - ErrorKind::InvalidInput, - "required_version:{}, specified_version:{}", - required_version, - specified_version, - ); - - track!(self.validate_stream_inf_tags())?; - track!(self.validate_i_frame_stream_inf_tags())?; - track!(self.validate_session_data_tags())?; - track!(self.validate_session_key_tags())?; - - Ok(MasterPlaylist { - version_tag: ExtXVersion::new(specified_version), - independent_segments_tag: self.independent_segments_tag, - start_tag: self.start_tag, - media_tags: self.media_tags, - stream_inf_tags: self.stream_inf_tags, - i_frame_stream_inf_tags: self.i_frame_stream_inf_tags, - session_data_tags: self.session_data_tags, - session_key_tags: self.session_key_tags, - }) - } - - fn required_version(&self) -> ProtocolVersion { - iter::empty() - .chain( - self.independent_segments_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.start_tag.iter().map(|t| t.requires_version())) - .chain(self.media_tags.iter().map(|t| t.requires_version())) - .chain(self.stream_inf_tags.iter().map(|t| t.requires_version())) - .chain( - self.i_frame_stream_inf_tags - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.session_data_tags.iter().map(|t| t.requires_version())) - .chain(self.session_key_tags.iter().map(|t| t.requires_version())) - .max() - .expect("Never fails") - } - - fn validate_stream_inf_tags(&self) -> Result<()> { - let mut has_none_closed_captions = false; - for t in &self.stream_inf_tags { - if let Some(group_id) = t.audio() { - track_assert!( - self.check_media_group(MediaType::Audio, group_id), - ErrorKind::InvalidInput, - "Unmatched audio group: {:?}", - group_id - ); - } - if let Some(group_id) = t.video() { - track_assert!( - self.check_media_group(MediaType::Video, group_id), - ErrorKind::InvalidInput, - "Unmatched video group: {:?}", - group_id - ); - } - if let Some(group_id) = t.subtitles() { - track_assert!( - self.check_media_group(MediaType::Subtitles, group_id), - ErrorKind::InvalidInput, - "Unmatched subtitles group: {:?}", - group_id - ); - } - match t.closed_captions() { - Some(&ClosedCaptions::GroupId(ref group_id)) => { - track_assert!( - self.check_media_group(MediaType::ClosedCaptions, group_id), - ErrorKind::InvalidInput, - "Unmatched closed-captions group: {:?}", - group_id - ); - } - Some(&ClosedCaptions::None) => { - has_none_closed_captions = true; - } - None => {} - } - } - if has_none_closed_captions { - track_assert!( - self.stream_inf_tags - .iter() - .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)), - ErrorKind::InvalidInput - ); - } - Ok(()) - } - - fn validate_i_frame_stream_inf_tags(&self) -> Result<()> { - for t in &self.i_frame_stream_inf_tags { - if let Some(group_id) = t.video() { - track_assert!( - self.check_media_group(MediaType::Video, group_id), - ErrorKind::InvalidInput, - "Unmatched video group: {:?}", - group_id - ); - } - } - Ok(()) - } - - fn validate_session_data_tags(&self) -> Result<()> { - let mut set = HashSet::new(); - for t in &self.session_data_tags { - track_assert!( - set.insert((t.data_id(), t.language())), - ErrorKind::InvalidInput, - "Conflict: {}", - t - ); - } - Ok(()) - } - - fn validate_session_key_tags(&self) -> Result<()> { - let mut set = HashSet::new(); - for t in &self.session_key_tags { - track_assert!( - set.insert(t.key()), - ErrorKind::InvalidInput, - "Conflict: {}", - t - ); - } - Ok(()) - } - - fn check_media_group(&self, media_type: MediaType, group_id: &QuotedString) -> bool { - self.media_tags - .iter() - .any(|t| t.media_type() == media_type && t.group_id() == group_id) - } -} -impl Default for MasterPlaylistBuilder { - fn default() -> Self { - Self::new() - } -} +use crate::line::{Line, Lines, Tag}; +use crate::tags::{ + ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, + ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, +}; +use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; +use crate::Error; /// Master playlist. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Builder)] +#[builder(build_fn(validate = "Self::validate"))] +#[builder(setter(into, strip_option))] pub struct MasterPlaylist { + #[builder(default, setter(name = "version"))] + /// Sets the protocol compatibility version of the resulting playlist. + /// + /// If the resulting playlist has tags which requires a compatibility version greater than + /// `version`, + /// `build()` method will fail with an `ErrorKind::InvalidInput` error. + /// + /// The default is the maximum version among the tags in the playlist. version_tag: ExtXVersion, + #[builder(default)] + /// Sets the [ExtXIndependentSegments] tag. independent_segments_tag: Option, + #[builder(default)] + /// Sets the [ExtXStart] tag. start_tag: Option, + /// Sets the [ExtXMedia] tag. media_tags: Vec, + /// Sets all [ExtXStreamInf]s. stream_inf_tags: Vec, + /// Sets all [ExtXIFrameStreamInf]s. i_frame_stream_inf_tags: Vec, + /// Sets all [ExtXSessionData]s. session_data_tags: Vec, + /// Sets all [ExtXSessionKey]s. session_key_tags: Vec, } + impl MasterPlaylist { + /// Returns a Builder for a MasterPlaylist. + pub fn builder() -> MasterPlaylistBuilder { + MasterPlaylistBuilder::default() + } + /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub fn version_tag(&self) -> ExtXVersion { + pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. - pub fn independent_segments_tag(&self) -> Option { + pub const fn independent_segments_tag(&self) -> Option { self.independent_segments_tag } /// Returns the `EXT-X-START` tag contained in the playlist. - pub fn start_tag(&self) -> Option { + pub const fn start_tag(&self) -> Option { self.start_tag } @@ -274,6 +91,156 @@ impl MasterPlaylist { &self.session_key_tags } } + +impl MasterPlaylistBuilder { + fn validate(&self) -> Result<(), String> { + let required_version = self.required_version(); + let specified_version = self + .version_tag + .unwrap_or(required_version.into()) + .version(); + + if required_version > specified_version { + return Err(Error::required_version(required_version, specified_version).to_string()); + } + + self.validate_stream_inf_tags().map_err(|e| e.to_string())?; + self.validate_i_frame_stream_inf_tags() + .map_err(|e| e.to_string())?; + self.validate_session_data_tags() + .map_err(|e| e.to_string())?; + + Ok(()) + } + + fn required_version(&self) -> ProtocolVersion { + iter::empty() + .chain( + self.independent_segments_tag + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.start_tag + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.media_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.stream_inf_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.i_frame_stream_inf_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.session_data_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .chain( + self.session_key_tags + .iter() + .map(|t| t.iter().map(|t| t.requires_version())) + .flatten(), + ) + .max() + .unwrap_or(ProtocolVersion::latest()) + } + + fn validate_stream_inf_tags(&self) -> crate::Result<()> { + if let Some(value) = &self.stream_inf_tags { + let mut has_none_closed_captions = false; + + for t in value { + if let Some(group_id) = t.audio() { + if !self.check_media_group(MediaType::Audio, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + if let Some(group_id) = t.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + if let Some(group_id) = t.subtitles() { + if !self.check_media_group(MediaType::Subtitles, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + match t.closed_captions() { + Some(&ClosedCaptions::GroupId(ref group_id)) => { + if !self.check_media_group(MediaType::ClosedCaptions, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + Some(&ClosedCaptions::None) => { + has_none_closed_captions = true; + } + None => {} + } + } + if has_none_closed_captions { + if !value + .iter() + .all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) + { + return Err(Error::invalid_input()); + } + } + } + Ok(()) + } + + fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> { + if let Some(value) = &self.i_frame_stream_inf_tags { + for t in value { + if let Some(group_id) = t.video() { + if !self.check_media_group(MediaType::Video, group_id) { + return Err(Error::unmatched_group(group_id)); + } + } + } + } + Ok(()) + } + + fn validate_session_data_tags(&self) -> crate::Result<()> { + let mut set = HashSet::new(); + if let Some(value) = &self.session_data_tags { + for t in value { + if !set.insert((t.data_id(), t.language())) { + 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 + .iter() + .any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string()) + } else { + false + } + } +} + impl fmt::Display for MasterPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; @@ -295,33 +262,42 @@ impl fmt::Display for MasterPlaylist { for t in &self.session_key_tags { writeln!(f, "{}", t)?; } - if let Some(ref t) = self.independent_segments_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.independent_segments_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.start_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.start_tag { + writeln!(f, "{}", value)?; } Ok(()) } } + impl FromStr for MasterPlaylist { type Err = Error; - fn from_str(s: &str) -> Result { - let mut builder = MasterPlaylistBuilder::new(); - for (i, line) in Lines::new(s).enumerate() { - match track!(line)? { - Line::Blank | Line::Comment(_) => {} + + fn from_str(input: &str) -> Result { + let mut builder = MasterPlaylist::builder(); + + let mut media_tags = vec![]; + let mut stream_inf_tags = vec![]; + let mut i_frame_stream_inf_tags = vec![]; + let mut session_data_tags = vec![]; + let mut session_key_tags = vec![]; + + for (i, line) in input.parse::()?.into_iter().enumerate() { + match line { Line::Tag(tag) => { if i == 0 { - track_assert_eq!(tag, Tag::ExtM3u(ExtM3u), ErrorKind::InvalidInput); + if tag != Tag::ExtM3u(ExtM3u) { + return Err(Error::invalid_input()); + } continue; } match tag { Tag::ExtM3u(_) => { - track_panic!(ErrorKind::InvalidInput); + return Err(Error::invalid_input()); } Tag::ExtXVersion(t) => { - track_assert_eq!(builder.version, None, ErrorKind::InvalidInput); builder.version(t.version()); } Tag::ExtInf(_) @@ -337,46 +313,92 @@ impl FromStr for MasterPlaylist { | Tag::ExtXEndList(_) | Tag::ExtXPlaylistType(_) | Tag::ExtXIFramesOnly(_) => { - track_panic!(ErrorKind::InvalidInput, "{}", tag) + return Err(Error::custom(format!( + "This tag isn't allowed in a master playlist: {}", + tag + ))); } Tag::ExtXMedia(t) => { - builder.tag(t); + media_tags.push(t); } Tag::ExtXStreamInf(t) => { - builder.tag(t); + stream_inf_tags.push(t); } Tag::ExtXIFrameStreamInf(t) => { - builder.tag(t); + i_frame_stream_inf_tags.push(t); } Tag::ExtXSessionData(t) => { - builder.tag(t); + session_data_tags.push(t); } Tag::ExtXSessionKey(t) => { - builder.tag(t); + session_key_tags.push(t); } Tag::ExtXIndependentSegments(t) => { - track_assert_eq!( - builder.independent_segments_tag, - None, - ErrorKind::InvalidInput - ); - builder.tag(t); + builder.independent_segments_tag(t); } Tag::ExtXStart(t) => { - track_assert_eq!(builder.start_tag, None, ErrorKind::InvalidInput); - builder.tag(t); + builder.start_tag(t); } - Tag::Unknown(_) => { + _ => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. + // TODO: collect custom tags } } } Line::Uri(uri) => { - track_panic!(ErrorKind::InvalidInput, "Unexpected URI: {:?}", uri); + return Err(Error::custom(format!("Unexpected URI: {:?}", uri))); } } } - track!(builder.finish()) + + builder.media_tags(media_tags); + builder.stream_inf_tags(stream_inf_tags); + builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags); + builder.session_data_tags(session_data_tags); + builder.session_key_tags(session_key_tags); + + builder.build().map_err(Error::builder_error) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parser() { + r#"#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/low/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/lo_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/hi_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/high/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" +http://example.com/audio/index.m3u8 +"# + .parse::() + .unwrap(); + } + + #[test] + fn test_display() { + let input = r#"#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/low/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/lo_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/hi_mid/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2" +http://example.com/high/index.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5" +http://example.com/audio/index.m3u8 +"#; + let playlist = input.parse::().unwrap(); + assert_eq!(playlist.to_string(), input); } } diff --git a/src/media_playlist.rs b/src/media_playlist.rs index a25d816..3f673c0 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -1,153 +1,137 @@ -use crate::line::{Line, Lines, Tag}; -use crate::media_segment::{MediaSegment, MediaSegmentBuilder}; -use crate::tags::{ - ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, - ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, - MediaPlaylistTag, -}; -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::iter; use std::str::FromStr; use std::time::Duration; -/// Media playlist builder. -#[derive(Debug, Clone)] -pub struct MediaPlaylistBuilder { - version: Option, - target_duration_tag: Option, - media_sequence_tag: Option, - discontinuity_sequence_tag: Option, - playlist_type_tag: Option, - i_frames_only_tag: Option, - independent_segments_tag: Option, - start_tag: Option, - end_list_tag: Option, - segments: Vec, - options: MediaPlaylistOptions, -} -impl MediaPlaylistBuilder { - /// Makes a new `MediaPlaylistBuilder` instance. - pub fn new() -> Self { - MediaPlaylistBuilder { - version: None, - target_duration_tag: None, - media_sequence_tag: None, - discontinuity_sequence_tag: None, - playlist_type_tag: None, - i_frames_only_tag: None, - independent_segments_tag: None, - start_tag: None, - end_list_tag: None, - segments: Vec::new(), - options: MediaPlaylistOptions::new(), - } - } +use derive_builder::Builder; +use crate::line::{Line, Lines, Tag}; +use crate::media_segment::MediaSegment; +use crate::tags::{ + ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, + ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, +}; +use crate::types::ProtocolVersion; +use crate::Error; + +/// Media playlist. +#[derive(Debug, Clone, Builder)] +#[builder(build_fn(validate = "Self::validate"))] +#[builder(setter(into, strip_option))] +pub struct MediaPlaylist { /// Sets the protocol compatibility version of the resulting playlist. /// - /// If the resulting playlist has tags which requires a compatibility version greater than `version`, - /// `finish()` method will fail with an `ErrorKind::InvalidInput` error. + /// If the resulting playlist has tags which requires a compatibility + /// version greater than `version`, + /// `build()` method will fail with an `ErrorKind::InvalidInput` error. /// /// The default is the maximum version among the tags in the playlist. - pub fn version(&mut self, version: ProtocolVersion) -> &mut Self { - self.version = Some(version); - self - } - - /// Sets the given tag to the resulting playlist. - pub fn tag>(&mut self, tag: T) -> &mut Self { - match tag.into() { - MediaPlaylistTag::ExtXTargetDuration(t) => self.target_duration_tag = Some(t), - MediaPlaylistTag::ExtXMediaSequence(t) => self.media_sequence_tag = Some(t), - MediaPlaylistTag::ExtXDiscontinuitySequence(t) => { - self.discontinuity_sequence_tag = Some(t) - } - MediaPlaylistTag::ExtXPlaylistType(t) => self.playlist_type_tag = Some(t), - MediaPlaylistTag::ExtXIFramesOnly(t) => self.i_frames_only_tag = Some(t), - MediaPlaylistTag::ExtXIndependentSegments(t) => self.independent_segments_tag = Some(t), - MediaPlaylistTag::ExtXStart(t) => self.start_tag = Some(t), - MediaPlaylistTag::ExtXEndList(t) => self.end_list_tag = Some(t), - } - self - } - - /// Adds a media segment to the resulting playlist. - pub fn segment(&mut self, segment: MediaSegment) -> &mut Self { - self.segments.push(segment); - self - } - - /// Sets the options that will be associated to the resulting playlist. + #[builder(setter(name = "version"))] + version_tag: ExtXVersion, + /// Sets the [ExtXTargetDuration] tag. + target_duration_tag: ExtXTargetDuration, + #[builder(default)] + /// Sets the [ExtXMediaSequence] tag. + media_sequence_tag: Option, + #[builder(default)] + /// Sets the [ExtXDiscontinuitySequence] tag. + discontinuity_sequence_tag: Option, + #[builder(default)] + /// Sets the [ExtXPlaylistType] tag. + playlist_type_tag: Option, + #[builder(default)] + /// Sets the [ExtXIFramesOnly] tag. + i_frames_only_tag: Option, + #[builder(default)] + /// Sets the [ExtXIndependentSegments] tag. + independent_segments_tag: Option, + #[builder(default)] + /// Sets the [ExtXStart] tag. + start_tag: Option, + #[builder(default)] + /// Sets the [ExtXEndList] tag. + end_list_tag: Option, + /// Sets all [MediaSegment]s. + segments: Vec, + /// Sets the allowable excess duration of each media segment in the associated playlist. /// - /// The default value is `MediaPlaylistOptions::default()`. - pub fn options(&mut self, options: MediaPlaylistOptions) -> &mut Self { - self.options = options; - self - } + /// # Error + /// If there is a media segment of which duration exceeds + /// `#EXT-X-TARGETDURATION + allowable_excess_duration`, + /// the invocation of `MediaPlaylistBuilder::build()` method will fail. + /// + /// The default value is `Duration::from_secs(0)`. + #[builder(default = "Duration::from_secs(0)")] + allowable_excess_duration: Duration, +} - /// Builds a `MediaPlaylist` instance. - pub fn finish(self) -> Result { +impl MediaPlaylistBuilder { + fn validate(&self) -> Result<(), String> { let required_version = self.required_version(); - let specified_version = self.version.unwrap_or(required_version); - track_assert!( - required_version <= specified_version, - ErrorKind::InvalidInput, - "required_version:{}, specified_version:{}", - required_version, - specified_version, - ); + let specified_version = self + .version_tag + .unwrap_or(required_version.into()) + .version(); - let target_duration_tag = - track_assert_some!(self.target_duration_tag, ErrorKind::InvalidInput); - track!(self.validate_media_segments(target_duration_tag.duration()))?; + if required_version > specified_version { + return Err(Error::custom(format!( + "required_version: {}, specified_version: {}", + required_version, specified_version + )) + .to_string()); + } - Ok(MediaPlaylist { - version_tag: ExtXVersion::new(specified_version), - target_duration_tag, - media_sequence_tag: self.media_sequence_tag, - discontinuity_sequence_tag: self.discontinuity_sequence_tag, - playlist_type_tag: self.playlist_type_tag, - i_frames_only_tag: self.i_frames_only_tag, - independent_segments_tag: self.independent_segments_tag, - start_tag: self.start_tag, - end_list_tag: self.end_list_tag, - segments: self.segments, - }) + if let Some(target_duration) = &self.target_duration_tag { + self.validate_media_segments(target_duration.duration()) + .map_err(|e| e.to_string())?; + } + + Ok(()) } - fn validate_media_segments(&self, target_duration: Duration) -> Result<()> { + fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> { let mut last_range_uri = None; - for s in &self.segments { - // CHECK: `#EXT-X-TARGETDURATION` - let segment_duration = s.inf_tag().duration(); - let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { - Duration::from_secs(segment_duration.as_secs()) - } else { - Duration::from_secs(segment_duration.as_secs() + 1) - }; - let max_segment_duration = target_duration + self.options.allowable_excess_duration; - track_assert!( - rounded_segment_duration <= max_segment_duration, - ErrorKind::InvalidInput, - "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", - segment_duration, - max_segment_duration, - target_duration, - s.uri() - ); - - // CHECK: `#EXT-X-BYTE-RANGE` - if let Some(tag) = s.byte_range_tag() { - if tag.range().start.is_none() { - let last_uri = track_assert_some!(last_range_uri, ErrorKind::InvalidInput); - track_assert_eq!(last_uri, s.uri(), ErrorKind::InvalidInput); + if let Some(segments) = &self.segments { + for s in segments { + // CHECK: `#EXT-X-TARGETDURATION` + let segment_duration = s.inf_tag().duration(); + let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { + Duration::from_secs(segment_duration.as_secs()) } else { - last_range_uri = Some(s.uri()); + Duration::from_secs(segment_duration.as_secs() + 1) + }; + + let max_segment_duration = { + if let Some(value) = &self.allowable_excess_duration { + target_duration + *value + } else { + target_duration + } + }; + + if !(rounded_segment_duration <= max_segment_duration) { + return Err(Error::custom(format!( + "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", + segment_duration, + max_segment_duration, + target_duration, + s.uri() + ))); + } + + // CHECK: `#EXT-X-BYTE-RANGE` + if let Some(tag) = s.byte_range_tag() { + if tag.to_range().start().is_none() { + let last_uri = last_range_uri.ok_or(Error::invalid_input())?; + if last_uri != s.uri() { + return Err(Error::invalid_input()); + } + } else { + last_range_uri = Some(s.uri()); + } + } else { + last_range_uri = None; } - } else { - last_range_uri = None; } } Ok(()) @@ -160,89 +144,128 @@ impl MediaPlaylistBuilder { .iter() .map(|t| t.requires_version()), ) - .chain(self.media_sequence_tag.iter().map(|t| t.requires_version())) - .chain( - self.discontinuity_sequence_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.playlist_type_tag.iter().map(|t| t.requires_version())) - .chain(self.i_frames_only_tag.iter().map(|t| t.requires_version())) - .chain( - self.independent_segments_tag - .iter() - .map(|t| t.requires_version()), - ) - .chain(self.start_tag.iter().map(|t| t.requires_version())) - .chain(self.end_list_tag.iter().map(|t| t.requires_version())) - .chain(self.segments.iter().map(|s| s.requires_version())) + .chain(self.media_sequence_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.discontinuity_sequence_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.playlist_type_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.i_frames_only_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.independent_segments_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.start_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.end_list_tag.iter().map(|t| { + if let Some(p) = t { + p.requires_version() + } else { + ProtocolVersion::V1 + } + })) + .chain(self.segments.iter().map(|t| { + t.iter() + .map(|s| s.requires_version()) + .max() + .unwrap_or(ProtocolVersion::V1) + })) .max() - .unwrap_or(ProtocolVersion::V1) + .unwrap_or(ProtocolVersion::latest()) } -} -impl Default for MediaPlaylistBuilder { - fn default() -> Self { - Self::new() + + /// Adds a media segment to the resulting playlist. + pub fn push_segment>(&mut self, value: VALUE) -> &mut Self { + if let Some(segments) = &mut self.segments { + segments.push(value.into()); + } else { + self.segments = Some(vec![value.into()]); + } + self + } + + /// Parse the rest of the [MediaPlaylist] from an m3u8 file. + pub fn parse(&mut self, input: &str) -> crate::Result { + parse_media_playlist(input, self) } } -/// Media playlist. -#[derive(Debug, Clone)] -pub struct MediaPlaylist { - version_tag: ExtXVersion, - target_duration_tag: ExtXTargetDuration, - media_sequence_tag: Option, - discontinuity_sequence_tag: Option, - playlist_type_tag: Option, - i_frames_only_tag: Option, - independent_segments_tag: Option, - start_tag: Option, - end_list_tag: Option, - segments: Vec, -} impl MediaPlaylist { + /// Creates a [MediaPlaylistBuilder]. + pub fn builder() -> MediaPlaylistBuilder { + MediaPlaylistBuilder::default() + } /// Returns the `EXT-X-VERSION` tag contained in the playlist. - pub fn version_tag(&self) -> ExtXVersion { + pub const fn version_tag(&self) -> ExtXVersion { self.version_tag } /// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist. - pub fn target_duration_tag(&self) -> ExtXTargetDuration { + pub const fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag } /// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist. - pub fn media_sequence_tag(&self) -> Option { + pub const fn media_sequence_tag(&self) -> Option { self.media_sequence_tag } /// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the playlist. - pub fn discontinuity_sequence_tag(&self) -> Option { + pub const fn discontinuity_sequence_tag(&self) -> Option { self.discontinuity_sequence_tag } /// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist. - pub fn playlist_type_tag(&self) -> Option { + pub const fn playlist_type_tag(&self) -> Option { self.playlist_type_tag } /// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist. - pub fn i_frames_only_tag(&self) -> Option { + pub const fn i_frames_only_tag(&self) -> Option { self.i_frames_only_tag } /// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist. - pub fn independent_segments_tag(&self) -> Option { + pub const fn independent_segments_tag(&self) -> Option { self.independent_segments_tag } /// Returns the `EXT-X-START` tag contained in the playlist. - pub fn start_tag(&self) -> Option { + pub const fn start_tag(&self) -> Option { self.start_tag } /// Returns the `EXT-X-ENDLIST` tag contained in the playlist. - pub fn end_list_tag(&self) -> Option { + pub const fn end_list_tag(&self) -> Option { self.end_list_tag } @@ -251,6 +274,7 @@ impl MediaPlaylist { &self.segments } } + impl fmt::Display for MediaPlaylist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; @@ -258,201 +282,156 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", self.version_tag)?; } writeln!(f, "{}", self.target_duration_tag)?; - if let Some(ref t) = self.media_sequence_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.media_sequence_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.discontinuity_sequence_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.discontinuity_sequence_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.playlist_type_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.playlist_type_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.i_frames_only_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.i_frames_only_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.independent_segments_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.independent_segments_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.start_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.start_tag { + writeln!(f, "{}", value)?; } for segment in &self.segments { write!(f, "{}", segment)?; } - if let Some(ref t) = self.end_list_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.end_list_tag { + writeln!(f, "{}", value)?; } Ok(()) } } -impl FromStr for MediaPlaylist { - type Err = Error; - fn from_str(s: &str) -> Result { - track!(MediaPlaylistOptions::new().parse(s)) - } -} -/// Media playlist options. -#[derive(Debug, Clone)] -pub struct MediaPlaylistOptions { - allowable_excess_duration: Duration, -} -impl MediaPlaylistOptions { - /// Makes a new `MediaPlaylistOptions` with the default settings. - pub fn new() -> Self { - MediaPlaylistOptions { - allowable_excess_duration: Duration::from_secs(0), - } - } +fn parse_media_playlist( + input: &str, + builder: &mut MediaPlaylistBuilder, +) -> crate::Result { + let mut segment = MediaSegment::builder(); + let mut segments = vec![]; - /// Sets the allowable excess duration of each media segment in the associated playlist. - /// - /// If there is a media segment of which duration exceeds - /// `#EXT-X-TARGETDURATION + allowable_excess_duration`, - /// the invocation of `MediaPlaylistBuilder::finish()` method will fail. - /// - /// The default value is `Duration::from_secs(0)`. - pub fn allowable_excess_segment_duration( - &mut self, - allowable_excess_duration: Duration, - ) -> &mut Self { - self.allowable_excess_duration = allowable_excess_duration; - self - } + let mut has_partial_segment = false; + let mut has_discontinuity_tag = false; + let mut has_version = false; // m3u8 files without ExtXVersion tags are ProtocolVersion::V1 - /// Parses the given M3U8 text with the specified settings. - pub fn parse(&self, m3u8: &str) -> Result { - let mut builder = MediaPlaylistBuilder::new(); - builder.options(self.clone()); - - let mut segment = MediaSegmentBuilder::new(); - let mut has_partial_segment = false; - let mut has_discontinuity_tag = false; - for (i, line) in Lines::new(m3u8).enumerate() { - match track!(line)? { - Line::Blank | Line::Comment(_) => {} - Line::Tag(tag) => { - if i == 0 { - track_assert_eq!(tag, Tag::ExtM3u(ExtM3u), ErrorKind::InvalidInput); - continue; - } - match tag { - Tag::ExtM3u(_) => track_panic!(ErrorKind::InvalidInput), - Tag::ExtXVersion(t) => { - track_assert_eq!(builder.version, None, ErrorKind::InvalidInput); - builder.version(t.version()); - } - Tag::ExtInf(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXByteRange(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXDiscontinuity(t) => { - has_discontinuity_tag = true; - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXKey(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXMap(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXProgramDateTime(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXDateRange(t) => { - has_partial_segment = true; - segment.tag(t); - } - Tag::ExtXTargetDuration(t) => { - track_assert_eq!( - builder.target_duration_tag, - None, - ErrorKind::InvalidInput - ); - builder.tag(t); - } - Tag::ExtXMediaSequence(t) => { - track_assert_eq!( - builder.media_sequence_tag, - None, - ErrorKind::InvalidInput - ); - track_assert!(builder.segments.is_empty(), ErrorKind::InvalidInput); - builder.tag(t); - } - Tag::ExtXDiscontinuitySequence(t) => { - track_assert!(builder.segments.is_empty(), ErrorKind::InvalidInput); - track_assert!(!has_discontinuity_tag, ErrorKind::InvalidInput); - builder.tag(t); - } - Tag::ExtXEndList(t) => { - track_assert_eq!(builder.end_list_tag, None, ErrorKind::InvalidInput); - builder.tag(t); - } - Tag::ExtXPlaylistType(t) => { - track_assert_eq!( - builder.playlist_type_tag, - None, - ErrorKind::InvalidInput - ); - builder.tag(t); - } - Tag::ExtXIFramesOnly(t) => { - track_assert_eq!( - builder.i_frames_only_tag, - None, - ErrorKind::InvalidInput - ); - builder.tag(t); - } - Tag::ExtXMedia(_) - | Tag::ExtXStreamInf(_) - | Tag::ExtXIFrameStreamInf(_) - | Tag::ExtXSessionData(_) - | Tag::ExtXSessionKey(_) => { - track_panic!(ErrorKind::InvalidInput, "{}", tag) - } - Tag::ExtXIndependentSegments(t) => { - track_assert_eq!( - builder.independent_segments_tag, - None, - ErrorKind::InvalidInput - ); - builder.tag(t); - } - Tag::ExtXStart(t) => { - track_assert_eq!(builder.start_tag, None, ErrorKind::InvalidInput); - builder.tag(t); - } - Tag::Unknown(_) => { - // [6.3.1. General Client Responsibilities] - // > ignore any unrecognized tags. - } + for (i, line) in input.parse::()?.into_iter().enumerate() { + match line { + Line::Tag(tag) => { + if i == 0 { + if tag != Tag::ExtM3u(ExtM3u) { + return Err(Error::custom("m3u8 doesn't start with #EXTM3U")); } + continue; } - Line::Uri(uri) => { - segment.uri(uri); - builder.segment(track!(segment.finish())?); - segment = MediaSegmentBuilder::new(); - has_partial_segment = false; + match tag { + Tag::ExtM3u(_) => return Err(Error::invalid_input()), + Tag::ExtXVersion(t) => { + builder.version(t.version()); + has_version = true; + } + Tag::ExtInf(t) => { + has_partial_segment = true; + segment.inf_tag(t); + } + Tag::ExtXByteRange(t) => { + has_partial_segment = true; + segment.byte_range_tag(t); + } + Tag::ExtXDiscontinuity(t) => { + has_discontinuity_tag = true; + has_partial_segment = true; + segment.discontinuity_tag(t); + } + Tag::ExtXKey(t) => { + has_partial_segment = true; + segment.push_key_tag(t); + } + Tag::ExtXMap(t) => { + has_partial_segment = true; + segment.map_tag(t); + } + Tag::ExtXProgramDateTime(t) => { + has_partial_segment = true; + segment.program_date_time_tag(t); + } + Tag::ExtXDateRange(t) => { + has_partial_segment = true; + segment.date_range_tag(t); + } + Tag::ExtXTargetDuration(t) => { + builder.target_duration_tag(t); + } + Tag::ExtXMediaSequence(t) => { + builder.media_sequence_tag(t); + } + Tag::ExtXDiscontinuitySequence(t) => { + if segments.is_empty() { + return Err(Error::invalid_input()); + } + if has_discontinuity_tag { + return Err(Error::invalid_input()); + } + builder.discontinuity_sequence_tag(t); + } + Tag::ExtXEndList(t) => { + builder.end_list_tag(t); + } + Tag::ExtXPlaylistType(t) => { + builder.playlist_type_tag(t); + } + Tag::ExtXIFramesOnly(t) => { + builder.i_frames_only_tag(t); + } + Tag::ExtXMedia(_) + | Tag::ExtXStreamInf(_) + | Tag::ExtXIFrameStreamInf(_) + | Tag::ExtXSessionData(_) + | Tag::ExtXSessionKey(_) => { + return Err(Error::custom(tag)); + } + Tag::ExtXIndependentSegments(t) => { + builder.independent_segments_tag(t); + } + Tag::ExtXStart(t) => { + builder.start_tag(t); + } + Tag::Unknown(_) => { + // [6.3.1. General Client Responsibilities] + // > ignore any unrecognized tags. + } } } + Line::Uri(uri) => { + segment.uri(uri); + segments.push(segment.build().map_err(Error::builder_error)?); + segment = MediaSegment::builder(); + has_partial_segment = false; + } } - track_assert!(!has_partial_segment, ErrorKind::InvalidInput); - track!(builder.finish()) } + if has_partial_segment { + return Err(Error::invalid_input()); + } + if !has_version { + builder.version(ProtocolVersion::V1); + } + + builder.segments(segments); + builder.build().map_err(Error::builder_error) } -impl Default for MediaPlaylistOptions { - fn default() -> Self { - Self::new() + +impl FromStr for MediaPlaylist { + type Err = Error; + + fn from_str(input: &str) -> Result { + parse_media_playlist(input, &mut Self::builder()) } } @@ -462,36 +441,37 @@ mod tests { #[test] fn too_large_segment_duration_test() { - let m3u8 = "#EXTM3U\n\ - #EXT-X-TARGETDURATION:8\n\ - #EXT-X-VERSION:3\n\ - #EXTINF:9.009,\n\ - http://media.example.com/first.ts\n\ - #EXTINF:9.509,\n\ - http://media.example.com/second.ts\n\ - #EXTINF:3.003,\n\ - http://media.example.com/third.ts\n\ - #EXT-X-ENDLIST"; + let playlist = r#" + #EXTM3U + #EXT-X-TARGETDURATION:8 + #EXT-X-VERSION:3 + #EXTINF:9.009, + http://media.example.com/first.ts + #EXTINF:9.509, + http://media.example.com/second.ts + #EXTINF:3.003, + http://media.example.com/third.ts + #EXT-X-ENDLIST"#; // Error (allowable segment duration = target duration = 8) - assert!(m3u8.parse::().is_err()); + assert!(playlist.parse::().is_err()); // Error (allowable segment duration = 9) - assert!(MediaPlaylistOptions::new() - .allowable_excess_segment_duration(Duration::from_secs(1)) - .parse(m3u8) + assert!(MediaPlaylist::builder() + .allowable_excess_duration(Duration::from_secs(1)) + .parse(playlist) .is_err()); // Ok (allowable segment duration = 10) - assert!(MediaPlaylistOptions::new() - .allowable_excess_segment_duration(Duration::from_secs(2)) - .parse(m3u8) - .is_ok()); + MediaPlaylist::builder() + .allowable_excess_duration(Duration::from_secs(2)) + .parse(playlist) + .unwrap(); } #[test] - fn empty_m3u8_parse_test() { - let m3u8 = ""; - assert!(m3u8.parse::().is_err()); + fn test_empty_playlist() { + let playlist = ""; + assert!(playlist.parse::().is_err()); } } diff --git a/src/media_segment.rs b/src/media_segment.rs index 7794819..d637131 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -1,131 +1,97 @@ -use crate::tags::{ - ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, - MediaSegmentTag, -}; -use crate::types::{ProtocolVersion, SingleLineString}; -use crate::{ErrorKind, Result}; use std::fmt; use std::iter; -/// Media segment builder. -#[derive(Debug, Clone)] -pub struct MediaSegmentBuilder { - key_tags: Vec, - map_tag: Option, - byte_range_tag: Option, - date_range_tag: Option, - discontinuity_tag: Option, - program_date_time_tag: Option, - inf_tag: Option, - uri: Option, -} -impl MediaSegmentBuilder { - /// Makes a new `MediaSegmentBuilder` instance. - pub fn new() -> Self { - MediaSegmentBuilder { - key_tags: Vec::new(), - map_tag: None, - byte_range_tag: None, - date_range_tag: None, - discontinuity_tag: None, - program_date_time_tag: None, - inf_tag: None, - uri: None, - } - } +use derive_builder::Builder; +use url::Url; - /// Sets the URI of the resulting media segment. - pub fn uri(&mut self, uri: SingleLineString) -> &mut Self { - self.uri = Some(uri); - self - } - - /// Sets the given tag to the resulting media segment. - pub fn tag>(&mut self, tag: T) -> &mut Self { - match tag.into() { - MediaSegmentTag::ExtInf(t) => self.inf_tag = Some(t), - MediaSegmentTag::ExtXByteRange(t) => self.byte_range_tag = Some(t), - MediaSegmentTag::ExtXDateRange(t) => self.date_range_tag = Some(t), - MediaSegmentTag::ExtXDiscontinuity(t) => self.discontinuity_tag = Some(t), - MediaSegmentTag::ExtXKey(t) => self.key_tags.push(t), - MediaSegmentTag::ExtXMap(t) => self.map_tag = Some(t), - MediaSegmentTag::ExtXProgramDateTime(t) => self.program_date_time_tag = Some(t), - } - self - } - - /// Builds a `MediaSegment` instance. - pub fn finish(self) -> Result { - let uri = track_assert_some!(self.uri, ErrorKind::InvalidInput); - let inf_tag = track_assert_some!(self.inf_tag, ErrorKind::InvalidInput); - Ok(MediaSegment { - key_tags: self.key_tags, - map_tag: self.map_tag, - byte_range_tag: self.byte_range_tag, - date_range_tag: self.date_range_tag, - discontinuity_tag: self.discontinuity_tag, - program_date_time_tag: self.program_date_time_tag, - inf_tag, - uri, - }) - } -} -impl Default for MediaSegmentBuilder { - fn default() -> Self { - Self::new() - } -} +use crate::tags::{ + ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime, +}; +use crate::types::ProtocolVersion; /// Media segment. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Builder)] +#[builder(setter(into, strip_option))] pub struct MediaSegment { + #[builder(default)] + /// Sets all [ExtXKey] tags. key_tags: Vec, + #[builder(default)] + /// Sets an [ExtXMap] tag. map_tag: Option, + #[builder(default)] + /// Sets an [ExtXByteRange] tag. byte_range_tag: Option, + #[builder(default)] + /// Sets an [ExtXDateRange] tag. date_range_tag: Option, + #[builder(default)] + /// Sets an [ExtXDiscontinuity] tag. discontinuity_tag: Option, + #[builder(default)] + /// Sets an [ExtXProgramDateTime] tag. program_date_time_tag: Option, + /// Sets an [ExtInf] tag. inf_tag: ExtInf, - uri: SingleLineString, + /// Sets an Uri. + uri: Url, } + +impl MediaSegmentBuilder { + /// Pushes an [ExtXKey] tag. + pub fn push_key_tag>(&mut self, value: VALUE) -> &mut Self { + if let Some(key_tags) = &mut self.key_tags { + key_tags.push(value.into()); + } else { + self.key_tags = Some(vec![value.into()]); + } + self + } +} + impl fmt::Display for MediaSegment { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for t in &self.key_tags { - writeln!(f, "{}", t)?; + for value in &self.key_tags { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.map_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.map_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.byte_range_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.byte_range_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.date_range_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.date_range_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.discontinuity_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.discontinuity_tag { + writeln!(f, "{}", value)?; } - if let Some(ref t) = self.program_date_time_tag { - writeln!(f, "{}", t)?; + if let Some(value) = &self.program_date_time_tag { + writeln!(f, "{}", value)?; } writeln!(f, "{},", self.inf_tag)?; writeln!(f, "{}", self.uri)?; Ok(()) } } + impl MediaSegment { + /// Creates a [MediaSegmentBuilder]. + pub fn builder() -> MediaSegmentBuilder { + MediaSegmentBuilder::default() + } /// Returns the URI of the media segment. - pub fn uri(&self) -> &SingleLineString { + pub const fn uri(&self) -> &Url { &self.uri } /// Returns the `EXT-X-INF` tag associated with the media segment. - pub fn inf_tag(&self) -> &ExtInf { + pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag } /// Returns the `EXT-X-BYTERANGE` tag associated with the media segment. - pub fn byte_range_tag(&self) -> Option { + pub const fn byte_range_tag(&self) -> Option { self.byte_range_tag } @@ -135,7 +101,7 @@ impl MediaSegment { } /// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment. - pub fn discontinuity_tag(&self) -> Option { + pub const fn discontinuity_tag(&self) -> Option { self.discontinuity_tag } @@ -169,6 +135,6 @@ impl MediaSegment { ) .chain(iter::once(self.inf_tag.requires_version())) .max() - .expect("Never fails") + .unwrap_or(ProtocolVersion::V7) } } diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 5d44b90..07f95ec 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -1,30 +1,36 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.1.1. EXTM3U] /// /// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ExtM3u; + impl ExtM3u { pub(crate) const PREFIX: &'static str = "#EXTM3U"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } + impl fmt::Display for ExtM3u { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } + impl FromStr for ExtM3u { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtM3u) } } @@ -34,9 +40,17 @@ mod test { use super::*; #[test] - fn extm3u() { + fn test_display() { + assert_eq!(ExtM3u.to_string(), "#EXTM3U".to_string()); + } + + #[test] + fn test_parser() { assert_eq!("#EXTM3U".parse::().ok(), Some(ExtM3u)); - assert_eq!(ExtM3u.to_string(), "#EXTM3U"); + } + + #[test] + fn test_requires_version() { assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1); } } diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index f0ab35f..5f196db 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -1,48 +1,59 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.1.2. EXT-X-VERSION] /// /// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXVersion { - version: ProtocolVersion, -} +pub struct ExtXVersion(ProtocolVersion); impl ExtXVersion { pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:"; /// Makes a new `ExtXVersion` tag. - pub fn new(version: ProtocolVersion) -> Self { - ExtXVersion { version } + pub const fn new(version: ProtocolVersion) -> Self { + Self(version) } /// Returns the protocol compatibility version of the playlist containing this tag. - pub fn version(self) -> ProtocolVersion { - self.version + pub const fn version(&self) -> ProtocolVersion { + self.0 } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.version) + write!(f, "{}{}", Self::PREFIX, self.0) + } +} + +impl Default for ExtXVersion { + fn default() -> Self { + Self(ProtocolVersion::V1) + } +} + +impl From for ExtXVersion { + fn from(value: ProtocolVersion) -> Self { + Self(value) } } impl FromStr for ExtXVersion { 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; - let version = track!(suffix.parse())?; - Ok(ExtXVersion { version }) + + fn from_str(input: &str) -> Result { + let version = tag(input, Self::PREFIX)?.parse()?; + Ok(ExtXVersion::new(version)) } } @@ -51,11 +62,34 @@ mod test { use super::*; #[test] - fn ext_x_version() { - let tag = ExtXVersion::new(ProtocolVersion::V6); - assert_eq!("#EXT-X-VERSION:6".parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), "#EXT-X-VERSION:6"); - assert_eq!(tag.version(), ProtocolVersion::V6); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_display() { + assert_eq!( + ExtXVersion::new(ProtocolVersion::V6).to_string(), + "#EXT-X-VERSION:6" + ); + } + + #[test] + fn test_parser() { + assert_eq!( + "#EXT-X-VERSION:6".parse().ok(), + Some(ExtXVersion::new(ProtocolVersion::V6)) + ); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ExtXVersion::new(ProtocolVersion::V6).requires_version(), + ProtocolVersion::V1 + ); + } + + #[test] + fn test_version() { + assert_eq!( + ExtXVersion::new(ProtocolVersion::V6).version(), + ProtocolVersion::V6 + ); } } diff --git a/src/tags/master_playlist/i_frame_stream_inf.rs b/src/tags/master_playlist/i_frame_stream_inf.rs index 3850e47..1567c07 100644 --- a/src/tags/master_playlist/i_frame_stream_inf.rs +++ b/src/tags/master_playlist/i_frame_stream_inf.rs @@ -1,31 +1,60 @@ -use crate::attribute::AttributePairs; -use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion, QuotedString}; -use crate::utils::parse_u64; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use getset::{Getters, MutGetters, Setters}; + +use crate::attribute::AttributePairs; +use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion}; +use crate::utils::parse_u64; +use crate::utils::{quote, tag, unquote}; +use crate::Error; + /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] /// /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Getters, Setters, MutGetters, Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXIFrameStreamInf { - uri: QuotedString, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] + /// The URI, that identifies the associated media playlist. + uri: String, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] + /// The peak segment bit rate of the variant stream. bandwidth: u64, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] + /// The average segment bit rate of the variant stream. average_bandwidth: Option, - codecs: Option, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] + /// A string that represents the list of codec types contained the variant stream. + codecs: Option, + /// The optimal pixel resolution at which to display all the video in the variant stream. resolution: Option, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] + /// The HDCP level of the variant stream. hdcp_level: Option, - video: Option, + #[get = "pub"] + #[set = "pub"] + #[get_mut = "pub"] + /// The group identifier for the video in the variant stream. + video: Option, } impl ExtXIFrameStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; /// Makes a new `ExtXIFrameStreamInf` tag. - pub fn new(uri: QuotedString, bandwidth: u64) -> Self { + pub fn new(uri: T, bandwidth: u64) -> Self { ExtXIFrameStreamInf { - uri, + uri: uri.to_string(), bandwidth, average_bandwidth: None, codecs: None, @@ -35,43 +64,27 @@ impl ExtXIFrameStreamInf { } } - /// Returns the URI that identifies the associated media playlist. - pub fn uri(&self) -> &QuotedString { - &self.uri + /// The optimal pixel resolution at which to display all the video in the variant stream. + pub fn resolution(&self) -> Option<(usize, usize)> { + if let Some(res) = &self.resolution { + Some((res.width(), res.height())) + } else { + None + } } - /// Returns the peak segment bit rate of the variant stream. - pub fn bandwidth(&self) -> u64 { - self.bandwidth + /// Sets the optimal pixel resolution at which to display all the video in the variant stream. + pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self { + if let Some(res) = &mut self.resolution { + res.set_width(width); + res.set_height(height); + } else { + self.resolution = Some(DecimalResolution::new(width, height)); + } + self } - - /// Returns the average segment bit rate of the variant stream. - pub fn average_bandwidth(&self) -> Option { - self.average_bandwidth - } - - /// Returns a string that represents the list of codec types contained the variant stream. - pub fn codecs(&self) -> Option<&QuotedString> { - self.codecs.as_ref() - } - - /// Returns the optimal pixel resolution at which to display all the video in the variant stream. - pub fn resolution(&self) -> Option { - self.resolution - } - - /// Returns the HDCP level of the variant stream. - pub fn hdcp_level(&self) -> Option { - self.hdcp_level - } - - /// Returns the group identifier for the video in the variant stream. - pub fn video(&self) -> Option<&QuotedString> { - self.video.as_ref() - } - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -79,22 +92,23 @@ impl ExtXIFrameStreamInf { impl fmt::Display for ExtXIFrameStreamInf { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - write!(f, "URI={}", self.uri)?; + write!(f, "URI={}", quote(&self.uri))?; write!(f, ",BANDWIDTH={}", self.bandwidth)?; - if let Some(ref x) = self.average_bandwidth { - write!(f, ",AVERAGE-BANDWIDTH={}", x)?; + + if let Some(value) = &self.average_bandwidth { + write!(f, ",AVERAGE-BANDWIDTH={}", value)?; } - if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", x)?; + if let Some(value) = &self.codecs { + write!(f, ",CODECS={}", quote(value))?; } - if let Some(ref x) = self.resolution { - write!(f, ",RESOLUTION={}", x)?; + if let Some(value) = &self.resolution { + write!(f, ",RESOLUTION={}", value)?; } - if let Some(ref x) = self.hdcp_level { - write!(f, ",HDCP-LEVEL={}", x)?; + if let Some(value) = &self.hdcp_level { + write!(f, ",HDCP-LEVEL={}", value)?; } - if let Some(ref x) = self.video { - write!(f, ",VIDEO={}", x)?; + if let Some(value) = &self.video { + write!(f, ",VIDEO={}", quote(value))?; } Ok(()) } @@ -102,8 +116,9 @@ impl fmt::Display for ExtXIFrameStreamInf { impl FromStr for ExtXIFrameStreamInf { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut uri = None; let mut bandwidth = None; @@ -112,17 +127,16 @@ impl FromStr for ExtXIFrameStreamInf { let mut resolution = None; let mut hdcp_level = None; let mut video = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "URI" => uri = Some(track!(value.parse())?), - "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), - "CODECS" => codecs = Some(track!(value.parse())?), - "RESOLUTION" => resolution = Some(track!(value.parse())?), - "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), - "VIDEO" => video = Some(track!(value.parse())?), + + for (key, value) in input.parse::()? { + match key.as_str() { + "URI" => uri = Some(unquote(value)), + "BANDWIDTH" => bandwidth = Some(parse_u64(value)?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some(parse_u64(value)?), + "CODECS" => codecs = Some(unquote(value)), + "RESOLUTION" => resolution = Some(value.parse()?), + "HDCP-LEVEL" => hdcp_level = Some(value.parse()?), + "VIDEO" => video = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -130,8 +144,9 @@ impl FromStr for ExtXIFrameStreamInf { } } - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); - let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); + let uri = uri.ok_or(Error::missing_value("URI"))?; + let bandwidth = bandwidth.ok_or(Error::missing_value("BANDWIDTH"))?; + Ok(ExtXIFrameStreamInf { uri, bandwidth, @@ -149,15 +164,30 @@ mod test { use super::*; #[test] - fn ext_x_i_frame_stream_inf() { - let tag = ExtXIFrameStreamInf::new(quoted_string("foo"), 1000); + fn test_display() { let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + assert_eq!(ExtXIFrameStreamInf::new("foo", 1000).to_string(), text); } - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() + #[test] + fn test_parser() { + let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; + let i_frame_stream_inf = ExtXIFrameStreamInf::new("foo", 1000); + assert_eq!( + text.parse::().unwrap(), + i_frame_stream_inf.clone() + ); + + assert_eq!(i_frame_stream_inf.uri(), "foo"); + assert_eq!(*i_frame_stream_inf.bandwidth(), 1000); + // TODO: test all the optional fields + } + + #[test] + fn test_requires_version() { + assert_eq!( + ExtXIFrameStreamInf::new("foo", 1000).requires_version(), + ProtocolVersion::V1 + ); } } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index 82ee440..819f62b 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -1,30 +1,31 @@ -use crate::attribute::AttributePairs; -use crate::types::{InStreamId, MediaType, ProtocolVersion, QuotedString}; -use crate::utils::parse_yes_or_no; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::attribute::AttributePairs; +use crate::types::{InStreamId, MediaType, ProtocolVersion}; +use crate::utils::{parse_yes_or_no, quote, tag, unquote}; +use crate::Error; + /// `ExtXMedia` builder. #[derive(Debug, Clone)] pub struct ExtXMediaBuilder { media_type: Option, - uri: Option, - group_id: Option, - language: Option, - assoc_language: Option, - name: Option, + uri: Option, + group_id: Option, + language: Option, + assoc_language: Option, + name: Option, default: bool, autoselect: Option, forced: Option, instream_id: Option, - characteristics: Option, - channels: Option, + characteristics: Option, + channels: Option, } impl ExtXMediaBuilder { /// Makes a `ExtXMediaBuilder` instance. - pub fn new() -> Self { + pub const fn new() -> Self { ExtXMediaBuilder { media_type: None, uri: None, @@ -48,32 +49,32 @@ impl ExtXMediaBuilder { } /// Sets the identifier that specifies the group to which the rendition belongs. - pub fn group_id(&mut self, group_id: QuotedString) -> &mut Self { - self.group_id = Some(group_id); + pub fn group_id(&mut self, group_id: T) -> &mut Self { + self.group_id = Some(group_id.to_string()); self } /// Sets a human-readable description of the rendition. - pub fn name(&mut self, name: QuotedString) -> &mut Self { - self.name = Some(name); + pub fn name(&mut self, name: T) -> &mut Self { + self.name = Some(name.to_string()); self } /// Sets the URI that identifies the media playlist. - pub fn uri(&mut self, uri: QuotedString) -> &mut Self { - self.uri = Some(uri); + pub fn uri(&mut self, uri: T) -> &mut Self { + self.uri = Some(uri.to_string()); self } /// Sets the name of the primary language used in the rendition. - pub fn language(&mut self, language: QuotedString) -> &mut Self { - self.language = Some(language); + pub fn language(&mut self, language: T) -> &mut Self { + self.language = Some(language.to_string()); self } /// Sets the name of a language associated with the rendition. - pub fn assoc_language(&mut self, language: QuotedString) -> &mut Self { - self.assoc_language = Some(language); + pub fn assoc_language(&mut self, language: T) -> &mut Self { + self.assoc_language = Some(language.to_string()); self } @@ -102,34 +103,51 @@ impl ExtXMediaBuilder { } /// Sets the string that represents uniform type identifiers (UTI). - pub fn characteristics(&mut self, characteristics: QuotedString) -> &mut Self { - self.characteristics = Some(characteristics); + pub fn characteristics(&mut self, characteristics: T) -> &mut Self { + self.characteristics = Some(characteristics.to_string()); self } /// Sets the string that represents the parameters of the rendition. - pub fn channels(&mut self, channels: QuotedString) -> &mut Self { - self.channels = Some(channels); + pub fn channels(&mut self, channels: T) -> &mut Self { + self.channels = Some(channels.to_string()); self } /// Builds a `ExtXMedia` instance. - pub fn finish(self) -> Result { - let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput); - let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput); - let name = track_assert_some!(self.name, ErrorKind::InvalidInput); + pub fn finish(self) -> crate::Result { + let media_type = self + .media_type + .ok_or(Error::missing_value("self.media_type"))?; + let group_id = self.group_id.ok_or(Error::missing_value("self.group_id"))?; + let name = self.name.ok_or(Error::missing_value("self.name"))?; + if MediaType::ClosedCaptions == media_type { - track_assert_ne!(self.uri, None, ErrorKind::InvalidInput); - track_assert!(self.instream_id.is_some(), ErrorKind::InvalidInput); + if let None = self.uri { + return Err(Error::missing_value("self.uri")); + } + self.instream_id + .ok_or(Error::missing_value("self.instream_id"))?; } else { - track_assert!(self.instream_id.is_none(), ErrorKind::InvalidInput); + if let Some(_) = &self.instream_id { + Err(Error::invalid_input())?; + } } + if self.default && self.autoselect.is_some() { - track_assert_eq!(self.autoselect, Some(true), ErrorKind::InvalidInput); + if let Some(value) = &self.autoselect { + if *value { + Err(Error::invalid_input())?; + } + } } + if MediaType::Subtitles != media_type { - track_assert_eq!(self.forced, None, ErrorKind::InvalidInput); + if self.forced.is_some() { + Err(Error::invalid_input())?; + } } + Ok(ExtXMedia { media_type, uri: self.uri, @@ -159,31 +177,31 @@ impl Default for ExtXMediaBuilder { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ExtXMedia { media_type: MediaType, - uri: Option, - group_id: QuotedString, - language: Option, - assoc_language: Option, - name: QuotedString, + uri: Option, + group_id: String, + language: Option, + assoc_language: Option, + name: String, default: bool, autoselect: bool, forced: bool, instream_id: Option, - characteristics: Option, - channels: Option, + characteristics: Option, + channels: Option, } impl ExtXMedia { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; /// Makes a new `ExtXMedia` tag. - pub fn new(media_type: MediaType, group_id: QuotedString, name: QuotedString) -> Self { + pub fn new(media_type: MediaType, group_id: T, name: T) -> Self { ExtXMedia { media_type, uri: None, - group_id, + group_id: group_id.to_string(), language: None, assoc_language: None, - name, + name: name.to_string(), default: false, autoselect: false, forced: false, @@ -194,65 +212,65 @@ impl ExtXMedia { } /// Returns the type of the media associated with this tag. - pub fn media_type(&self) -> MediaType { + pub const fn media_type(&self) -> MediaType { self.media_type } /// Returns the identifier that specifies the group to which the rendition belongs. - pub fn group_id(&self) -> &QuotedString { + pub const fn group_id(&self) -> &String { &self.group_id } /// Returns a human-readable description of the rendition. - pub fn name(&self) -> &QuotedString { + pub const fn name(&self) -> &String { &self.name } /// Returns the URI that identifies the media playlist. - pub fn uri(&self) -> Option<&QuotedString> { + pub fn uri(&self) -> Option<&String> { self.uri.as_ref() } /// Returns the name of the primary language used in the rendition. - pub fn language(&self) -> Option<&QuotedString> { + pub fn language(&self) -> Option<&String> { self.language.as_ref() } /// Returns the name of a language associated with the rendition. - pub fn assoc_language(&self) -> Option<&QuotedString> { + pub fn assoc_language(&self) -> Option<&String> { self.assoc_language.as_ref() } /// Returns whether this is the default rendition. - pub fn default(&self) -> bool { + pub const fn default(&self) -> bool { self.default } /// Returns whether the client may choose to /// play this rendition in the absence of explicit user preference. - pub fn autoselect(&self) -> bool { + pub const fn autoselect(&self) -> bool { self.autoselect } /// Returns whether the rendition contains content that is considered essential to play. - pub fn forced(&self) -> bool { + pub const fn forced(&self) -> bool { self.forced } /// Returns the identifier that specifies a rendition within the segments in the media playlist. - pub fn instream_id(&self) -> Option { + pub const fn instream_id(&self) -> Option { self.instream_id } /// Returns a string that represents uniform type identifiers (UTI). /// /// Each UTI indicates an individual characteristic of the rendition. - pub fn characteristics(&self) -> Option<&QuotedString> { + pub fn characteristics(&self) -> Option<&String> { self.characteristics.as_ref() } /// Returns a string that represents the parameters of the rendition. - pub fn channels(&self) -> Option<&QuotedString> { + pub fn channels(&self) -> Option<&String> { self.channels.as_ref() } @@ -273,17 +291,17 @@ impl fmt::Display for ExtXMedia { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "TYPE={}", self.media_type)?; - if let Some(ref x) = self.uri { - write!(f, ",URI={}", x)?; + if let Some(value) = &self.uri { + write!(f, ",URI={}", quote(value))?; } - write!(f, ",GROUP-ID={}", self.group_id)?; - if let Some(ref x) = self.language { - write!(f, ",LANGUAGE={}", x)?; + write!(f, ",GROUP-ID={}", quote(&self.group_id))?; + if let Some(value) = &self.language { + write!(f, ",LANGUAGE={}", quote(value))?; } - if let Some(ref x) = self.assoc_language { - write!(f, ",ASSOC-LANGUAGE={}", x)?; + if let Some(value) = &self.assoc_language { + write!(f, ",ASSOC-LANGUAGE={}", quote(value))?; } - write!(f, ",NAME={}", self.name)?; + write!(f, ",NAME={}", quote(&self.name))?; if self.default { write!(f, ",DEFAULT=YES")?; } @@ -293,14 +311,14 @@ impl fmt::Display for ExtXMedia { if self.forced { write!(f, ",FORCED=YES")?; } - if let Some(ref x) = self.instream_id { - write!(f, ",INSTREAM-ID=\"{}\"", x)?; + if let Some(value) = &self.instream_id { + write!(f, ",INSTREAM-ID={}", quote(value))?; } - if let Some(ref x) = self.characteristics { - write!(f, ",CHARACTERISTICS={}", x)?; + if let Some(value) = &self.characteristics { + write!(f, ",CHARACTERISTICS={}", quote(value))?; } - if let Some(ref x) = self.channels { - write!(f, ",CHANNELS={}", x)?; + if let Some(value) = &self.channels { + write!(f, ",CHANNELS={}", quote(value))?; } Ok(()) } @@ -308,50 +326,49 @@ impl fmt::Display for ExtXMedia { impl FromStr for ExtXMedia { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut builder = ExtXMediaBuilder::new(); - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { + + for (key, value) in input.parse::()? { + match key.as_str() { "TYPE" => { - builder.media_type(track!(value.parse())?); + builder.media_type(value.parse()?); } "URI" => { - builder.uri(track!(value.parse())?); + builder.uri(unquote(value)); } "GROUP-ID" => { - builder.group_id(track!(value.parse())?); + builder.group_id(unquote(value)); } "LANGUAGE" => { - builder.language(track!(value.parse())?); + builder.language(unquote(value)); } "ASSOC-LANGUAGE" => { - builder.assoc_language(track!(value.parse())?); + builder.assoc_language(unquote(value)); } "NAME" => { - builder.name(track!(value.parse())?); + builder.name(unquote(value)); } "DEFAULT" => { - builder.default(track!(parse_yes_or_no(value))?); + builder.default((parse_yes_or_no(value))?); } "AUTOSELECT" => { - builder.autoselect(track!(parse_yes_or_no(value))?); + builder.autoselect((parse_yes_or_no(value))?); } "FORCED" => { - builder.forced(track!(parse_yes_or_no(value))?); + builder.forced((parse_yes_or_no(value))?); } "INSTREAM-ID" => { - let s: QuotedString = track!(value.parse())?; - builder.instream_id(track!(s.parse())?); + builder.instream_id(unquote(value).parse()?); } "CHARACTERISTICS" => { - builder.characteristics(track!(value.parse())?); + builder.characteristics(unquote(value)); } "CHANNELS" => { - builder.channels(track!(value.parse())?); + builder.channels(unquote(value)); } _ => { // [6.3.1. General Client Responsibilities] @@ -359,7 +376,7 @@ impl FromStr for ExtXMedia { } } } - track!(builder.finish()) + (builder.finish()) } } @@ -369,14 +386,10 @@ mod test { #[test] fn ext_x_media() { - let tag = ExtXMedia::new(MediaType::Audio, quoted_string("foo"), quoted_string("bar")); + let tag = ExtXMedia::new(MediaType::Audio, "foo", "bar"); let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#; assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V1); } - - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() - } } diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index 3ddc153..0c7d1aa 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -1,57 +1,64 @@ -use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, QuotedString, SessionData}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use getset::{Getters, MutGetters, Setters}; + +use crate::attribute::AttributePairs; +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 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Getters, MutGetters, Setters, Debug, Clone, PartialEq, Eq, Hash)] +#[get = "pub"] +#[set = "pub"] +#[get_mut = "pub"] pub struct ExtXSessionData { - data_id: QuotedString, + /// The identifier of the data. + data_id: String, + /// The session data. data: SessionData, - language: Option, + /// The language of the data. + language: Option, } impl ExtXSessionData { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:"; /// Makes a new `ExtXSessionData` tag. - pub fn new(data_id: QuotedString, data: SessionData) -> Self { + pub fn new(data_id: T, data: SessionData) -> Self { ExtXSessionData { - data_id, + data_id: data_id.to_string(), data, language: None, } } /// Makes a new `ExtXSessionData` with the given language. - pub fn with_language(data_id: QuotedString, data: SessionData, language: QuotedString) -> Self { + pub fn with_language(data_id: T, data: SessionData, language: T) -> Self { ExtXSessionData { - data_id, + data_id: data_id.to_string(), data, - language: Some(language), + language: Some(language.to_string()), } } - /// Returns the identifier of the data. - pub fn data_id(&self) -> &QuotedString { - &self.data_id - } - - /// Returns the session data. - pub fn data(&self) -> &SessionData { - &self.data - } - - /// Returns the language of the data. - pub fn language(&self) -> Option<&QuotedString> { - self.language.as_ref() - } - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -59,13 +66,13 @@ impl ExtXSessionData { impl fmt::Display for ExtXSessionData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; - write!(f, "DATA-ID={}", self.data_id)?; - match self.data { - SessionData::Value(ref x) => write!(f, ",VALUE={}", x)?, - SessionData::Uri(ref x) => write!(f, ",URI={}", x)?, + write!(f, "DATA-ID={}", quote(&self.data_id))?; + match &self.data { + SessionData::Value(value) => write!(f, ",VALUE={}", quote(value))?, + SessionData::Uri(value) => write!(f, ",URI={}", quote(value))?, } - if let Some(ref x) = self.language { - write!(f, ",LANGUAGE={}", x)?; + if let Some(value) = &self.language { + write!(f, ",LANGUAGE={}", quote(value))?; } Ok(()) } @@ -73,21 +80,21 @@ impl fmt::Display for ExtXSessionData { impl FromStr for ExtXSessionData { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut data_id = None; let mut session_value = None; let mut uri = None; let mut language = None; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "DATA-ID" => data_id = Some(track!(value.parse())?), - "VALUE" => session_value = Some(track!(value.parse())?), - "URI" => uri = Some(track!(value.parse())?), - "LANGUAGE" => language = Some(track!(value.parse())?), + + for (key, value) in input.parse::()? { + match key.as_str() { + "DATA-ID" => data_id = Some(unquote(value)), + "VALUE" => session_value = Some(unquote(value)), + "URI" => uri = Some(unquote(value)), + "LANGUAGE" => language = Some(unquote(value)), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -95,15 +102,21 @@ impl FromStr for ExtXSessionData { } } - let data_id = track_assert_some!(data_id, ErrorKind::InvalidInput); - let data = if let Some(value) = session_value { - track_assert_eq!(uri, None, ErrorKind::InvalidInput); - SessionData::Value(value) - } else if let Some(uri) = uri { - SessionData::Uri(uri) - } else { - track_panic!(ErrorKind::InvalidInput); + let data_id = data_id.ok_or(Error::missing_value("EXT-X-DATA-ID"))?; + let data = { + if let Some(value) = session_value { + if uri.is_some() { + return Err(Error::invalid_input()); + } else { + SessionData::Value(value) + } + } else if let Some(uri) = uri { + SessionData::Uri(uri) + } else { + return Err(Error::invalid_input()); + } }; + Ok(ExtXSessionData { data_id, data, @@ -117,35 +130,38 @@ mod test { use super::*; #[test] - fn ext_x_session_data() { - let tag = ExtXSessionData::new( - quoted_string("foo"), - SessionData::Value(quoted_string("bar")), - ); + fn test_display() { + let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - let tag = - ExtXSessionData::new(quoted_string("foo"), SessionData::Uri(quoted_string("bar"))); + let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); - let tag = ExtXSessionData::with_language( - quoted_string("foo"), - SessionData::Value(quoted_string("bar")), - quoted_string("baz"), - ); + let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; - assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); } - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() + #[test] + fn test_parser() { + let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; + assert_eq!(text.parse::().unwrap(), tag); + + let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; + assert_eq!(text.parse::().unwrap(), tag); + + let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); + let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; + assert_eq!(text.parse::().unwrap(), tag); + } + + #[test] + fn test_requires_version() { + let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); + assert_eq!(tag.requires_version(), ProtocolVersion::V1); } } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index dc0838d..0d7d275 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,75 +1,148 @@ -use crate::types::{DecryptionKey, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use url::Url; + +use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion}; +use crate::utils::tag; +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, -} +pub struct ExtXSessionKey(DecryptionKey); impl ExtXSessionKey { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; - /// Makes a new `ExtXSessionKey` tag. - pub 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"); + } - /// Returns a decryption key for the playlist. - pub fn key(&self) -> &DecryptionKey { - &self.key + Self(DecryptionKey::new(method, uri)) } /// 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.0.key_format.is_some() | self.0.key_format_versions.is_some() { + ProtocolVersion::V5 + } else if self.0.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, "{}{}", Self::PREFIX, self.0) } } impl FromStr for ExtXSessionKey { 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; - let key = track!(suffix.parse())?; - Ok(ExtXSessionKey { key }) + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; + Ok(Self(input.parse()?)) + } +} + +impl Deref for ExtXSessionKey { + type Target = DecryptionKey; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ExtXSessionKey { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } #[cfg(test)] mod test { use super::*; - use crate::types::{EncryptionMethod, InitializationVector, QuotedString}; + use crate::types::EncryptionMethod; #[test] - fn ext_x_session_key() { - let tag = ExtXSessionKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: quoted_string("foo"), - 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() + ); } - fn quoted_string(s: &str) -> QuotedString { - QuotedString::new(s).unwrap() + #[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/master_playlist/stream_inf.rs b/src/tags/master_playlist/stream_inf.rs index d919e4d..808d4fc 100644 --- a/src/tags/master_playlist/stream_inf.rs +++ b/src/tags/master_playlist/stream_inf.rs @@ -1,28 +1,30 @@ +use std::fmt; +use std::str::FromStr; + +use url::Url; + use crate::attribute::AttributePairs; use crate::types::{ ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion, - QuotedString, SingleLineString, }; -use crate::utils::parse_u64; -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::str::FromStr; +use crate::utils::{parse_u64, quote, tag, unquote}; +use crate::Error; /// [4.3.4.2. EXT-X-STREAM-INF] /// /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExtXStreamInf { - uri: SingleLineString, + uri: Url, bandwidth: u64, average_bandwidth: Option, - codecs: Option, + codecs: Option, resolution: Option, frame_rate: Option, hdcp_level: Option, - audio: Option, - video: Option, - subtitles: Option, + audio: Option, + video: Option, + subtitles: Option, closed_captions: Option, } @@ -30,7 +32,7 @@ impl ExtXStreamInf { pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; /// Makes a new `ExtXStreamInf` tag. - pub fn new(uri: SingleLineString, bandwidth: u64) -> Self { + pub const fn new(uri: Url, bandwidth: u64) -> Self { ExtXStreamInf { uri, bandwidth, @@ -47,52 +49,57 @@ impl ExtXStreamInf { } /// Returns the URI that identifies the associated media playlist. - pub fn uri(&self) -> &SingleLineString { + pub const fn uri(&self) -> &Url { &self.uri } /// Returns the peak segment bit rate of the variant stream. - pub fn bandwidth(&self) -> u64 { + pub const fn bandwidth(&self) -> u64 { self.bandwidth } /// Returns the average segment bit rate of the variant stream. - pub fn average_bandwidth(&self) -> Option { + pub const fn average_bandwidth(&self) -> Option { self.average_bandwidth } /// Returns a string that represents the list of codec types contained the variant stream. - pub fn codecs(&self) -> Option<&QuotedString> { + pub fn codecs(&self) -> Option<&String> { self.codecs.as_ref() } - /// Returns the optimal pixel resolution at which to display all the video in the variant stream. - pub fn resolution(&self) -> Option { - self.resolution + /// Returns the optimal pixel resolution at which to display all the video in the variant + /// stream. + pub fn resolution(&self) -> Option<(usize, usize)> { + if let Some(res) = &self.resolution { + Some((res.width(), res.height())) + } else { + None + } } /// Returns the maximum frame rate for all the video in the variant stream. - pub fn frame_rate(&self) -> Option { - self.frame_rate + pub fn frame_rate(&self) -> Option { + self.frame_rate.map_or(None, |v| Some(v.as_f64())) } /// Returns the HDCP level of the variant stream. - pub fn hdcp_level(&self) -> Option { + pub const fn hdcp_level(&self) -> Option { self.hdcp_level } /// Returns the group identifier for the audio in the variant stream. - pub fn audio(&self) -> Option<&QuotedString> { + pub fn audio(&self) -> Option<&String> { self.audio.as_ref() } /// Returns the group identifier for the video in the variant stream. - pub fn video(&self) -> Option<&QuotedString> { + pub fn video(&self) -> Option<&String> { self.video.as_ref() } /// Returns the group identifier for the subtitles in the variant stream. - pub fn subtitles(&self) -> Option<&QuotedString> { + pub fn subtitles(&self) -> Option<&String> { self.subtitles.as_ref() } @@ -102,7 +109,7 @@ impl ExtXStreamInf { } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -111,32 +118,32 @@ impl fmt::Display for ExtXStreamInf { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "BANDWIDTH={}", self.bandwidth)?; - if let Some(ref x) = self.average_bandwidth { - write!(f, ",AVERAGE-BANDWIDTH={}", x)?; + if let Some(value) = &self.average_bandwidth { + write!(f, ",AVERAGE-BANDWIDTH={}", value)?; } - if let Some(ref x) = self.codecs { - write!(f, ",CODECS={}", x)?; + if let Some(value) = &self.resolution { + write!(f, ",RESOLUTION={}", value)?; } - if let Some(ref x) = self.resolution { - write!(f, ",RESOLUTION={}", x)?; + if let Some(value) = &self.codecs { + write!(f, ",CODECS={}", quote(value))?; } - if let Some(ref x) = self.frame_rate { - write!(f, ",FRAME-RATE={:.3}", x.as_f64())?; + if let Some(value) = &self.frame_rate { + write!(f, ",FRAME-RATE={:.3}", value.as_f64())?; } - if let Some(ref x) = self.hdcp_level { - write!(f, ",HDCP-LEVEL={}", x)?; + if let Some(value) = &self.hdcp_level { + write!(f, ",HDCP-LEVEL={}", value)?; } - if let Some(ref x) = self.audio { - write!(f, ",AUDIO={}", x)?; + if let Some(value) = &self.audio { + write!(f, ",AUDIO={}", quote(value))?; } - if let Some(ref x) = self.video { - write!(f, ",VIDEO={}", x)?; + if let Some(value) = &self.video { + write!(f, ",VIDEO={}", quote(value))?; } - if let Some(ref x) = self.subtitles { - write!(f, ",SUBTITLES={}", x)?; + if let Some(value) = &self.subtitles { + write!(f, ",SUBTITLES={}", quote(value))?; } - if let Some(ref x) = self.closed_captions { - write!(f, ",CLOSED-CAPTIONS={}", x)?; + if let Some(value) = &self.closed_captions { + write!(f, ",CLOSED-CAPTIONS={}", value)?; } write!(f, "\n{}", self.uri)?; Ok(()) @@ -145,16 +152,14 @@ impl fmt::Display for ExtXStreamInf { impl FromStr for ExtXStreamInf { type Err = Error; - fn from_str(s: &str) -> Result { - let mut lines = s.splitn(2, '\n'); - let first_line = lines.next().expect("Never fails").trim_end_matches('\r'); - let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput); - track_assert!( - first_line.starts_with(Self::PREFIX), - ErrorKind::InvalidInput - ); - let uri = track!(SingleLineString::new(second_line))?; + fn from_str(input: &str) -> Result { + let mut lines = input.lines(); + let first_line = lines.next().ok_or(Error::missing_value("first_line"))?; + let uri = lines.next().ok_or(Error::missing_value("second_line"))?; + + let first_line = tag(first_line, Self::PREFIX)?; + let mut bandwidth = None; let mut average_bandwidth = None; let mut codecs = None; @@ -165,29 +170,30 @@ impl FromStr for ExtXStreamInf { let mut video = None; let mut subtitles = None; let mut closed_captions = None; - let attrs = AttributePairs::parse(first_line.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?), - "AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?), - "CODECS" => codecs = Some(track!(value.parse())?), - "RESOLUTION" => resolution = Some(track!(value.parse())?), - "FRAME-RATE" => frame_rate = Some(track!(value.parse())?), - "HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?), - "AUDIO" => audio = Some(track!(value.parse())?), - "VIDEO" => video = Some(track!(value.parse())?), - "SUBTITLES" => subtitles = Some(track!(value.parse())?), - "CLOSED-CAPTIONS" => closed_captions = Some(track!(value.parse())?), + + for (key, value) in first_line.parse::()? { + match key.as_str() { + "BANDWIDTH" => bandwidth = Some((parse_u64(value))?), + "AVERAGE-BANDWIDTH" => average_bandwidth = Some((parse_u64(value))?), + "CODECS" => codecs = Some(unquote(value)), + "RESOLUTION" => resolution = Some((value.parse())?), + "FRAME-RATE" => frame_rate = Some((value.parse())?), + "HDCP-LEVEL" => hdcp_level = Some((value.parse())?), + "AUDIO" => audio = Some(unquote(value)), + "VIDEO" => video = Some(unquote(value)), + "SUBTITLES" => subtitles = Some(unquote(value)), + "CLOSED-CAPTIONS" => closed_captions = Some((value.parse())?), _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. } } } - let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput); + + let bandwidth = bandwidth.ok_or(Error::missing_value("EXT-X-BANDWIDTH"))?; + Ok(ExtXStreamInf { - uri, + uri: uri.parse()?, bandwidth, average_bandwidth, codecs, @@ -207,11 +213,30 @@ mod test { use super::*; #[test] - fn ext_x_stream_inf() { - let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 1000); - let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo"; - assert_eq!(text.parse().ok(), Some(tag.clone())); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_parser() { + let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com" + .parse::() + .unwrap(); + + assert_eq!( + stream_inf, + ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000) + ); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ProtocolVersion::V1, + ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000).requires_version() + ); + } + + #[test] + fn test_display() { + assert_eq!( + ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000).to_string(), + "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string() + ); } } diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 9aa6539..6619db1 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -1,8 +1,8 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; -use trackable::error::ErrorKindExt; + +use crate::types::ProtocolVersion; +use crate::utils::tag; /// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// @@ -16,18 +16,18 @@ impl ExtXDiscontinuitySequence { pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:"; /// Makes a new `ExtXDiscontinuitySequence` tag. - pub fn new(seq_num: u64) -> Self { + pub const fn new(seq_num: u64) -> Self { ExtXDiscontinuitySequence { seq_num } } /// Returns the discontinuity sequence number of /// the first media segment that appears in the associated playlist. - pub fn seq_num(self) -> u64 { + pub const fn seq_num(self) -> u64 { self.seq_num } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -39,11 +39,11 @@ impl fmt::Display for ExtXDiscontinuitySequence { } impl FromStr for ExtXDiscontinuitySequence { - type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXDiscontinuitySequence { seq_num }) + type Err = crate::Error; + + fn from_str(input: &str) -> Result { + let seq_num = tag(input, Self::PREFIX)?.parse().unwrap(); // TODO! + Ok(Self::new(seq_num)) } } diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index f8e333f..b34b357 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -1,8 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.3.4. EXT-X-ENDLIST] /// /// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4 @@ -12,19 +14,22 @@ impl ExtXEndList { pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V1 } } + impl fmt::Display for ExtXEndList { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Self::PREFIX.fmt(f) } } + impl FromStr for ExtXEndList { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtXEndList) } } diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 4bea636..dfe3abc 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -1,8 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.3.6. EXT-X-I-FRAMES-ONLY] /// /// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6 @@ -13,7 +15,7 @@ impl ExtXIFramesOnly { pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V4 } } @@ -26,8 +28,9 @@ impl fmt::Display for ExtXIFramesOnly { impl FromStr for ExtXIFramesOnly { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtXIFramesOnly) } } diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 99be333..1f328f0 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -1,8 +1,9 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; -use trackable::error::ErrorKindExt; + +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; /// [4.3.3.2. EXT-X-MEDIA-SEQUENCE] /// @@ -16,17 +17,17 @@ impl ExtXMediaSequence { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:"; /// Makes a new `ExtXMediaSequence` tag. - pub fn new(seq_num: u64) -> Self { + pub const fn new(seq_num: u64) -> Self { ExtXMediaSequence { seq_num } } /// Returns the sequence number of the first media segment that appears in the associated playlist. - pub fn seq_num(self) -> u64 { + pub const fn seq_num(self) -> u64 { self.seq_num } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -39,10 +40,11 @@ impl fmt::Display for ExtXMediaSequence { impl FromStr for ExtXMediaSequence { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXMediaSequence { seq_num }) + + fn from_str(input: &str) -> Result { + let seq_num = tag(input, Self::PREFIX)?.parse()?; + + Ok(ExtXMediaSequence::new(seq_num)) } } diff --git a/src/tags/media_playlist/playlist_type.rs b/src/tags/media_playlist/playlist_type.rs index e2a5f99..221914f 100644 --- a/src/tags/media_playlist/playlist_type.rs +++ b/src/tags/media_playlist/playlist_type.rs @@ -1,48 +1,62 @@ -use crate::types::{PlaylistType, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; -use trackable::error::ErrorKindExt; -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE] +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + +/// [4.3.3.5. EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/rfc8216#section-4.3.3.5) /// -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 +/// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the +/// Media Playlist. It applies to the entire Media Playlist. +/// It is OPTIONAL. Its format is: +/// +/// ```text +/// #EXT-X-PLAYLIST-TYPE: +/// ``` +/// +/// # Note +/// If the EXT-X-PLAYLIST-TYPE tag is omitted from a Media Playlist, the +/// Playlist can be updated according to the rules in Section 6.2.1 with +/// no additional restrictions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ExtXPlaylistType { - playlist_type: PlaylistType, +pub enum ExtXPlaylistType { + /// If the ExtXPlaylistType is Event, Media Segments can only be added to + /// the end of the Media Playlist. + Event, + /// If the ExtXPlaylistType is Video On Demand (Vod), + /// the Media Playlist cannot change. + Vod, } impl ExtXPlaylistType { pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:"; - /// Makes a new `ExtXPlaylistType` tag. - pub fn new(playlist_type: PlaylistType) -> Self { - ExtXPlaylistType { playlist_type } - } - - /// Returns the type of the associated media playlist. - pub fn playlist_type(self) -> PlaylistType { - self.playlist_type - } - /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } impl fmt::Display for ExtXPlaylistType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.playlist_type) + match &self { + Self::Event => write!(f, "{}EVENT", Self::PREFIX), + Self::Vod => write!(f, "{}VOD", Self::PREFIX), + } } } impl FromStr for ExtXPlaylistType { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; - Ok(ExtXPlaylistType { playlist_type }) + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; + match input { + "EVENT" => Ok(Self::Event), + "VOD" => Ok(Self::Vod), + _ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))), + } } } @@ -51,11 +65,48 @@ mod test { use super::*; #[test] - fn ext_x_playlist_type() { - let tag = ExtXPlaylistType::new(PlaylistType::Vod); - let text = "#EXT-X-PLAYLIST-TYPE:VOD"; - assert_eq!(text.parse().ok(), Some(tag)); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + fn test_parser() { + assert_eq!( + "#EXT-X-PLAYLIST-TYPE:VOD" + .parse::() + .unwrap(), + ExtXPlaylistType::Vod, + ); + + assert_eq!( + "#EXT-X-PLAYLIST-TYPE:EVENT" + .parse::() + .unwrap(), + ExtXPlaylistType::Event, + ); + + assert!("#EXT-X-PLAYLIST-TYPE:H" + .parse::() + .is_err()); + } + + #[test] + fn test_display() { + assert_eq!( + "#EXT-X-PLAYLIST-TYPE:VOD".to_string(), + ExtXPlaylistType::Vod.to_string(), + ); + + assert_eq!( + "#EXT-X-PLAYLIST-TYPE:EVENT".to_string(), + ExtXPlaylistType::Event.to_string(), + ); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ExtXPlaylistType::Vod.requires_version(), + ProtocolVersion::V1 + ); + assert_eq!( + ExtXPlaylistType::Event.requires_version(), + ProtocolVersion::V1 + ); } } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index a2af667..71b0c66 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -1,14 +1,15 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; use std::time::Duration; -use trackable::error::ErrorKindExt; + +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; /// [4.3.3.1. EXT-X-TARGETDURATION] /// /// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct ExtXTargetDuration { duration: Duration, } @@ -19,18 +20,18 @@ impl ExtXTargetDuration { /// Makes a new `ExtXTargetduration` tag. /// /// Note that the nanoseconds part of the `duration` will be discarded. - pub fn new(duration: Duration) -> Self { + pub const fn new(duration: Duration) -> Self { let duration = Duration::from_secs(duration.as_secs()); ExtXTargetDuration { duration } } /// Returns the maximum media segment duration in the associated playlist. - pub fn duration(&self) -> Duration { + pub const fn duration(&self) -> Duration { self.duration } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -43,11 +44,11 @@ impl fmt::Display for ExtXTargetDuration { impl FromStr for ExtXTargetDuration { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?; + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?.parse()?; Ok(ExtXTargetDuration { - duration: Duration::from_secs(duration), + duration: Duration::from_secs(input), }) } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 52397ba..e7a895a 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -1,48 +1,96 @@ -use crate::types::{ByteRange, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; use std::fmt; +use std::ops::Deref; use std::str::FromStr; -use trackable::error::ErrorKindExt; + +use crate::types::{ByteRange, ProtocolVersion}; +use crate::utils::tag; +use crate::Error; /// [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, -} +pub struct ExtXByteRange(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 } + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXByteRange; + /// + /// let byte_range = ExtXByteRange::new(20, Some(5)); + /// ``` + pub const fn new(length: usize, start: Option) -> Self { + Self(ByteRange::new(length, start)) } - /// Returns the range of the associated media segment. - pub fn range(&self) -> ByteRange { - self.range + /// Converts the [ExtXByteRange] to a [ByteRange]. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXByteRange; + /// use hls_m3u8::types::ByteRange; + /// + /// let byte_range = ExtXByteRange::new(20, Some(5)); + /// let range: ByteRange = byte_range.to_range(); + /// ``` + pub const fn to_range(&self) -> ByteRange { + self.0 } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXByteRange; + /// use hls_m3u8::types::ProtocolVersion; + /// + /// let byte_range = ExtXByteRange::new(20, Some(5)); + /// assert_eq!(byte_range.requires_version(), ProtocolVersion::V4); + /// ``` + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } +impl Deref for ExtXByteRange { + type Target = ByteRange; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl fmt::Display for ExtXByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}{}", Self::PREFIX, self.range) + write!(f, "{}", Self::PREFIX)?; + write!(f, "{}", self.0)?; + Ok(()) } } 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 }) + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; + + let tokens = input.splitn(2, '@').collect::>(); + if tokens.is_empty() { + return Err(Error::invalid_input()); + } + + let length = tokens[0].parse()?; + + let start = { + let mut result = None; + if tokens.len() == 2 { + result = Some(tokens[1].parse()?); + } + result + }; + + Ok(ExtXByteRange::new(length, start)) } } @@ -51,21 +99,40 @@ mod test { use super::*; #[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); + fn test_display() { + let byte_range = ExtXByteRange::new(0, Some(5)); + assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:0@5".to_string()); - 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); + let byte_range = ExtXByteRange::new(99999, Some(2)); + assert_eq!( + byte_range.to_string(), + "#EXT-X-BYTERANGE:99999@2".to_string() + ); + + let byte_range = ExtXByteRange::new(99999, None); + assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:99999".to_string()); + } + + #[test] + fn test_parse() { + let byte_range = ExtXByteRange::new(99999, Some(2)); + assert_eq!( + byte_range, + "#EXT-X-BYTERANGE:99999@2".parse::().unwrap() + ); + + let byte_range = ExtXByteRange::new(99999, None); + assert_eq!( + byte_range, + "#EXT-X-BYTERANGE:99999".parse::().unwrap() + ); + } + + #[test] + fn test_deref() { + let byte_range = ExtXByteRange::new(0, Some(22)); + + assert_eq!(*byte_range.length(), 0); + assert_eq!(*byte_range.start(), Some(22)); } } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index b406289..d1c4fbd 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -1,37 +1,75 @@ -use crate::attribute::AttributePairs; -use crate::types::{DecimalFloatingPoint, ProtocolVersion, QuotedString}; -use crate::{Error, ErrorKind, Result}; use std::collections::BTreeMap; use std::fmt; use std::str::FromStr; use std::time::Duration; +use chrono::{DateTime, FixedOffset}; +use getset::{Getters, MutGetters, Setters}; + +use crate::attribute::AttributePairs; +use crate::types::{DecimalFloatingPoint, ProtocolVersion}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; + /// [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)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters, MutGetters, Setters)] +#[get = "pub"] +#[set = "pub"] +#[get_mut = "pub"] pub struct ExtXDateRange { - pub id: QuotedString, - pub class: Option, - pub start_date: QuotedString, - 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: bool, - pub client_attributes: BTreeMap, + /// A string that uniquely identifies a Date Range in the Playlist. + /// This attribute is REQUIRED. + id: String, + /// A client-defined string that specifies some set of attributes and their associated value + /// semantics. All Date Ranges with the same CLASS attribute value MUST adhere to these + /// semantics. This attribute is OPTIONAL. + class: Option, + /// The date at which the Date Range begins. This attribute is REQUIRED. + start_date: DateTime, + /// The date at which the Date Range ends. It MUST be equal to or later than the value of the + /// START-DATE attribute. This attribute is OPTIONAL. + end_date: Option>, + /// The duration of the Date Range. It MUST NOT be negative. A single + /// instant in time (e.g., crossing a finish line) SHOULD be + /// represented with a duration of 0. This attribute is OPTIONAL. + duration: Option, + /// The expected duration of the Date Range. It MUST NOT be negative. This + /// attribute SHOULD be used to indicate the expected duration of a + /// Date Range whose actual duration is not yet known. + /// It is OPTIONAL. + planned_duration: Option, + /// + scte35_cmd: Option, + /// + scte35_out: Option, + /// + scte35_in: Option, + /// This attribute indicates that the end of the range containing it is equal to the + /// START-DATE of its Following Range. The Following Range is the + /// Date Range of the same CLASS, that has the earliest START-DATE + /// after the START-DATE of the range in question. This attribute is + /// OPTIONAL. + end_on_next: bool, + /// The "X-" prefix defines a namespace reserved for client-defined + /// attributes. The client-attribute MUST be a legal AttributeName. + /// Clients SHOULD use a reverse-DNS syntax when defining their own + /// attribute names to avoid collisions. The attribute value MUST be + /// a quoted-string, a hexadecimal-sequence, or a decimal-floating- + /// point. An example of a client-defined attribute is X-COM-EXAMPLE- + /// AD-ID="XYZ123". These attributes are OPTIONAL. + 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 { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -39,13 +77,13 @@ impl ExtXDateRange { 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, "ID={}", quote(&self.id))?; + if let Some(value) = &self.class { + write!(f, ",CLASS={}", quote(value))?; } - write!(f, ",START-DATE={}", self.start_date)?; - if let Some(ref x) = self.end_date { - write!(f, ",END-DATE={}", x)?; + write!(f, ",START-DATE={}", quote(&self.start_date))?; + if let Some(value) = &self.end_date { + write!(f, ",END-DATE={}", quote(value))?; } if let Some(x) = self.duration { write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?; @@ -57,14 +95,14 @@ impl fmt::Display for ExtXDateRange { DecimalFloatingPoint::from_duration(x) )?; } - if let Some(ref x) = self.scte35_cmd { - write!(f, ",SCTE35-CMD={}", x)?; + if let Some(value) = &self.scte35_cmd { + write!(f, ",SCTE35-CMD={}", quote(value))?; } - if let Some(ref x) = self.scte35_out { - write!(f, ",SCTE35-OUT={}", x)?; + if let Some(value) = &self.scte35_out { + write!(f, ",SCTE35-OUT={}", quote(value))?; } - if let Some(ref x) = self.scte35_in { - write!(f, ",SCTE35-IN={}", x)?; + if let Some(value) = &self.scte35_in { + write!(f, ",SCTE35-IN={}", quote(value))?; } if self.end_on_next { write!(f, ",END-ON-NEXT=YES",)?; @@ -78,8 +116,9 @@ impl fmt::Display for ExtXDateRange { impl FromStr for ExtXDateRange { type Err = Error; - fn from_str(s: &str) -> Result { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result { + let input = tag(input, Self::PREFIX)?; let mut id = None; let mut class = None; @@ -91,28 +130,30 @@ impl FromStr for ExtXDateRange { let mut scte35_out = None; let mut scte35_in = None; let mut end_on_next = false; + 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" => start_date = Some(track!(value.parse())?), - "END-DATE" => end_date = Some(track!(value.parse())?), + + for (key, value) in input.parse::()? { + match key.as_str() { + "ID" => id = Some(unquote(value)), + "CLASS" => class = Some(unquote(value)), + "START-DATE" => start_date = Some(unquote(value)), + "END-DATE" => end_date = Some(unquote(value).parse()?), "DURATION" => { - let seconds: DecimalFloatingPoint = track!(value.parse())?; + let seconds: DecimalFloatingPoint = (value.parse())?; duration = Some(seconds.to_duration()); } "PLANNED-DURATION" => { - let seconds: DecimalFloatingPoint = track!(value.parse())?; + let seconds: DecimalFloatingPoint = (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())?), + "SCTE35-CMD" => scte35_cmd = Some(unquote(value)), + "SCTE35-OUT" => scte35_out = Some(unquote(value)), + "SCTE35-IN" => scte35_in = Some(unquote(value)), "END-ON-NEXT" => { - track_assert_eq!(value, "YES", ErrorKind::InvalidInput); + if value != "YES" { + return Err(Error::invalid_input()); + } end_on_next = true; } _ => { @@ -126,10 +167,15 @@ impl FromStr for ExtXDateRange { } } - let id = track_assert_some!(id, ErrorKind::InvalidInput); - let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput); + let id = id.ok_or(Error::missing_value("EXT-X-ID"))?; + let start_date = start_date + .ok_or(Error::missing_value("EXT-X-START-DATE"))? + .parse()?; + if end_on_next { - track_assert!(class.is_some(), ErrorKind::InvalidInput); + if class.is_none() { + return Err(Error::invalid_input()); + } } Ok(ExtXDateRange { id, diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 8e23325..9571f2c 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -1,30 +1,36 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::{Error, Result}; + /// [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 { + pub const 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); + + fn from_str(input: &str) -> Result { + tag(input, Self::PREFIX)?; Ok(ExtXDiscontinuity) } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 3543f69..287864b 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -1,24 +1,60 @@ -use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; use std::time::Duration; -use trackable::error::ErrorKindExt; -/// [4.3.2.1. EXTINF] +use crate::types::{DecimalFloatingPoint, ProtocolVersion}; +use crate::utils::tag; +use crate::Error; + +/// [4.3.2.1. EXTINF](https://tools.ietf.org/html/rfc8216#section-4.3.2.1) /// -/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1 -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies +/// only to the next [Media Segment]. This tag is REQUIRED for each [Media Segment]. +/// +/// Its format is: +/// ```text +/// #EXTINF:,[] +/// ``` +/// The title is an optional informative title about the [Media Segment]. +/// +/// [Media Segment]: crate::media_segment::MediaSegment +/// +/// # Examples +/// Parsing from a String: +/// ``` +/// use std::time::Duration; +/// use hls_m3u8::tags::ExtInf; +/// +/// let ext_inf = "#EXTINF:8,".parse::<ExtInf>().expect("Failed to parse tag!"); +/// +/// assert_eq!(ext_inf.duration(), Duration::from_secs(8)); +/// assert_eq!(ext_inf.title(), None); +/// ``` +/// +/// Converting to a String: +/// ``` +/// use std::time::Duration; +/// use hls_m3u8::tags::ExtInf; +/// +/// let ext_inf = ExtInf::with_title( +/// Duration::from_millis(88), +/// "title" +/// ); +/// +/// assert_eq!(ext_inf.duration(), Duration::from_millis(88)); +/// assert_eq!(ext_inf.to_string(), "#EXTINF:0.088,title".to_string()); +/// ``` +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ExtInf { duration: Duration, - title: Option<SingleLineString>, + title: Option<String>, } impl ExtInf { pub(crate) const PREFIX: &'static str = "#EXTINF:"; /// Makes a new `ExtInf` tag. - pub fn new(duration: Duration) -> Self { + pub const fn new(duration: Duration) -> Self { ExtInf { duration, title: None, @@ -26,20 +62,20 @@ impl ExtInf { } /// Makes a new `ExtInf` tag with the given title. - pub fn with_title(duration: Duration, title: SingleLineString) -> Self { + pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self { ExtInf { duration, - title: Some(title), + title: Some(title.to_string()), } } /// Returns the duration of the associated media segment. - pub fn duration(&self) -> Duration { + pub const fn duration(&self) -> Duration { self.duration } /// Returns the title of the associated media segment. - pub fn title(&self) -> Option<&SingleLineString> { + pub fn title(&self) -> Option<&String> { self.title.as_ref() } @@ -59,10 +95,10 @@ impl fmt::Display for ExtInf { let duration = (self.duration.as_secs() as f64) + (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0); - write!(f, "{}", duration)?; + write!(f, "{},", duration)?; - if let Some(ref title) = self.title { - write!(f, ",{}", title)?; + if let Some(value) = &self.title { + write!(f, "{}", value)?; } Ok(()) } @@ -70,45 +106,114 @@ impl fmt::Display for ExtInf { impl FromStr for ExtInf { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - 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(); + fn from_str(input: &str) -> Result<Self, Self::Err> { + let input = tag(input, Self::PREFIX)?; + dbg!(&input); + let tokens = input.splitn(2, ',').collect::<Vec<_>>(); - let title = if let Some(title) = tokens.next() { - Some(track!(SingleLineString::new(title))?) - } else { - None + if tokens.len() == 0 { + return Err(Error::custom(format!( + "failed to parse #EXTINF tag, couldn't split input: {:?}", + input + ))); + } + + let duration = tokens[0].parse::<DecimalFloatingPoint>()?.to_duration(); + + let title = { + if tokens.len() >= 2 { + if tokens[1].trim().is_empty() { + None + } else { + Some(tokens[1].to_string()) + } + } else { + None + } }; + Ok(ExtInf { duration, title }) } } +impl From<Duration> for ExtInf { + fn from(value: Duration) -> Self { + Self::new(value) + } +} + #[cfg(test)] mod test { 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), - SingleLineString::new("foo").unwrap(), + fn test_display() { + assert_eq!( + "#EXTINF:5,".to_string(), + ExtInf::new(Duration::from_secs(5)).to_string() ); - 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); + assert_eq!( + "#EXTINF:5.5,".to_string(), + ExtInf::new(Duration::from_millis(5500)).to_string() + ); + assert_eq!( + "#EXTINF:5.5,title".to_string(), + ExtInf::with_title(Duration::from_millis(5500), "title").to_string() + ); + assert_eq!( + "#EXTINF:5,title".to_string(), + ExtInf::with_title(Duration::from_secs(5), "title").to_string() + ); + } - 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 test_parser() { + // #EXTINF:<duration>,[<title>] + assert_eq!( + "#EXTINF:5".parse::<ExtInf>().unwrap(), + ExtInf::new(Duration::from_secs(5)) + ); + assert_eq!( + "#EXTINF:5,".parse::<ExtInf>().unwrap(), + ExtInf::new(Duration::from_secs(5)) + ); + assert_eq!( + "#EXTINF:5.5".parse::<ExtInf>().unwrap(), + ExtInf::new(Duration::from_millis(5500)) + ); + assert_eq!( + "#EXTINF:5.5,".parse::<ExtInf>().unwrap(), + ExtInf::new(Duration::from_millis(5500)) + ); + assert_eq!( + "#EXTINF:5.5,title".parse::<ExtInf>().unwrap(), + ExtInf::with_title(Duration::from_millis(5500), "title") + ); + assert_eq!( + "#EXTINF:5,title".parse::<ExtInf>().unwrap(), + ExtInf::with_title(Duration::from_secs(5), "title") + ); + } + + #[test] + fn test_title() { + assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), None); + assert_eq!( + ExtInf::with_title(Duration::from_secs(5), "title").title(), + Some(&"title".to_string()) + ); + } + + #[test] + fn test_requires_version() { + assert_eq!( + ExtInf::new(Duration::from_secs(4)).requires_version(), + ProtocolVersion::V1 + ); + assert_eq!( + ExtInf::new(Duration::from_millis(4400)).requires_version(), + ProtocolVersion::V3 + ); } } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index e9e535c..64abf6d 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,130 +1,157 @@ -use crate::attribute::AttributePairs; -use crate::types::{DecryptionKey, ProtocolVersion}; -use crate::{Error, ErrorKind, Result}; use std::fmt; +use std::ops::{Deref, DerefMut}; use std::str::FromStr; +use url::Url; + +use crate::types::{DecryptionKey, EncryptionMethod}; +use crate::utils::tag; +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 { - key: Option<DecryptionKey>, -} +pub struct ExtXKey(DecryptionKey); 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) } + /// # 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(DecryptionKey::new(method, uri)) } /// Makes a new `ExtXKey` tag without a decryption key. + /// # Example + /// ``` + /// use hls_m3u8::tags::ExtXKey; /// - /// This tag has the `METHDO=NONE` attribute. - pub fn new_without_key() -> Self { - ExtXKey { key: None } + /// let key = ExtXKey::empty(); + /// + /// assert_eq!( + /// key.to_string(), + /// "#EXT-X-KEY:METHOD=NONE" + /// ); + /// ``` + pub const fn empty() -> Self { + Self(DecryptionKey { + 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.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(()) + /// Returns whether the [EncryptionMethod] is [None](EncryptionMethod::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 { + self.0.method() == EncryptionMethod::None } } impl FromStr for ExtXKey { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; - if AttributePairs::parse(suffix).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) { - 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) }) - } + fn from_str(input: &str) -> Result<Self, Self::Err> { + let input = tag(input, Self::PREFIX)?; + Ok(Self(input.parse()?)) + } +} + +impl fmt::Display for ExtXKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", Self::PREFIX, self.0) + } +} + +impl Deref for ExtXKey { + type Target = DecryptionKey; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ExtXKey { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } #[cfg(test)] mod test { use super::*; - use crate::types::{EncryptionMethod, InitializationVector, QuotedString}; + 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: 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 mut key = ExtXKey::empty(); + // it is expected, that all attributes will be ignored in an empty key! + key.set_key_format("hi"); + key.set_iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]); + key.set_uri("https://www.example.com".parse().unwrap()); + key.set_key_format_versions("1/2/3"); - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: QuotedString::new("foo").unwrap(), - 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); + assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); + } - let tag = ExtXKey::new(DecryptionKey { - method: EncryptionMethod::Aes128, - uri: QuotedString::new("foo").unwrap(), - iv: Some(InitializationVector([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ])), - key_format: Some(QuotedString::new("baz").unwrap()), - 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); + #[test] + fn test_parser() { + assert_eq!( + r#"#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + .parse::<ExtXKey>() + .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, + ]); } } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 53c408c..df6659c 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -1,15 +1,17 @@ -use crate::attribute::AttributePairs; -use crate::types::{ByteRange, ProtocolVersion, QuotedString}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::attribute::AttributePairs; +use crate::types::{ByteRange, ProtocolVersion}; +use crate::utils::{quote, tag, unquote}; +use crate::Error; + /// [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, + uri: String, range: Option<ByteRange>, } @@ -17,30 +19,33 @@ 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 } + pub fn new<T: ToString>(uri: T) -> Self { + ExtXMap { + uri: uri.to_string(), + range: None, + } } /// Makes a new `ExtXMap` tag with the given range. - pub fn with_range(uri: QuotedString, range: ByteRange) -> Self { + pub fn with_range<T: ToString>(uri: T, range: ByteRange) -> Self { ExtXMap { - uri, + uri: uri.to_string(), range: Some(range), } } /// Returns the URI that identifies a resource that contains the media initialization section. - pub fn uri(&self) -> &QuotedString { + pub const fn uri(&self) -> &String { &self.uri } /// Returns the range of the media initialization section. - pub fn range(&self) -> Option<ByteRange> { + pub const fn range(&self) -> Option<ByteRange> { self.range } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V6 } } @@ -48,9 +53,9 @@ impl ExtXMap { 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)?; + write!(f, "URI={}", quote(&self.uri))?; + if let Some(value) = &self.range { + write!(f, ",BYTERANGE={}", quote(value))?; } Ok(()) } @@ -58,19 +63,18 @@ impl fmt::Display for ExtXMap { impl FromStr for ExtXMap { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result<Self, Self::Err> { + let input = tag(input, Self::PREFIX)?; 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" => uri = Some(track!(value.parse())?), + + for (key, value) in input.parse::<AttributePairs>()? { + match key.as_str() { + "URI" => uri = Some(unquote(value)), "BYTERANGE" => { - let s: QuotedString = track!(value.parse())?; - range = Some(track!(s.parse())?); + range = Some((unquote(value).parse())?); } _ => { // [6.3.1. General Client Responsibilities] @@ -79,7 +83,7 @@ impl FromStr for ExtXMap { } } - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + let uri = uri.ok_or(Error::missing_value("EXT-X-URI"))?; Ok(ExtXMap { uri, range }) } } @@ -90,21 +94,16 @@ mod test { #[test] fn ext_x_map() { - let tag = ExtXMap::new(QuotedString::new("foo").unwrap()); + let tag = ExtXMap::new("foo"); 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 tag = ExtXMap::with_range("foo", ByteRange::new(9, Some(2))); let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#; - track_try_unwrap!(ExtXMap::from_str(text)); + ExtXMap::from_str(text).unwrap(); + assert_eq!(text.parse().ok(), Some(tag.clone())); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V6); diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index b694c93..643ec4b 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,49 +1,53 @@ -use crate::types::{ProtocolVersion, SingleLineString}; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use chrono::{DateTime, FixedOffset}; + +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [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, PartialEq, Eq, Hash)] -pub struct ExtXProgramDateTime { - date_time: SingleLineString, -} +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ExtXProgramDateTime(DateTime<FixedOffset>); impl ExtXProgramDateTime { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; /// Makes a new `ExtXProgramDateTime` tag. - pub fn new(date_time: SingleLineString) -> Self { - ExtXProgramDateTime { date_time } + pub fn new<T: Into<DateTime<FixedOffset>>>(date_time: T) -> Self { + Self(date_time.into()) } /// Returns the date-time of the first sample of the associated media segment. - pub fn date_time(&self) -> &SingleLineString { - &self.date_time + pub const fn date_time(&self) -> &DateTime<FixedOffset> { + &self.0 } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const 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) + let date_time = self.0.to_rfc3339(); + write!(f, "{}{}", Self::PREFIX, date_time) } } impl FromStr for ExtXProgramDateTime { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); - let suffix = s.split_at(Self::PREFIX.len()).1; - Ok(ExtXProgramDateTime { - date_time: track!(SingleLineString::new(suffix))?, - }) + + fn from_str(input: &str) -> Result<Self, Self::Err> { + let input = tag(input, Self::PREFIX)?; + + // TODO: parse with chrono + let date_time = DateTime::parse_from_rfc3339(input)?; + Ok(Self::new(date_time)) } } @@ -52,12 +56,34 @@ mod test { use super::*; #[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::<ExtXProgramDateTime>().is_ok()); + fn test_display() { + let date_time = "2010-02-19T14:54:23.031+08:00" + .parse::<DateTime<FixedOffset>>() + .unwrap(); - let tag = text.parse::<ExtXProgramDateTime>().unwrap(); - assert_eq!(tag.to_string(), text); - assert_eq!(tag.requires_version(), ProtocolVersion::V1); + let program_date_time = ExtXProgramDateTime::new(date_time); + + assert_eq!( + program_date_time.to_string(), + "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string() + ); + } + + #[test] + fn test_parser() { + "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00" + .parse::<ExtXProgramDateTime>() + .unwrap(); + } + + #[test] + fn test_requires_version() { + let date_time = "2010-02-19T14:54:23.031+08:00" + .parse::<DateTime<FixedOffset>>() + .unwrap(); + + let program_date_time = ExtXProgramDateTime::new(date_time); + + assert_eq!(program_date_time.requires_version(), ProtocolVersion::V1); } } diff --git a/src/tags/mod.rs b/src/tags/mod.rs index b4a72fa..ebad0b7 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -2,12 +2,6 @@ //! //! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3 -macro_rules! may_invalid { - ($expr:expr) => { - $expr.map_err(|e| track!(Error::from(ErrorKind::InvalidInput.cause(e)))) - }; -} - macro_rules! impl_from { ($to:ident, $from:ident) => { impl From<$from> for $to { diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index d4916f9..5268dcd 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -1,8 +1,10 @@ -use crate::types::ProtocolVersion; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::types::ProtocolVersion; +use crate::utils::tag; +use crate::Error; + /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] /// /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1 @@ -12,7 +14,7 @@ impl ExtXIndependentSegments { pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS"; /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -25,8 +27,9 @@ impl fmt::Display for ExtXIndependentSegments { impl FromStr for ExtXIndependentSegments { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result<Self, Self::Err> { + tag(input, Self::PREFIX)?; Ok(ExtXIndependentSegments) } } diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index 5c301c1..a4c594a 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -1,10 +1,11 @@ -use crate::attribute::AttributePairs; -use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; -use crate::utils::parse_yes_or_no; -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::str::FromStr; +use crate::attribute::AttributePairs; +use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint}; +use crate::utils::{parse_yes_or_no, tag}; +use crate::Error; + /// [4.3.5.2. EXT-X-START] /// /// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2 @@ -18,34 +19,46 @@ impl ExtXStart { pub(crate) const PREFIX: &'static str = "#EXT-X-START:"; /// Makes a new `ExtXStart` tag. - pub fn new(time_offset: SignedDecimalFloatingPoint) -> Self { + /// # Panic + /// Panics if the time_offset value is infinite. + pub fn new(time_offset: f64) -> Self { + if time_offset.is_infinite() { + panic!("EXT-X-START: Floating point value must be finite!"); + } + ExtXStart { - time_offset, + time_offset: SignedDecimalFloatingPoint::new(time_offset).unwrap(), precise: false, } } /// Makes a new `ExtXStart` tag with the given `precise` flag. - pub fn with_precise(time_offset: SignedDecimalFloatingPoint, precise: bool) -> Self { + /// # Panic + /// Panics if the time_offset value is infinite. + pub fn with_precise(time_offset: f64, precise: bool) -> Self { + if time_offset.is_infinite() { + panic!("EXT-X-START: Floating point value must be finite!"); + } + ExtXStart { - time_offset, + time_offset: SignedDecimalFloatingPoint::new(time_offset).unwrap(), precise, } } /// Returns the time offset of the media segments in the playlist. - pub fn time_offset(&self) -> SignedDecimalFloatingPoint { - self.time_offset + pub const fn time_offset(&self) -> f64 { + self.time_offset.as_f64() } /// Returns whether clients should not render media stream whose presentation times are /// prior to the specified time offset. - pub fn precise(&self) -> bool { + pub const fn precise(&self) -> bool { self.precise } /// Returns the protocol compatibility version that this tag requires. - pub fn requires_version(&self) -> ProtocolVersion { + pub const fn requires_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } @@ -63,17 +76,17 @@ impl fmt::Display for ExtXStart { impl FromStr for ExtXStart { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result<Self, Self::Err> { + let input = tag(input, Self::PREFIX)?; let mut time_offset = None; let mut precise = false; - let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1); - for attr in attrs { - let (key, value) = track!(attr)?; - match key { - "TIME-OFFSET" => time_offset = Some(track!(value.parse())?), - "PRECISE" => precise = track!(parse_yes_or_no(value))?, + + for (key, value) in input.parse::<AttributePairs>()? { + match key.as_str() { + "TIME-OFFSET" => time_offset = Some((value.parse())?), + "PRECISE" => precise = (parse_yes_or_no(value))?, _ => { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an unrecognized AttributeName. @@ -81,7 +94,8 @@ impl FromStr for ExtXStart { } } - let time_offset = track_assert_some!(time_offset, ErrorKind::InvalidInput); + let time_offset = time_offset.ok_or(Error::missing_value("EXT-X-TIME-OFFSET"))?; + Ok(ExtXStart { time_offset, precise, @@ -95,13 +109,13 @@ mod test { #[test] fn ext_x_start() { - let tag = ExtXStart::new(SignedDecimalFloatingPoint::new(-1.23).unwrap()); + let tag = ExtXStart::new(-1.23); let text = "#EXT-X-START:TIME-OFFSET=-1.23"; assert_eq!(text.parse().ok(), Some(tag)); assert_eq!(tag.to_string(), text); assert_eq!(tag.requires_version(), ProtocolVersion::V1); - let tag = ExtXStart::with_precise(SignedDecimalFloatingPoint::new(1.23).unwrap(), true); + let tag = ExtXStart::with_precise(1.23, true); let text = "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES"; assert_eq!(text.parse().ok(), Some(tag)); assert_eq!(tag.to_string(), text); diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index af26870..09f3535 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -1,18 +1,31 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; +use std::str::FromStr; + +use getset::{Getters, MutGetters, Setters}; + +use crate::Error; /// Byte range. /// /// See: [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 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Getters, Setters, MutGetters, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[get = "pub"] +#[set = "pub"] +#[get_mut = "pub"] pub struct ByteRange { - pub length: usize, - pub start: Option<usize>, + /// The length of the range. + length: usize, + /// The start of the range. + start: Option<usize>, +} + +impl ByteRange { + /// Creates a new [ByteRange]. + pub const fn new(length: usize, start: Option<usize>) -> Self { + Self { length, start } + } } impl fmt::Display for ByteRange { @@ -27,20 +40,23 @@ impl fmt::Display for ByteRange { impl FromStr for ByteRange { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - let mut tokens = s.splitn(2, '@'); - let length = tokens.next().expect("Never fails"); - let start = if let Some(start) = tokens.next() { - Some(track!(start - .parse() - .map_err(|e| ErrorKind::InvalidInput.cause(e)))?) - } else { - None + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let tokens = s.splitn(2, '@').collect::<Vec<_>>(); + if tokens.is_empty() { + return Err(Error::invalid_input()); + } + + let length = tokens[0].parse()?; + + let start = { + let mut result = None; + if tokens.len() == 2 { + result = Some(tokens[1].parse()?); + } + result }; - Ok(ByteRange { - length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, - start, - }) + Ok(ByteRange::new(length, start)) } } diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index fb4ed85..ad016f1 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -1,7 +1,8 @@ -use crate::types::QuotedString; -use crate::{Error, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::utils::{quote, unquote}; +use crate::{Error, Result}; /// The identifier of a closed captions group or its absence. /// @@ -11,14 +12,14 @@ use std::str::{self, FromStr}; #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ClosedCaptions { - GroupId(QuotedString), + GroupId(String), None, } impl fmt::Display for ClosedCaptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ClosedCaptions::GroupId(ref x) => x.fmt(f), + match &self { + ClosedCaptions::GroupId(value) => write!(f, "{}", quote(value)), ClosedCaptions::None => "NONE".fmt(f), } } @@ -30,7 +31,7 @@ impl FromStr for ClosedCaptions { if s == "NONE" { Ok(ClosedCaptions::None) } else { - Ok(ClosedCaptions::GroupId(track!(s.parse())?)) + Ok(ClosedCaptions::GroupId(unquote(s))) } } } @@ -44,7 +45,7 @@ mod tests { let closed_captions = ClosedCaptions::None; assert_eq!(closed_captions.to_string(), "NONE".to_string()); - let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap()); + let closed_captions = ClosedCaptions::GroupId("value".into()); assert_eq!(closed_captions.to_string(), "\"value\"".to_string()); } @@ -53,7 +54,7 @@ mod tests { let closed_captions = ClosedCaptions::None; assert_eq!(closed_captions, "NONE".parse::<ClosedCaptions>().unwrap()); - let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap()); + let closed_captions = ClosedCaptions::GroupId("value".into()); assert_eq!( closed_captions, "\"value\"".parse::<ClosedCaptions>().unwrap() diff --git a/src/types/decimal_floating_point.rs b/src/types/decimal_floating_point.rs index 5cd33cc..a829129 100644 --- a/src/types/decimal_floating_point.rs +++ b/src/types/decimal_floating_point.rs @@ -1,8 +1,8 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; use std::time::Duration; -use trackable::error::ErrorKindExt; + +use crate::Error; /// Non-negative decimal floating-point number. /// @@ -10,7 +10,7 @@ use trackable::error::ErrorKindExt; /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct DecimalFloatingPoint(f64); +pub(crate) struct DecimalFloatingPoint(f64); impl DecimalFloatingPoint { /// Makes a new `DecimalFloatingPoint` instance. @@ -19,14 +19,15 @@ impl DecimalFloatingPoint { /// /// The given value must have a positive sign and be finite, /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. - pub fn new(n: f64) -> Result<Self> { - track_assert!(n.is_sign_positive(), ErrorKind::InvalidInput); - track_assert!(n.is_finite(), ErrorKind::InvalidInput); + pub fn new(n: f64) -> crate::Result<Self> { + if !n.is_sign_positive() || !n.is_finite() { + return Err(Error::invalid_input()); + } Ok(DecimalFloatingPoint(n)) } /// Converts `DecimalFloatingPoint` to `f64`. - pub fn as_f64(self) -> f64 { + pub const fn as_f64(self) -> f64 { self.0 } @@ -59,13 +60,13 @@ impl fmt::Display for DecimalFloatingPoint { impl FromStr for DecimalFloatingPoint { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert!( - s.chars().all(|c| c.is_digit(10) || c == '.'), - ErrorKind::InvalidInput - ); - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - Ok(DecimalFloatingPoint(n)) + + fn from_str(input: &str) -> Result<Self, Self::Err> { + if !input.chars().all(|c| c.is_digit(10) || c == '.') { + return Err(Error::invalid_input()); + } + let n = input.parse()?; + DecimalFloatingPoint::new(n) } } diff --git a/src/types/decimal_resolution.rs b/src/types/decimal_resolution.rs index f323577..7d225c6 100644 --- a/src/types/decimal_resolution.rs +++ b/src/types/decimal_resolution.rs @@ -1,7 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; +use std::str::FromStr; + +use crate::Error; /// Decimal resolution. /// @@ -9,12 +9,38 @@ use trackable::error::ErrorKindExt; /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct DecimalResolution { +pub(crate) struct DecimalResolution { + width: usize, + height: usize, +} + +impl DecimalResolution { + /// Creates a new DecimalResolution. + pub const fn new(width: usize, height: usize) -> Self { + Self { width, height } + } + /// Horizontal pixel dimension. - pub width: usize, + pub const fn width(&self) -> usize { + self.width + } + + /// Sets Horizontal pixel dimension. + pub fn set_width(&mut self, value: usize) -> &mut Self { + self.width = value; + self + } /// Vertical pixel dimension. - pub height: usize, + pub const fn height(&self) -> usize { + self.height + } + + /// Sets Vertical pixel dimension. + pub fn set_height(&mut self, value: usize) -> &mut Self { + self.height = value; + self + } } impl fmt::Display for DecimalResolution { @@ -25,13 +51,23 @@ impl fmt::Display for DecimalResolution { impl FromStr for DecimalResolution { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - let mut tokens = s.splitn(2, 'x'); - let width = tokens.next().expect("Never fails"); - let height = track_assert_some!(tokens.next(), ErrorKind::InvalidInput); + + fn from_str(input: &str) -> Result<Self, Self::Err> { + let tokens = input.splitn(2, 'x').collect::<Vec<_>>(); + + if tokens.len() != 2 { + return Err(Error::custom(format!( + "InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}", + input, + ))); + } + + let width = tokens[0]; + let height = tokens[1]; + Ok(DecimalResolution { - width: track!(width.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, - height: track!(height.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?, + width: width.parse()?, + height: height.parse()?, }) } } @@ -42,37 +78,44 @@ mod tests { #[test] fn test_display() { - let decimal_resolution = DecimalResolution { - width: 1920, - height: 1080, - }; - assert_eq!(decimal_resolution.to_string(), "1920x1080".to_string()); + assert_eq!( + DecimalResolution::new(1920, 1080).to_string(), + "1920x1080".to_string() + ); - let decimal_resolution = DecimalResolution { - width: 1280, - height: 720, - }; - assert_eq!(decimal_resolution.to_string(), "1280x720".to_string()); + assert_eq!( + DecimalResolution::new(1280, 720).to_string(), + "1280x720".to_string() + ); } #[test] fn test_parse() { - let decimal_resolution = DecimalResolution { - width: 1920, - height: 1080, - }; assert_eq!( - decimal_resolution, + DecimalResolution::new(1920, 1080), "1920x1080".parse::<DecimalResolution>().unwrap() ); - let decimal_resolution = DecimalResolution { - width: 1280, - height: 720, - }; assert_eq!( - decimal_resolution, + DecimalResolution::new(1280, 720), "1280x720".parse::<DecimalResolution>().unwrap() ); + + assert!("1280".parse::<DecimalResolution>().is_err()); + } + + #[test] + fn test_width() { + assert_eq!(DecimalResolution::new(1920, 1080).width(), 1920); + assert_eq!(DecimalResolution::new(1920, 1080).set_width(12).width(), 12); + } + + #[test] + fn test_height() { + assert_eq!(DecimalResolution::new(1920, 1080).height(), 1080); + assert_eq!( + DecimalResolution::new(1920, 1080).set_height(12).height(), + 12 + ); } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index ac57237..7e82ff9 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -1,27 +1,316 @@ -use crate::attribute::AttributePairs; -use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, QuotedString}; -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; -/// 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)] +use derive_builder::Builder; +use url::Url; + +use crate::attribute::AttributePairs; +use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion}; +use crate::utils::{quote, unquote}; +use crate::Error; + +#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] +#[builder(setter(into))] pub struct DecryptionKey { - pub method: EncryptionMethod, - pub uri: QuotedString, - pub iv: Option<InitializationVector>, - pub key_format: Option<QuotedString>, - pub key_format_versions: Option<QuotedString>, + pub(crate) method: EncryptionMethod, + #[builder(setter(into, strip_option), default)] + pub(crate) uri: Option<Url>, + #[builder(setter(into, strip_option), default)] + pub(crate) iv: Option<InitializationVector>, + #[builder(setter(into, strip_option), default)] + pub(crate) key_format: Option<String>, + #[builder(setter(into, strip_option), default)] + pub(crate) key_format_versions: Option<String>, } impl DecryptionKey { - pub(crate) fn requires_version(&self) -> ProtocolVersion { - if self.key_format.is_some() | self.key_format_versions.is_some() { + /// Makes a new `DecryptionKey`. + /// # Example + /// ``` + /// use url::Url; + /// + /// use hls_m3u8::types::{EncryptionMethod, DecryptionKey}; + /// + /// let key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.to_string(), + /// "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, + } + } + + /// Returns the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.method(), + /// EncryptionMethod::Aes128 + /// ); + /// ``` + pub const fn method(&self) -> EncryptionMethod { + self.method + } + + /// Returns a Builder to build a `DecryptionKey`. + pub fn builder() -> DecryptionKeyBuilder { + DecryptionKeyBuilder::default() + } + + /// Sets the [EncryptionMethod]. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_method(EncryptionMethod::SampleAes); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() + /// ); + /// ``` + pub fn set_method(&mut self, value: EncryptionMethod) { + self.method = value; + } + + /// Returns an `URI` that specifies how to obtain the key. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let key = DecryptionKey::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<Url> { + &self.uri + } + + /// Sets the `URI` attribute. + /// + /// This attribute is required, if the [EncryptionMethod] is not None. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_uri("http://www.google.com".parse().unwrap()); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() + /// ); + /// ``` + pub fn set_uri(&mut self, value: Url) { + self.uri = Some(value); + } + + /// Returns the IV (Initialization Vector) attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::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::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::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(), + /// "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() + /// ); + /// ``` + pub fn set_iv<T>(&mut self, value: T) + where + T: Into<[u8; 16]>, + { + self.iv = Some(InitializationVector(value.into())); + } + + /// 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::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::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<String> { + &self.key_format + } + + /// Sets the `KEYFORMAT` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format("key_format_attribute"); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() + /// ); + /// ``` + pub fn set_key_format<T: ToString>(&mut self, value: T) { + self.key_format = Some(value.to_string()); + } + + /// 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::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::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<String> { + &self.key_format_versions + } + + /// Sets the `KEYFORMATVERSIONS` attribute. + /// + /// This attribute is optional. + /// # Example + /// ``` + /// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// key.set_key_format_versions("1/2/3/4/5"); + /// + /// assert_eq!( + /// key.to_string(), + /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() + /// ); + /// ``` + pub fn set_key_format_versions<T: ToString>(&mut self, value: T) { + self.key_format_versions = Some(value.to_string()); + } + + /// Returns the protocol compatibility version that this tag requires. + /// # Example + /// ``` + /// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion, DecryptionKey}; + /// + /// let mut key = DecryptionKey::new( + /// EncryptionMethod::Aes128, + /// "https://www.example.com".parse().unwrap() + /// ); + /// + /// assert_eq!( + /// key.requires_version(), + /// ProtocolVersion::V1 + /// ); + /// ``` + 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 @@ -31,48 +320,35 @@ impl DecryptionKey { } } -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<Self> { + + fn from_str(input: &str) -> Result<Self, Self::Err> { 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" => method = Some(track!(value.parse())?), - "URI" => uri = Some(track!(value.parse())?), - "IV" => iv = Some(track!(value.parse())?), - "KEYFORMAT" => key_format = Some(track!(value.parse())?), - "KEYFORMATVERSIONS" => key_format_versions = Some(track!(value.parse())?), + + for (key, value) in input.parse::<AttributePairs>()? { + 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 = track_assert_some!(method, ErrorKind::InvalidInput); - let uri = track_assert_some!(uri, ErrorKind::InvalidInput); + + let method = method.ok_or(Error::missing_value("METHOD"))?; + if method != EncryptionMethod::None && uri.is_none() { + return Err(Error::missing_value("URI")); + } + Ok(DecryptionKey { method, uri, @@ -82,3 +358,108 @@ impl FromStr for DecryptionKey { }) } } + +impl fmt::Display for DecryptionKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "METHOD={}", self.method)?; + + if self.method == EncryptionMethod::None { + return Ok(()); + } + if let Some(uri) = &self.uri { + write!(f, ",URI={}", quote(uri))?; + } + if let Some(value) = &self.iv { + 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(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::EncryptionMethod; + + #[test] + fn test_requires_version() { + let key = DecryptionKey::builder() + .method(EncryptionMethod::Aes128) + .uri("https://www.example.com".parse::<Url>().unwrap()) + .iv([ + 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, + ]) + .build() + .unwrap(); + } + + #[test] + fn test_display() { + let mut key = DecryptionKey::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(), + "METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0x10ef8f758ca555115584bb5b3c687f52" + .to_string() + ); + } + + #[test] + fn test_parser() { + assert_eq!( + r#"METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# + .parse::<DecryptionKey>() + .unwrap(), + DecryptionKey::new( + EncryptionMethod::Aes128, + "https://priv.example.com/key.php?r=52".parse().unwrap() + ) + ); + + let mut key = DecryptionKey::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!( + "METHOD=AES-128,\ + URI=\"https://www.example.com/hls-key/key.bin\",\ + IV=0X10ef8f758ca555115584bb5b3c687f52" + .parse::<DecryptionKey>() + .unwrap(), + key + ); + + let mut key = DecryptionKey::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, + ]); + key.set_key_format("baz"); + + assert_eq!( + r#"METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# + .parse::<DecryptionKey>().unwrap(), + key + ) + } +} diff --git a/src/types/encryption_method.rs b/src/types/encryption_method.rs index 971e7bb..a5389f8 100644 --- a/src/types/encryption_method.rs +++ b/src/types/encryption_method.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// Encryption method. /// @@ -10,30 +11,65 @@ use std::str::{self, FromStr}; #[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, } impl fmt::Display for EncryptionMethod { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { + match &self { EncryptionMethod::Aes128 => "AES-128".fmt(f), EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f), + EncryptionMethod::None => "NONE".fmt(f), } } } impl FromStr for EncryptionMethod { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - match s { + + fn from_str(input: &str) -> Result<Self, Self::Err> { + match input { "AES-128" => Ok(EncryptionMethod::Aes128), "SAMPLE-AES" => Ok(EncryptionMethod::SampleAes), - _ => track_panic!( - ErrorKind::InvalidInput, + "NONE" => Ok(EncryptionMethod::None), + _ => Err(Error::custom(format!( "Unknown encryption method: {:?}", - s - ), + input + ))), } } } @@ -44,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::<EncryptionMethod>().unwrap() ); - let encryption_method = EncryptionMethod::SampleAes; assert_eq!( - encryption_method, + EncryptionMethod::SampleAes, "SAMPLE-AES".parse::<EncryptionMethod>().unwrap() ); + + assert_eq!( + EncryptionMethod::None, + "NONE".parse::<EncryptionMethod>().unwrap() + ); } } diff --git a/src/types/hdcp_level.rs b/src/types/hdcp_level.rs index 436178f..741959c 100644 --- a/src/types/hdcp_level.rs +++ b/src/types/hdcp_level.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// HDCP level. /// @@ -16,7 +17,7 @@ pub enum HdcpLevel { impl fmt::Display for HdcpLevel { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { + match &self { HdcpLevel::Type0 => "TYPE-0".fmt(f), HdcpLevel::None => "NONE".fmt(f), } @@ -25,11 +26,12 @@ impl fmt::Display for HdcpLevel { impl FromStr for HdcpLevel { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - match s { + + fn from_str(input: &str) -> Result<Self, Self::Err> { + match input { "TYPE-0" => Ok(HdcpLevel::Type0), "NONE" => Ok(HdcpLevel::None), - _ => track_panic!(ErrorKind::InvalidInput, "Unknown HDCP level: {:?}", s), + _ => Err(Error::custom(format!("Unknown HDCP level: {:?}", input))), } } } diff --git a/src/types/hexadecimal_sequence.rs b/src/types/hexadecimal_sequence.rs deleted file mode 100644 index 930adca..0000000 --- a/src/types/hexadecimal_sequence.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::ops::Deref; -use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; - -/// Hexadecimal sequence. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct HexadecimalSequence(Vec<u8>); - -impl HexadecimalSequence { - /// Makes a new `HexadecimalSequence` instance. - pub fn new<T: Into<Vec<u8>>>(v: T) -> Self { - HexadecimalSequence(v.into()) - } - - /// Converts into the underlying byte sequence. - pub fn into_bytes(self) -> Vec<u8> { - self.0 - } -} - -impl Deref for HexadecimalSequence { - type Target = [u8]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef<[u8]> for HexadecimalSequence { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl fmt::Display for HexadecimalSequence { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "0x")?; - for b in &self.0 { - write!(f, "{:02x}", b)?; - } - Ok(()) - } -} - -impl FromStr for HexadecimalSequence { - type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert!( - s.starts_with("0x") || s.starts_with("0X"), - ErrorKind::InvalidInput - ); - track_assert!(s.len() % 2 == 0, ErrorKind::InvalidInput); - - let mut v = Vec::with_capacity(s.len() / 2 - 1); - for c in s.as_bytes().chunks(2).skip(1) { - let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - let b = - track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - v.push(b); - } - Ok(HexadecimalSequence(v)) - } -} diff --git a/src/types/in_stream_id.rs b/src/types/in_stream_id.rs index 8b4aa56..cdff1f5 100644 --- a/src/types/in_stream_id.rs +++ b/src/types/in_stream_id.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// Identifier of a rendition within the segments in a media playlist. /// @@ -87,8 +88,9 @@ impl fmt::Display for InStreamId { impl FromStr for InStreamId { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - Ok(match s { + + fn from_str(input: &str) -> Result<Self, Self::Err> { + Ok(match input { "CC1" => InStreamId::Cc1, "CC2" => InStreamId::Cc2, "CC3" => InStreamId::Cc3, @@ -156,7 +158,7 @@ impl FromStr for InStreamId { "SERVICE61" => InStreamId::Service61, "SERVICE62" => InStreamId::Service62, "SERVICE63" => InStreamId::Service63, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown instream id: {:?}", s), + _ => return Err(Error::custom(format!("Unknown instream id: {:?}", input))), }) } } diff --git a/src/types/initialization_vector.rs b/src/types/initialization_vector.rs index 9cb64fe..7c64323 100644 --- a/src/types/initialization_vector.rs +++ b/src/types/initialization_vector.rs @@ -1,8 +1,8 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; use std::ops::Deref; -use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; +use std::str::FromStr; + +use crate::Error; /// Initialization vector. /// @@ -12,6 +12,19 @@ use trackable::error::ErrorKindExt; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct InitializationVector(pub [u8; 16]); +impl InitializationVector { + /// Converts the initialization vector to a slice. + 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]; fn deref(&self) -> &Self::Target { @@ -37,20 +50,22 @@ impl fmt::Display for InitializationVector { impl FromStr for InitializationVector { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert!( - s.starts_with("0x") || s.starts_with("0X"), - ErrorKind::InvalidInput - ); - track_assert_eq!(s.len() - 2, 32, ErrorKind::InvalidInput); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if !(s.starts_with("0x") || s.starts_with("0X")) { + return Err(Error::invalid_input()); + } + if s.len() - 2 != 32 { + return Err(Error::invalid_input()); + } let mut v = [0; 16]; for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() { - let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - let b = - track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?; + let d = std::str::from_utf8(c).map_err(|e| Error::custom(e))?; + let b = u8::from_str_radix(d, 16).map_err(|e| Error::custom(e))?; v[i] = b; } + Ok(InitializationVector(v)) } } diff --git a/src/types/media_type.rs b/src/types/media_type.rs index 103abde..5bc10d4 100644 --- a/src/types/media_type.rs +++ b/src/types/media_type.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// Media type. /// @@ -18,7 +19,7 @@ pub enum MediaType { impl fmt::Display for MediaType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { + match &self { MediaType::Audio => "AUDIO".fmt(f), MediaType::Video => "VIDEO".fmt(f), MediaType::Subtitles => "SUBTITLES".fmt(f), @@ -29,13 +30,16 @@ impl fmt::Display for MediaType { impl FromStr for MediaType { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - Ok(match s { + + fn from_str(input: &str) -> Result<Self, Self::Err> { + Ok(match input { "AUDIO" => MediaType::Audio, "VIDEO" => MediaType::Video, "SUBTITLES" => MediaType::Subtitles, "CLOSED-CAPTIONS" => MediaType::ClosedCaptions, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown media type: {:?}", s), + _ => { + return Err(Error::invalid_input()); + } }) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index d3f30d2..6301ba4 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -6,31 +6,21 @@ mod decimal_resolution; mod decryption_key; mod encryption_method; mod hdcp_level; -mod hexadecimal_sequence; mod in_stream_id; mod initialization_vector; mod media_type; -mod playlist_type; mod protocol_version; -mod quoted_string; -mod session_data; mod signed_decimal_floating_point; -mod single_line_string; pub use byte_range::*; pub use closed_captions::*; -pub use decimal_floating_point::*; -pub use decimal_resolution::*; +pub(crate) 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 use media_type::*; -pub use playlist_type::*; pub use protocol_version::*; -pub use quoted_string::*; -pub use session_data::*; -pub use signed_decimal_floating_point::*; -pub use single_line_string::*; +pub(crate) use signed_decimal_floating_point::*; diff --git a/src/types/playlist_type.rs b/src/types/playlist_type.rs deleted file mode 100644 index e9dbb00..0000000 --- a/src/types/playlist_type.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::str::{self, FromStr}; - -/// Playlist type. -/// -/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE] -/// -/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum PlaylistType { - Event, - Vod, -} - -impl fmt::Display for PlaylistType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - PlaylistType::Event => write!(f, "EVENT"), - PlaylistType::Vod => write!(f, "VOD"), - } - } -} - -impl FromStr for PlaylistType { - type Err = Error; - fn from_str(s: &str) -> Result<Self> { - match s { - "EVENT" => Ok(PlaylistType::Event), - "VOD" => Ok(PlaylistType::Vod), - _ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s), - } - } -} diff --git a/src/types/protocol_version.rs b/src/types/protocol_version.rs index 312707b..1d54938 100644 --- a/src/types/protocol_version.rs +++ b/src/types/protocol_version.rs @@ -1,6 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; +use std::str::FromStr; + +use crate::Error; /// [7. Protocol Version Compatibility] /// @@ -16,32 +17,46 @@ pub enum ProtocolVersion { V6, V7, } + +impl ProtocolVersion { + /// Returns the newest ProtocolVersion, that is supported by this library. + pub const fn latest() -> Self { + Self::V7 + } +} + impl fmt::Display for ProtocolVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let n = match *self { - ProtocolVersion::V1 => 1, - ProtocolVersion::V2 => 2, - ProtocolVersion::V3 => 3, - ProtocolVersion::V4 => 4, - ProtocolVersion::V5 => 5, - ProtocolVersion::V6 => 6, - ProtocolVersion::V7 => 7, + let n = { + match &self { + ProtocolVersion::V1 => 1, + ProtocolVersion::V2 => 2, + ProtocolVersion::V3 => 3, + ProtocolVersion::V4 => 4, + ProtocolVersion::V5 => 5, + ProtocolVersion::V6 => 6, + ProtocolVersion::V7 => 7, + } }; write!(f, "{}", n) } } + impl FromStr for ProtocolVersion { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - Ok(match s { - "1" => ProtocolVersion::V1, - "2" => ProtocolVersion::V2, - "3" => ProtocolVersion::V3, - "4" => ProtocolVersion::V4, - "5" => ProtocolVersion::V5, - "6" => ProtocolVersion::V6, - "7" => ProtocolVersion::V7, - _ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s), + + fn from_str(input: &str) -> Result<Self, Self::Err> { + Ok({ + match input { + "1" => ProtocolVersion::V1, + "2" => ProtocolVersion::V2, + "3" => ProtocolVersion::V3, + "4" => ProtocolVersion::V4, + "5" => ProtocolVersion::V5, + "6" => ProtocolVersion::V6, + "7" => ProtocolVersion::V7, + _ => return Err(Error::unknown_protocol_version(input)), + } }) } } diff --git a/src/types/quoted_string.rs b/src/types/quoted_string.rs deleted file mode 100644 index 07c6118..0000000 --- a/src/types/quoted_string.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::{Error, ErrorKind, Result}; -use std::fmt; -use std::ops::Deref; -use std::str::{self, FromStr}; - -/// Quoted string. -/// -/// See: [4.2. Attribute Lists] -/// -/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct QuotedString(String); - -impl QuotedString { - /// Makes a new `QuotedString` instance. - /// - /// # Errors - /// - /// If the given string contains any control characters or double-quote character, - /// this function will return an error which has the kind `ErrorKind::InvalidInput`. - pub fn new<T: Into<String>>(s: T) -> Result<Self> { - let s = s.into(); - track_assert!( - !s.chars().any(|c| c.is_control() || c == '"'), - ErrorKind::InvalidInput - ); - Ok(QuotedString(s)) - } -} - -impl Deref for QuotedString { - type Target = str; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef<str> for QuotedString { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for QuotedString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -impl FromStr for QuotedString { - type Err = Error; - fn from_str(s: &str) -> Result<Self> { - let len = s.len(); - let bytes = s.as_bytes(); - track_assert!(len >= 2, ErrorKind::InvalidInput); - track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput); - track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput); - - let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) }; - track!(QuotedString::new(s)) - } -} diff --git a/src/types/session_data.rs b/src/types/session_data.rs deleted file mode 100644 index 25c90e1..0000000 --- a/src/types/session_data.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::types::QuotedString; - -/// 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(QuotedString), - Uri(QuotedString), -} diff --git a/src/types/signed_decimal_floating_point.rs b/src/types/signed_decimal_floating_point.rs index 3c50998..a10d893 100644 --- a/src/types/signed_decimal_floating_point.rs +++ b/src/types/signed_decimal_floating_point.rs @@ -1,7 +1,7 @@ -use crate::{Error, ErrorKind, Result}; use std::fmt; -use std::str::{self, FromStr}; -use trackable::error::ErrorKindExt; +use std::str::FromStr; + +use crate::Error; /// Signed decimal floating-point number. /// @@ -9,7 +9,7 @@ use trackable::error::ErrorKindExt; /// /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct SignedDecimalFloatingPoint(f64); +pub(crate) struct SignedDecimalFloatingPoint(f64); impl SignedDecimalFloatingPoint { /// Makes a new `SignedDecimalFloatingPoint` instance. @@ -18,13 +18,16 @@ impl SignedDecimalFloatingPoint { /// /// The given value must be finite, /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. - pub fn new(n: f64) -> Result<Self> { - track_assert!(n.is_finite(), ErrorKind::InvalidInput); - Ok(SignedDecimalFloatingPoint(n)) + pub fn new(n: f64) -> crate::Result<Self> { + if !n.is_finite() { + Err(Error::invalid_input()) + } else { + Ok(SignedDecimalFloatingPoint(n)) + } } /// Converts `DecimalFloatingPoint` to `f64`. - pub fn as_f64(self) -> f64 { + pub const fn as_f64(self) -> f64 { self.0 } } @@ -45,12 +48,8 @@ impl fmt::Display for SignedDecimalFloatingPoint { impl FromStr for SignedDecimalFloatingPoint { type Err = Error; - fn from_str(s: &str) -> Result<Self> { - track_assert!( - s.chars().all(|c| c.is_digit(10) || c == '.' || c == '-'), - ErrorKind::InvalidInput - ); - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; - Ok(SignedDecimalFloatingPoint(n)) + + fn from_str(input: &str) -> Result<Self, Self::Err> { + SignedDecimalFloatingPoint::new(input.parse().map_err(Error::parse_float_error)?) } } diff --git a/src/types/single_line_string.rs b/src/types/single_line_string.rs deleted file mode 100644 index 3f99d35..0000000 --- a/src/types/single_line_string.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::{ErrorKind, Result}; -use std::fmt; -use std::ops::Deref; - -/// String that represents a single line in a playlist file. -/// -/// See: [4.1. Definition of a Playlist] -/// -/// [4.1. Definition of a Playlist]: https://tools.ietf.org/html/rfc8216#section-4.1 -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SingleLineString(String); - -impl SingleLineString { - /// Makes a new `SingleLineString` instance. - /// - /// # Errors - /// - /// If the given string contains any control characters, - /// this function will return an error which has the kind `ErrorKind::InvalidInput`. - pub fn new<T: Into<String>>(s: T) -> Result<Self> { - let s = s.into(); - track_assert!(!s.chars().any(|c| c.is_control()), ErrorKind::InvalidInput); - Ok(SingleLineString(s)) - } -} - -impl Deref for SingleLineString { - type Target = str; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef<str> for SingleLineString { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for SingleLineString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn single_line_string() { - assert!(SingleLineString::new("foo").is_ok()); - assert!(SingleLineString::new("b\rar").is_err()); - } -} diff --git a/src/utils.rs b/src/utils.rs index b313c66..eeda232 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,15 +1,103 @@ -use crate::{ErrorKind, Result}; -use trackable::error::ErrorKindExt; +use crate::Error; -pub fn parse_yes_or_no(s: &str) -> Result<bool> { - match s { +pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> { + match s.as_ref() { "YES" => Ok(true), "NO" => Ok(false), - _ => track_panic!(ErrorKind::InvalidInput, "Unexpected value: {:?}", s), + _ => Err(Error::invalid_input()), } } -pub fn parse_u64(s: &str) -> Result<u64> { - let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?; +pub(crate) fn parse_u64<T: AsRef<str>>(s: T) -> crate::Result<u64> { + let n = s.as_ref().parse().map_err(Error::unknown)?; // TODO: Error::number Ok(n) } + +/// According to the documentation the following characters are forbidden +/// inside a quoted string: +/// - carriage return (`\r`) +/// - new line (`\n`) +/// - double quotes (`"`) +/// +/// Therefore it is safe to simply remove any occurence of those characters. +/// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2) +pub(crate) fn unquote<T: ToString>(value: T) -> String { + value + .to_string() + .replace("\"", "") + .replace("\n", "") + .replace("\r", "") +} + +/// Puts a string inside quotes. +pub(crate) fn quote<T: ToString>(value: T) -> String { + // the replace is for the case, that quote is called on an already quoted string, which could + // cause problems! + format!("\"{}\"", value.to_string().replace("\"", "")) +} + +/// Checks, if the given tag is at the start of the input. If this is the case, it will remove it +/// and return the rest of the input. +/// +/// # Error +/// This function will return `Error::MissingTag`, if the input doesn't start with the tag, that +/// has been passed to this function. +pub(crate) fn tag<T>(input: &str, tag: T) -> crate::Result<&str> +where + T: AsRef<str>, +{ + if !input.starts_with(tag.as_ref()) { + return Err(Error::missing_tag(tag.as_ref(), input)); + } + let result = input.split_at(tag.as_ref().len()).1; + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_yes_or_no() { + assert!(parse_yes_or_no("YES").unwrap()); + assert!(!parse_yes_or_no("NO").unwrap()); + // TODO: test for error + } + + #[test] + fn test_parse_u64() { + assert_eq!(parse_u64("1").unwrap(), 1); + assert_eq!(parse_u64("25").unwrap(), 25); + // TODO: test for error + } + + #[test] + fn test_unquote() { + assert_eq!(unquote("\"TestValue\""), "TestValue".to_string()); + assert_eq!(unquote("\"TestValue\n\""), "TestValue".to_string()); + assert_eq!(unquote("\"TestValue\n\r\""), "TestValue".to_string()); + } + + #[test] + fn test_quote() { + assert_eq!(quote("value"), "\"value\"".to_string()); + assert_eq!(quote("\"value\""), "\"value\"".to_string()); + } + + #[test] + fn test_tag() { + let input = "HelloMyFriendThisIsASampleString"; + + let input = tag(input, "Hello").unwrap(); + assert_eq!(input, "MyFriendThisIsASampleString"); + + let input = tag(input, "My").unwrap(); + assert_eq!(input, "FriendThisIsASampleString"); + + let input = tag(input, "FriendThisIs").unwrap(); + assert_eq!(input, "ASampleString"); + + let input = tag(input, "A").unwrap(); + assert_eq!(input, "SampleString"); + } +} diff --git a/tests/playlist.rs b/tests/playlist.rs new file mode 100644 index 0000000..b61068a --- /dev/null +++ b/tests/playlist.rs @@ -0,0 +1,46 @@ +//! Credits go to +//! - https://github.com/globocom/m3u8/blob/master/tests/playlists.py +use hls_m3u8::tags::*; +use hls_m3u8::MediaPlaylist; + +use std::time::Duration; + +#[test] +fn test_simple_playlist() { + let playlist = r#" + #EXTM3U + #EXT-X-TARGETDURATION:5220 + #EXTINF:0, + http://media.example.com/entire1.ts + #EXTINF:5220, + http://media.example.com/entire2.ts + #EXT-X-ENDLIST"#; + + let media_playlist = playlist.parse::<MediaPlaylist>().unwrap(); + assert_eq!( + media_playlist.target_duration_tag(), + ExtXTargetDuration::new(Duration::from_secs(5220)) + ); + + assert_eq!(media_playlist.segments().len(), 2); + + assert_eq!( + media_playlist.segments()[0].inf_tag(), + &ExtInf::new(Duration::from_secs(0)) + ); + + assert_eq!( + media_playlist.segments()[1].inf_tag(), + &ExtInf::new(Duration::from_secs(5220)) + ); + + assert_eq!( + media_playlist.segments()[0].uri(), + &"http://media.example.com/entire1.ts".parse().unwrap() + ); + + assert_eq!( + media_playlist.segments()[1].uri(), + &"http://media.example.com/entire2.ts".parse().unwrap() + ); +}