diff --git a/src/line.rs b/src/line.rs index 26dae79..90cb3b4 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::fmt; use std::str::FromStr; @@ -12,7 +13,7 @@ pub(crate) struct Lines<'a> { } impl<'a> Iterator for Lines<'a> { - type Item = crate::Result; + type Item = crate::Result>; fn next(&mut self) -> Option { let mut stream_inf = false; @@ -32,10 +33,7 @@ impl<'a> Iterator for Lines<'a> { continue; } else if line.starts_with("#EXT") { - match line.parse() { - Ok(value) => return Some(Ok(Line::Tag(value))), - Err(e) => return Some(Err(e)), - } + return Some(Tag::try_from(line).map(Line::Tag)); } else if line.starts_with('#') { continue; // ignore comments } else { @@ -44,17 +42,15 @@ impl<'a> Iterator for Lines<'a> { stream_inf = false; if let Some(first_line) = stream_inf_line { - match format!("{}\n{}", first_line, line).parse() { - Ok(value) => { - return Some(Ok(Line::Tag(value))); - } - Err(e) => return Some(Err(e)), - } + return Some( + tags::ExtXStreamInf::from_str(&format!("{}\n{}", first_line, line)) + .map(|v| Line::Tag(Tag::ExtXStreamInf(v))), + ); } else { continue; } } else { - return Some(Ok(Line::Uri(line.to_string()))); + return Some(Ok(Line::Uri(line))); } } } @@ -73,15 +69,14 @@ impl<'a> From<&'a str> for Lines<'a> { } #[derive(Debug, Clone, PartialEq)] -pub(crate) enum Line { - Tag(Tag), - Uri(String), +pub(crate) enum Line<'a> { + Tag(Tag<'a>), + Uri(&'a str), } #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)] -pub(crate) enum Tag { - ExtM3u(tags::ExtM3u), +pub(crate) enum Tag<'a> { ExtXVersion(tags::ExtXVersion), ExtInf(tags::ExtInf), ExtXByteRange(tags::ExtXByteRange), @@ -103,13 +98,12 @@ pub(crate) enum Tag { ExtXSessionKey(tags::ExtXSessionKey), ExtXIndependentSegments(tags::ExtXIndependentSegments), ExtXStart(tags::ExtXStart), - Unknown(String), + Unknown(&'a str), } -impl fmt::Display for Tag { +impl<'a> fmt::Display for Tag<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { - Self::ExtM3u(value) => value.fmt(f), Self::ExtXVersion(value) => value.fmt(f), Self::ExtInf(value) => value.fmt(f), Self::ExtXByteRange(value) => value.fmt(f), @@ -136,13 +130,11 @@ impl fmt::Display for Tag { } } -impl FromStr for Tag { - type Err = Error; +impl<'a> TryFrom<&'a str> for Tag<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { - if input.starts_with(tags::ExtM3u::PREFIX) { - input.parse().map(Self::ExtM3u) - } else if input.starts_with(tags::ExtXVersion::PREFIX) { + fn try_from(input: &'a str) -> Result { + if input.starts_with(tags::ExtXVersion::PREFIX) { input.parse().map(Self::ExtXVersion) } else if input.starts_with(tags::ExtInf::PREFIX) { input.parse().map(Self::ExtInf) @@ -185,7 +177,7 @@ impl FromStr for Tag { } else if input.starts_with(tags::ExtXStart::PREFIX) { input.parse().map(Self::ExtXStart) } else { - Ok(Self::Unknown(input.to_string())) + Ok(Self::Unknown(input)) } } } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index d21ef1a..cb88465 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -11,6 +11,7 @@ use crate::tags::{ ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, }; use crate::types::{ClosedCaptions, MediaType, ProtocolVersion}; +use crate::utils::tag; use crate::{Error, RequiredVersion}; /// Master playlist. @@ -263,6 +264,7 @@ impl FromStr for MasterPlaylist { type Err = Error; fn from_str(input: &str) -> Result { + let input = tag(input, ExtM3u::PREFIX)?; let mut builder = Self::builder(); let mut media_tags = vec![]; @@ -272,23 +274,15 @@ impl FromStr for MasterPlaylist { let mut session_key_tags = vec![]; let mut unknown_tags = vec![]; - for (i, line) in Lines::from(input).enumerate() { + for line in Lines::from(input) { match line? { Line::Tag(tag) => { - if i == 0 { - if tag != Tag::ExtM3u(ExtM3u) { - return Err(Error::invalid_input()); - } - continue; - } match tag { - Tag::ExtM3u(_) => { - return Err(Error::invalid_input()); - } Tag::ExtXVersion(_) => { // This tag can be ignored, because the // MasterPlaylist will automatically set the - // ExtXVersion tag to correct version! + // ExtXVersion tag to the minimum required version + // TODO: this might be verified? } Tag::ExtInf(_) | Tag::ExtXByteRange(_) diff --git a/src/media_playlist.rs b/src/media_playlist.rs index cf8b05d..ea9bf53 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -12,6 +12,7 @@ use crate::tags::{ ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, }; use crate::types::ProtocolVersion; +use crate::utils::tag; use crate::{Encrypted, Error, RequiredVersion}; /// Media playlist. @@ -276,6 +277,8 @@ fn parse_media_playlist( input: &str, builder: &mut MediaPlaylistBuilder, ) -> crate::Result { + let input = tag(input, "#EXTM3U")?; + let mut segment = MediaSegment::builder(); let mut segments = vec![]; @@ -285,17 +288,10 @@ fn parse_media_playlist( let mut available_key_tags: Vec = vec![]; - for (i, line) in Lines::from(input).enumerate() { + for line in Lines::from(input) { match line? { Line::Tag(tag) => { - if i == 0 { - if tag != Tag::ExtM3u(ExtM3u) { - return Err(Error::custom("m3u8 doesn't start with #EXTM3U")); - } - continue; - } match tag { - Tag::ExtM3u(_) => return Err(Error::invalid_input()), Tag::ExtInf(t) => { has_partial_segment = true; segment.inf_tag(t); diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index f930ddc..b23032e 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -11,31 +11,12 @@ use crate::{Error, RequiredVersion}; /// Playlist file. /// It is the at the start of every [`Media Playlist`] and [`Master Playlist`]. /// -/// # Examples -/// -/// Parsing from a [`str`]: -/// -/// ``` -/// # use hls_m3u8::tags::ExtM3u; -/// # -/// assert_eq!("#EXTM3U".parse::()?, ExtM3u); -/// # Ok::<(), Box>(()) -/// ``` -/// -/// Converting to a [`str`]: -/// -/// ``` -/// # use hls_m3u8::tags::ExtM3u; -/// # -/// assert_eq!("#EXTM3U".to_string(), ExtM3u.to_string()); -/// ``` -/// /// [`Media Playlist`]: crate::MediaPlaylist /// [`Master Playlist`]: crate::MasterPlaylist /// [`M3U`]: https://en.wikipedia.org/wiki/M3U /// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] -pub struct ExtM3u; +pub(crate) struct ExtM3u; impl ExtM3u { pub(crate) const PREFIX: &'static str = "#EXTM3U"; diff --git a/src/tags/basic/mod.rs b/src/tags/basic/mod.rs index e23eb19..642560b 100644 --- a/src/tags/basic/mod.rs +++ b/src/tags/basic/mod.rs @@ -1,5 +1,5 @@ mod m3u; mod version; -pub use m3u::*; +pub(crate) use m3u::*; pub use version::*; diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 9af41ca..ce1b733 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -66,23 +66,7 @@ impl FromStr for ExtXByteRange { 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 = { - if tokens.len() == 2 { - Some(tokens[1].parse()?) - } else { - None - } - }; - - Ok(Self::new(length, start)) + Ok(Self(ByteRange::from_str(input)?)) } } diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 60af89d..39d4591 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -151,29 +151,14 @@ impl FromStr for ExtInf { type Err = Error; fn from_str(input: &str) -> Result { - let input = tag(input, Self::PREFIX)?; - let tokens = input.splitn(2, ',').collect::>(); + let mut input = tag(input, Self::PREFIX)?.splitn(2, ','); - if tokens.is_empty() { - return Err(Error::custom(format!( - "failed to parse #EXTINF tag, couldn't split input: {:?}", - input - ))); - } - - let duration = Duration::from_secs_f64(tokens[0].parse()?); - - let title = { - if tokens.len() >= 2 { - if tokens[1].trim().is_empty() { - None - } else { - Some(tokens[1].to_string()) - } - } else { - None - } - }; + let duration = Duration::from_secs_f64(input.next().unwrap().parse()?); + let title = input + .next() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + .map(|value| value.to_string()); Ok(Self { duration, title }) } diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index 8ea8ddc..7d07045 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -74,22 +74,15 @@ impl fmt::Display for ByteRange { impl FromStr for ByteRange { type Err = Error; - fn from_str(s: &str) -> Result { - let tokens = s.splitn(2, '@').collect::>(); + fn from_str(input: &str) -> Result { + let mut input = input.splitn(2, '@'); - if tokens.is_empty() { - return Err(Error::invalid_input()); - } + let length = input + .next() + .ok_or_else(|| Error::custom("missing length for #EXT-X-BYTERANGE")) + .and_then(|s| s.parse().map_err(Error::parse_int))?; - let length = tokens[0].parse()?; - - let start = { - if tokens.len() == 2 { - Some(tokens[1].parse()?) - } else { - None - } - }; + let start = input.next().map(str::parse).transpose()?; Ok(Self::new(length, start)) } diff --git a/src/types/channels.rs b/src/types/channels.rs index e8136d3..56c1520 100644 --- a/src/types/channels.rs +++ b/src/types/channels.rs @@ -83,17 +83,17 @@ impl FromStr for Channels { type Err = Error; fn from_str(input: &str) -> Result { - let parameters = input.split('/').collect::>(); + let mut parameters = input.split('/'); let channel_number = parameters - .first() + .next() .ok_or_else(|| Error::missing_attribute("first parameter of channels"))? .parse() .map_err(Error::parse_int)?; Ok(Self { channel_number, - unknown: parameters[1..].iter().map(|v| v.to_string()).collect(), + unknown: parameters.map(|v| (*v).to_string()).collect(), }) } } diff --git a/src/types/resolution.rs b/src/types/resolution.rs index b4ad382..26ad26e 100644 --- a/src/types/resolution.rs +++ b/src/types/resolution.rs @@ -36,19 +36,19 @@ impl FromStr for Resolution { type Err = Error; fn from_str(input: &str) -> Result { - let tokens = input.splitn(2, 'x').collect::>(); + let mut input = input.splitn(2, 'x'); - if tokens.len() != 2 { - return Err(Error::custom(format!( - "InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}", - input, - ))); - } + let width = input + .next() + .ok_or_else(|| Error::custom("missing width for `Resolution` or an invalid input")) + .and_then(|v| v.parse().map_err(Error::parse_int))?; - Ok(Self { - width: tokens[0].parse().map_err(Error::parse_int)?, - height: tokens[1].parse().map_err(Error::parse_int)?, - }) + let height = input + .next() + .ok_or_else(|| Error::custom("missing height for `Resolution` or an invalid input")) + .and_then(|v| v.parse().map_err(Error::parse_int))?; + + Ok(Self { width, height }) } }