From c4643c708376127171cd51a66fb48ac4ac4c3842 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Wed, 22 Apr 2020 09:54:48 +0200 Subject: [PATCH] Use `Cow<'a, str>` to reduce clones #52 --- Cargo.toml | 1 + benches/benchmarks/media_playlist.rs | 19 ++- src/attribute.rs | 4 +- src/lib.rs | 25 +-- src/line.rs | 62 +++---- src/master_playlist.rs | 102 ++++++++---- src/media_playlist.rs | 91 ++++++++--- src/media_segment.rs | 53 ++++-- src/tags/basic/m3u.rs | 12 +- src/tags/basic/version.rs | 10 +- src/tags/master_playlist/media.rs | 154 ++++++++---------- src/tags/master_playlist/session_data.rs | 91 +++++++---- src/tags/master_playlist/session_key.rs | 36 ++-- src/tags/master_playlist/variant_stream.rs | 86 +++++++--- .../media_playlist/discontinuity_sequence.rs | 12 +- src/tags/media_playlist/end_list.rs | 13 +- src/tags/media_playlist/i_frames_only.rs | 15 +- src/tags/media_playlist/media_sequence.rs | 10 +- src/tags/media_playlist/target_duration.rs | 10 +- src/tags/media_segment/byte_range.rs | 14 +- src/tags/media_segment/date_range.rs | 102 +++++++----- src/tags/media_segment/discontinuity.rs | 15 +- src/tags/media_segment/inf.rs | 67 +++++--- src/tags/media_segment/key.rs | 60 ++++--- src/tags/media_segment/map.rs | 61 ++++--- src/tags/media_segment/program_date_time.rs | 52 ++++-- src/tags/shared/independent_segments.rs | 10 +- src/tags/shared/start.rs | 16 +- src/traits.rs | 10 +- src/types/byte_range.rs | 29 ++-- src/types/closed_captions.rs | 42 +++-- src/types/codecs.rs | 108 ++++++++++-- src/types/decryption_key.rs | 52 ++++-- src/types/playlist_type.rs | 18 +- src/types/stream_data.rs | 52 ++++-- src/types/value.rs | 70 +++++--- src/utils.rs | 29 +++- tests/master_playlist.rs | 4 +- tests/media_playlist.rs | 3 +- tests/rfc8216.rs | 3 +- 40 files changed, 1045 insertions(+), 578 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dda81a5..4c57382 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ thiserror = "1.0" derive_more = "0.99" shorthand = "0.1" strum = { version = "0.17", features = ["derive"] } + stable-vec = { version = "0.4" } [dev-dependencies] diff --git a/benches/benchmarks/media_playlist.rs b/benches/benchmarks/media_playlist.rs index 46ad550..84af6b9 100644 --- a/benches/benchmarks/media_playlist.rs +++ b/benches/benchmarks/media_playlist.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::str::FromStr; use std::time::Duration; @@ -58,7 +59,7 @@ fn create_manifest_data() -> Vec { builder.build().unwrap().to_string().into_bytes() } -fn criterion_benchmark(c: &mut Criterion) { +fn media_playlist_from_str(c: &mut Criterion) { let data = String::from_utf8(create_manifest_data()).unwrap(); let mut group = c.benchmark_group("MediaPlaylist::from_str"); @@ -72,4 +73,18 @@ fn criterion_benchmark(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, criterion_benchmark); +fn media_playlist_try_from(c: &mut Criterion) { + let data = String::from_utf8(create_manifest_data()).unwrap(); + + let mut group = c.benchmark_group("MediaPlaylist::try_from"); + + group.throughput(Throughput::Bytes(data.len() as u64)); + + group.bench_function("MediaPlaylist::try_from", |b| { + b.iter(|| MediaPlaylist::try_from(black_box(data.as_str())).unwrap()); + }); + + group.finish(); +} + +criterion_group!(benches, media_playlist_from_str, media_playlist_try_from); diff --git a/src/attribute.rs b/src/attribute.rs index 797e915..d21fcc0 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -30,7 +30,7 @@ impl<'a> Iterator for AttributePairs<'a> { // NOTE: it is okay to add 1 to the index, because an `=` is exactly 1 byte. self.index = end + 1; - ::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() + &self.string[start..end] }; let value = { @@ -66,7 +66,7 @@ impl<'a> Iterator for AttributePairs<'a> { self.index += end; self.index -= start; - ::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap() + &self.string[start..end] }; Some((key, value)) diff --git a/src/lib.rs b/src/lib.rs index 65bfe30..ec31b4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,19 +42,22 @@ //! //! ``` //! use hls_m3u8::MediaPlaylist; +//! use std::convert::TryFrom; //! -//! let m3u8 = "#EXTM3U -//! #EXT-X-TARGETDURATION:10 -//! #EXT-X-VERSION:3 -//! #EXTINF:9.009, -//! http://media.example.com/first.ts -//! #EXTINF:9.009, -//! http://media.example.com/second.ts -//! #EXTINF:3.003, -//! http://media.example.com/third.ts -//! #EXT-X-ENDLIST"; +//! let m3u8 = MediaPlaylist::try_from(concat!( +//! "#EXTM3U\n", +//! "#EXT-X-TARGETDURATION:10\n", +//! "#EXT-X-VERSION:3\n", +//! "#EXTINF:9.009,\n", +//! "http://media.example.com/first.ts\n", +//! "#EXTINF:9.009,\n", +//! "http://media.example.com/second.ts\n", +//! "#EXTINF:3.003,\n", +//! "http://media.example.com/third.ts\n", +//! "#EXT-X-ENDLIST", +//! )); //! -//! assert!(m3u8.parse::().is_ok()); +//! assert!(m3u8.is_ok()); //! ``` //! //! ## Crate Feature Flags diff --git a/src/line.rs b/src/line.rs index c684585..6713c28 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,6 +1,5 @@ use core::convert::TryFrom; use core::iter::FusedIterator; -use core::str::FromStr; use derive_more::Display; @@ -23,7 +22,8 @@ impl<'a> Iterator for Lines<'a> { let uri = self.lines.next()?; Some( - tags::VariantStream::from_str(&format!("{}\n{}", line, uri)) + tags::VariantStream::try_from(format!("{}\n{}", line, uri).as_str()) + .map(|v| v.into_owned()) .map(|v| Line::Tag(Tag::VariantStream(v))), ) } else if line.starts_with("#EXT") { @@ -60,25 +60,25 @@ pub(crate) enum Line<'a> { #[display(fmt = "{}")] pub(crate) enum Tag<'a> { ExtXVersion(tags::ExtXVersion), - ExtInf(tags::ExtInf), + ExtInf(tags::ExtInf<'a>), ExtXByteRange(tags::ExtXByteRange), ExtXDiscontinuity(tags::ExtXDiscontinuity), - ExtXKey(tags::ExtXKey), - ExtXMap(tags::ExtXMap), - ExtXProgramDateTime(tags::ExtXProgramDateTime), - ExtXDateRange(tags::ExtXDateRange), + ExtXKey(tags::ExtXKey<'a>), + ExtXMap(tags::ExtXMap<'a>), + ExtXProgramDateTime(tags::ExtXProgramDateTime<'a>), + ExtXDateRange(tags::ExtXDateRange<'a>), ExtXTargetDuration(tags::ExtXTargetDuration), ExtXMediaSequence(tags::ExtXMediaSequence), ExtXDiscontinuitySequence(tags::ExtXDiscontinuitySequence), ExtXEndList(tags::ExtXEndList), PlaylistType(PlaylistType), ExtXIFramesOnly(tags::ExtXIFramesOnly), - ExtXMedia(tags::ExtXMedia), - ExtXSessionData(tags::ExtXSessionData), - ExtXSessionKey(tags::ExtXSessionKey), + ExtXMedia(tags::ExtXMedia<'a>), + ExtXSessionData(tags::ExtXSessionData<'a>), + ExtXSessionKey(tags::ExtXSessionKey<'a>), ExtXIndependentSegments(tags::ExtXIndependentSegments), ExtXStart(tags::ExtXStart), - VariantStream(tags::VariantStream), + VariantStream(tags::VariantStream<'a>), Unknown(&'a str), } @@ -87,47 +87,47 @@ impl<'a> TryFrom<&'a str> for Tag<'a> { fn try_from(input: &'a str) -> Result { if input.starts_with(tags::ExtXVersion::PREFIX) { - input.parse().map(Self::ExtXVersion) + TryFrom::try_from(input).map(Self::ExtXVersion) } else if input.starts_with(tags::ExtInf::PREFIX) { - input.parse().map(Self::ExtInf) + TryFrom::try_from(input).map(Self::ExtInf) } else if input.starts_with(tags::ExtXByteRange::PREFIX) { - input.parse().map(Self::ExtXByteRange) + TryFrom::try_from(input).map(Self::ExtXByteRange) } else if input.starts_with(tags::ExtXDiscontinuity::PREFIX) { - input.parse().map(Self::ExtXDiscontinuity) + TryFrom::try_from(input).map(Self::ExtXDiscontinuity) } else if input.starts_with(tags::ExtXKey::PREFIX) { - input.parse().map(Self::ExtXKey) + TryFrom::try_from(input).map(Self::ExtXKey) } else if input.starts_with(tags::ExtXMap::PREFIX) { - input.parse().map(Self::ExtXMap) + TryFrom::try_from(input).map(Self::ExtXMap) } else if input.starts_with(tags::ExtXProgramDateTime::PREFIX) { - input.parse().map(Self::ExtXProgramDateTime) + TryFrom::try_from(input).map(Self::ExtXProgramDateTime) } else if input.starts_with(tags::ExtXTargetDuration::PREFIX) { - input.parse().map(Self::ExtXTargetDuration) + TryFrom::try_from(input).map(Self::ExtXTargetDuration) } else if input.starts_with(tags::ExtXDateRange::PREFIX) { - input.parse().map(Self::ExtXDateRange) + TryFrom::try_from(input).map(Self::ExtXDateRange) } else if input.starts_with(tags::ExtXMediaSequence::PREFIX) { - input.parse().map(Self::ExtXMediaSequence) + TryFrom::try_from(input).map(Self::ExtXMediaSequence) } else if input.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) { - input.parse().map(Self::ExtXDiscontinuitySequence) + TryFrom::try_from(input).map(Self::ExtXDiscontinuitySequence) } else if input.starts_with(tags::ExtXEndList::PREFIX) { - input.parse().map(Self::ExtXEndList) + TryFrom::try_from(input).map(Self::ExtXEndList) } else if input.starts_with(PlaylistType::PREFIX) { - input.parse().map(Self::PlaylistType) + TryFrom::try_from(input).map(Self::PlaylistType) } else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) { - input.parse().map(Self::ExtXIFramesOnly) + TryFrom::try_from(input).map(Self::ExtXIFramesOnly) } else if input.starts_with(tags::ExtXMedia::PREFIX) { - input.parse().map(Self::ExtXMedia) + TryFrom::try_from(input).map(Self::ExtXMedia) } else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME) || input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) { - input.parse().map(Self::VariantStream) + TryFrom::try_from(input).map(Self::VariantStream) } else if input.starts_with(tags::ExtXSessionData::PREFIX) { - input.parse().map(Self::ExtXSessionData) + TryFrom::try_from(input).map(Self::ExtXSessionData) } else if input.starts_with(tags::ExtXSessionKey::PREFIX) { - input.parse().map(Self::ExtXSessionKey) + TryFrom::try_from(input).map(Self::ExtXSessionKey) } else if input.starts_with(tags::ExtXIndependentSegments::PREFIX) { - input.parse().map(Self::ExtXIndependentSegments) + TryFrom::try_from(input).map(Self::ExtXIndependentSegments) } else if input.starts_with(tags::ExtXStart::PREFIX) { - input.parse().map(Self::ExtXStart) + TryFrom::try_from(input).map(Self::ExtXStart) } else { Ok(Self::Unknown(input)) } diff --git a/src/master_playlist.rs b/src/master_playlist.rs index df28c74..efd73d6 100644 --- a/src/master_playlist.rs +++ b/src/master_playlist.rs @@ -1,6 +1,7 @@ +use std::borrow::Cow; use std::collections::HashSet; +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use derive_builder::Builder; @@ -24,11 +25,11 @@ use crate::{Error, RequiredVersion}; /// A [`MasterPlaylist`] can be parsed from a `str`: /// /// ``` -/// use core::str::FromStr; +/// use core::convert::TryFrom; /// use hls_m3u8::MasterPlaylist; /// /// // the concat! macro joins multiple `&'static str`. -/// let master_playlist = concat!( +/// let master_playlist = MasterPlaylist::try_from(concat!( /// "#EXTM3U\n", /// "#EXT-X-STREAM-INF:", /// "BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n", @@ -44,8 +45,7 @@ use crate::{Error, RequiredVersion}; /// "http://example.com/high/index.m3u8\n", /// "#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n", /// "http://example.com/audio/index.m3u8\n" -/// ) -/// .parse::()?; +/// ))?; /// /// println!("{}", master_playlist.has_independent_segments); /// # Ok::<(), hls_m3u8::Error>(()) @@ -98,7 +98,7 @@ use crate::{Error, RequiredVersion}; #[builder(build_fn(validate = "Self::validate"))] #[builder(setter(into, strip_option))] #[non_exhaustive] -pub struct MasterPlaylist { +pub struct MasterPlaylist<'a> { /// Indicates that all media samples in a [`MediaSegment`] can be /// decoded without information from other segments. /// @@ -135,14 +135,14 @@ pub struct MasterPlaylist { /// /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] - pub media: Vec, + pub media: Vec>, /// A list of all streams of this [`MasterPlaylist`]. /// /// ### Note /// /// This field is optional. #[builder(default)] - pub variant_streams: Vec, + pub variant_streams: Vec>, /// The [`ExtXSessionData`] tag allows arbitrary session data to be /// carried in a [`MasterPlaylist`]. /// @@ -150,7 +150,7 @@ pub struct MasterPlaylist { /// /// This field is optional. #[builder(default)] - pub session_data: Vec, + pub session_data: Vec>, /// A list of [`ExtXSessionKey`]s, that allows the client to preload /// these keys without having to read the [`MediaPlaylist`]s first. /// @@ -160,17 +160,17 @@ pub struct MasterPlaylist { /// /// [`MediaPlaylist`]: crate::MediaPlaylist #[builder(default)] - pub session_keys: Vec, + pub session_keys: Vec>, /// A list of all tags that could not be identified while parsing the input. /// /// ### Note /// /// This field is optional. #[builder(default)] - pub unknown_tags: Vec, + pub unknown_tags: Vec>, } -impl MasterPlaylist { +impl<'a> MasterPlaylist<'a> { /// Returns a builder for a [`MasterPlaylist`]. /// /// # Example @@ -216,10 +216,10 @@ impl MasterPlaylist { /// ``` #[must_use] #[inline] - pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } + pub fn builder() -> MasterPlaylistBuilder<'a> { MasterPlaylistBuilder::default() } /// Returns all streams, which have an audio group id. - pub fn audio_streams(&self) -> impl Iterator { + pub fn audio_streams(&self) -> impl Iterator> { self.variant_streams.iter().filter(|stream| { if let VariantStream::ExtXStreamInf { audio: Some(_), .. } = stream { true @@ -230,7 +230,7 @@ impl MasterPlaylist { } /// Returns all streams, which have a video group id. - pub fn video_streams(&self) -> impl Iterator { + pub fn video_streams(&self) -> impl Iterator> { self.variant_streams.iter().filter(|stream| { if let VariantStream::ExtXStreamInf { stream_data, .. } = stream { stream_data.video().is_some() @@ -243,7 +243,7 @@ impl MasterPlaylist { } /// Returns all streams, which have no group id. - pub fn unassociated_streams(&self) -> impl Iterator { + pub fn unassociated_streams(&self) -> impl Iterator> { self.variant_streams.iter().filter(|stream| { if let VariantStream::ExtXStreamInf { stream_data, @@ -263,17 +263,52 @@ impl MasterPlaylist { } /// Returns all `ExtXMedia` tags, associated with the provided stream. - pub fn associated_with<'a>( - &'a self, - stream: &'a VariantStream, - ) -> impl Iterator + 'a { + pub fn associated_with<'b>( + &'b self, + stream: &'b VariantStream<'_>, + ) -> impl Iterator> + 'b { self.media .iter() .filter(move |media| stream.is_associated(media)) } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> MasterPlaylist<'static> { + MasterPlaylist { + has_independent_segments: self.has_independent_segments, + start: self.start, + media: self.media.into_iter().map(|v| v.into_owned()).collect(), + variant_streams: self + .variant_streams + .into_iter() + .map(|v| v.into_owned()) + .collect(), + session_data: self + .session_data + .into_iter() + .map(|v| v.into_owned()) + .collect(), + session_keys: self + .session_keys + .into_iter() + .map(|v| v.into_owned()) + .collect(), + unknown_tags: self + .unknown_tags + .into_iter() + .map(|v| Cow::Owned(v.into_owned())) + .collect(), + } + } } -impl RequiredVersion for MasterPlaylist { +impl<'a> RequiredVersion for MasterPlaylist<'a> { fn required_version(&self) -> ProtocolVersion { required_version![ self.has_independent_segments @@ -287,7 +322,7 @@ impl RequiredVersion for MasterPlaylist { } } -impl MasterPlaylistBuilder { +impl<'a> MasterPlaylistBuilder<'a> { fn validate(&self) -> Result<(), String> { if let Some(variant_streams) = &self.variant_streams { self.validate_variants(variant_streams) @@ -300,7 +335,7 @@ impl MasterPlaylistBuilder { Ok(()) } - fn validate_variants(&self, variant_streams: &[VariantStream]) -> crate::Result<()> { + fn validate_variants(&self, variant_streams: &[VariantStream<'_>]) -> crate::Result<()> { let mut closed_captions_none = false; for variant in variant_streams { @@ -382,7 +417,7 @@ impl MasterPlaylistBuilder { fn check_media_group>(&self, media_type: MediaType, group_id: T) -> bool { if let Some(value) = &self.media { value.iter().any(|media| { - media.media_type == media_type && media.group_id().as_str() == group_id.as_ref() + media.media_type == media_type && media.group_id().as_ref() == group_id.as_ref() }) } else { false @@ -390,7 +425,7 @@ impl MasterPlaylistBuilder { } } -impl RequiredVersion for MasterPlaylistBuilder { +impl<'a> RequiredVersion for MasterPlaylistBuilder<'a> { fn required_version(&self) -> ProtocolVersion { // TODO: the .flatten() can be removed as soon as `recursive traits` are // supported. (RequiredVersion is implemented for Option, but @@ -409,7 +444,7 @@ impl RequiredVersion for MasterPlaylistBuilder { } } -impl fmt::Display for MasterPlaylist { +impl<'a> fmt::Display for MasterPlaylist<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; @@ -449,10 +484,10 @@ impl fmt::Display for MasterPlaylist { } } -impl FromStr for MasterPlaylist { - type Err = Error; +impl<'a> TryFrom<&'a str> for MasterPlaylist<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &'a str) -> Result { let input = tag(input, ExtM3u::PREFIX)?; let mut builder = Self::builder(); @@ -505,10 +540,10 @@ impl FromStr for MasterPlaylist { Tag::ExtXStart(t) => { builder.start(t); } - _ => { + Tag::Unknown(value) => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. - unknown_tags.push(tag.to_string()); + unknown_tags.push(Cow::Borrowed(value)); } } } @@ -604,7 +639,7 @@ mod tests { #[test] fn test_parser() { assert_eq!( - concat!( + MasterPlaylist::try_from(concat!( "#EXTM3U\n", "#EXT-X-STREAM-INF:", "BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n", @@ -620,8 +655,7 @@ mod tests { "http://example.com/high/index.m3u8\n", "#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n", "http://example.com/audio/index.m3u8\n" - ) - .parse::() + )) .unwrap(), MasterPlaylist::builder() .variant_streams(vec![ diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 80068ea..20a8164 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -1,4 +1,6 @@ +use std::borrow::Cow; use std::collections::HashSet; +use std::convert::TryFrom; use std::fmt; use std::str::FromStr; use std::time::Duration; @@ -23,7 +25,7 @@ use crate::{Error, RequiredVersion}; #[derive(Builder, Debug, Clone, PartialEq, Eq)] #[builder(build_fn(skip), setter(strip_option))] #[non_exhaustive] -pub struct MediaPlaylist { +pub struct MediaPlaylist<'a> { /// Specifies the maximum [`MediaSegment::duration`]. A typical target /// duration is 10 seconds. /// @@ -106,7 +108,7 @@ pub struct MediaPlaylist { /// /// This field is required. #[builder(setter(custom))] - pub segments: StableVec, + pub segments: StableVec>, /// The allowable excess duration of each media segment in the /// associated playlist. /// @@ -129,10 +131,10 @@ pub struct MediaPlaylist { /// /// This field is optional. #[builder(default, setter(into))] - pub unknown: Vec, + pub unknown: Vec>, } -impl MediaPlaylistBuilder { +impl<'a> MediaPlaylistBuilder<'a> { fn validate(&self) -> Result<(), String> { if let Some(target_duration) = &self.target_duration { self.validate_media_segments(*target_duration) @@ -225,7 +227,7 @@ impl MediaPlaylistBuilder { /// Adds a media segment to the resulting playlist and assigns the next free /// [`MediaSegment::number`] to the segment. - pub fn push_segment(&mut self, segment: MediaSegment) -> &mut Self { + pub fn push_segment(&mut self, segment: MediaSegment<'a>) -> &mut Self { let segments = self.segments.get_or_insert_with(StableVec::new); if segment.explicit_number { @@ -239,7 +241,7 @@ impl MediaPlaylistBuilder { } /// Parse the rest of the [`MediaPlaylist`] from an m3u8 file. - pub fn parse(&mut self, input: &str) -> crate::Result { + pub fn parse(&mut self, input: &'a str) -> crate::Result> { parse_media_playlist(input, self) } @@ -253,8 +255,8 @@ impl MediaPlaylistBuilder { /// number has been set explicitly. This function assumes, that all segments /// will be present in the final media playlist and the following is only /// possible if the segment is marked with `ExtXDiscontinuity`. - pub fn segments(&mut self, segments: Vec) -> &mut Self { - let mut vec = StableVec::::with_capacity(segments.len()); + pub fn segments(&mut self, segments: Vec>) -> &mut Self { + let mut vec = StableVec::>::with_capacity(segments.len()); let mut remaining = Vec::with_capacity(segments.len()); for segment in segments { @@ -278,7 +280,7 @@ impl MediaPlaylistBuilder { /// # Errors /// /// If a required field has not been initialized. - pub fn build(&self) -> Result { + pub fn build(&self) -> Result, String> { // validate builder self.validate()?; @@ -371,7 +373,7 @@ impl MediaPlaylistBuilder { } } -impl RequiredVersion for MediaPlaylistBuilder { +impl<'a> RequiredVersion for MediaPlaylistBuilder<'a> { fn required_version(&self) -> ProtocolVersion { required_version![ self.target_duration.map(ExtXTargetDuration), @@ -393,11 +395,11 @@ impl RequiredVersion for MediaPlaylistBuilder { } } -impl MediaPlaylist { +impl<'a> MediaPlaylist<'a> { /// Returns a builder for [`MediaPlaylist`]. #[must_use] #[inline] - pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() } + pub fn builder() -> MediaPlaylistBuilder<'a> { MediaPlaylistBuilder::default() } /// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment /// duration together. @@ -405,9 +407,42 @@ impl MediaPlaylist { pub fn duration(&self) -> Duration { self.segments.values().map(|s| s.duration.duration()).sum() } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> MediaPlaylist<'static> { + MediaPlaylist { + target_duration: self.target_duration, + media_sequence: self.media_sequence, + discontinuity_sequence: self.discontinuity_sequence, + playlist_type: self.playlist_type, + has_i_frames_only: self.has_i_frames_only, + has_independent_segments: self.has_independent_segments, + start: self.start, + has_end_list: self.has_end_list, + segments: { + self.segments + .into_iter() + .map(|(_, s)| s.into_owned()) + .collect() + }, + allowable_excess_duration: self.allowable_excess_duration, + unknown: { + self.unknown + .into_iter() + .map(|v| Cow::Owned(v.into_owned())) + .collect() + }, + } + } } -impl RequiredVersion for MediaPlaylist { +impl<'a> RequiredVersion for MediaPlaylist<'a> { fn required_version(&self) -> ProtocolVersion { required_version![ ExtXTargetDuration(self.target_duration), @@ -425,7 +460,7 @@ impl RequiredVersion for MediaPlaylist { } } -impl fmt::Display for MediaPlaylist { +impl<'a> fmt::Display for MediaPlaylist<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{}", ExtM3u)?; @@ -463,7 +498,7 @@ impl fmt::Display for MediaPlaylist { writeln!(f, "{}", value)?; } - let mut available_keys = HashSet::::new(); + let mut available_keys = HashSet::>::new(); for segment in self.segments.values() { for key in &segment.keys { @@ -530,10 +565,10 @@ impl fmt::Display for MediaPlaylist { } } -fn parse_media_playlist( - input: &str, - builder: &mut MediaPlaylistBuilder, -) -> crate::Result { +fn parse_media_playlist<'a>( + input: &'a str, + builder: &mut MediaPlaylistBuilder<'a>, +) -> crate::Result> { let input = tag(input, "#EXTM3U")?; let mut segment = MediaSegment::builder(); @@ -656,10 +691,10 @@ fn parse_media_playlist( builder.start(t); } Tag::ExtXVersion(_) => {} - Tag::Unknown(_) => { + Tag::Unknown(s) => { // [6.3.1. General Client Responsibilities] // > ignore any unrecognized tags. - unknown.push(tag.to_string()); + unknown.push(Cow::Borrowed(s)); } } } @@ -684,10 +719,18 @@ fn parse_media_playlist( builder.build().map_err(Error::builder) } -impl FromStr for MediaPlaylist { +impl FromStr for MediaPlaylist<'static> { type Err = Error; fn from_str(input: &str) -> Result { + Ok(parse_media_playlist(input, &mut Self::builder())?.into_owned()) + } +} + +impl<'a> TryFrom<&'a str> for MediaPlaylist<'a> { + type Error = Error; + + fn try_from(input: &'a str) -> Result { parse_media_playlist(input, &mut Self::builder()) } } @@ -713,7 +756,7 @@ mod tests { ); // Error (allowable segment duration = target duration = 8) - assert!(playlist.parse::().is_err()); + assert!(MediaPlaylist::try_from(playlist).is_err()); // Error (allowable segment duration = 9) assert!(MediaPlaylist::builder() @@ -819,6 +862,6 @@ mod tests { #[test] fn test_empty_playlist() { let playlist = ""; - assert!(playlist.parse::().is_err()); + assert!(MediaPlaylist::try_from(playlist).is_err()); } } diff --git a/src/media_segment.rs b/src/media_segment.rs index 046b60b..7213ef9 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt; use derive_builder::Builder; @@ -34,7 +35,7 @@ use crate::{Decryptable, RequiredVersion}; #[derive(ShortHand, Debug, Clone, Builder, PartialEq, Eq, PartialOrd, Ord, Hash)] #[builder(setter(strip_option))] #[shorthand(enable(must_use, skip))] -pub struct MediaSegment { +pub struct MediaSegment<'a> { /// Each [`MediaSegment`] has a number, which allows synchronization between /// different variants. /// @@ -80,7 +81,7 @@ pub struct MediaSegment { /// [`KeyFormat`]: crate::types::KeyFormat /// [`EncryptionMethod`]: crate::types::EncryptionMethod #[builder(default, setter(into))] - pub keys: Vec, + pub keys: Vec>, /// This field specifies how to obtain the Media Initialization Section /// required to parse the applicable `MediaSegment`s. /// @@ -94,7 +95,7 @@ pub struct MediaSegment { /// /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly #[builder(default)] - pub map: Option, + pub map: Option>, /// This field indicates that a `MediaSegment` is a sub-range of the /// resource identified by its URI. /// @@ -110,7 +111,7 @@ pub struct MediaSegment { /// /// This field is optional. #[builder(default)] - pub date_range: Option, + pub date_range: Option>, /// This field indicates a discontinuity between the `MediaSegment` that /// follows it and the one that preceded it. /// @@ -134,14 +135,14 @@ pub struct MediaSegment { /// /// This field is optional. #[builder(default)] - pub program_date_time: Option, + pub program_date_time: Option>, /// This field indicates the duration of a media segment. /// /// ## Note /// /// This field is required. #[builder(setter(into))] - pub duration: ExtInf, + pub duration: ExtInf<'a>, /// The URI of a media segment. /// /// ## Note @@ -149,10 +150,10 @@ pub struct MediaSegment { /// This field is required. #[builder(setter(into))] #[shorthand(enable(into), disable(skip))] - uri: String, + uri: Cow<'a, str>, } -impl MediaSegment { +impl<'a> MediaSegment<'a> { /// Returns a builder for a [`MediaSegment`]. /// /// # Example @@ -173,12 +174,34 @@ impl MediaSegment { /// ``` #[must_use] #[inline] - pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } + pub fn builder() -> MediaSegmentBuilder<'static> { MediaSegmentBuilder::default() } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> MediaSegment<'static> { + MediaSegment { + number: self.number, + explicit_number: self.explicit_number, + keys: self.keys.into_iter().map(|k| k.into_owned()).collect(), + map: self.map.map(|v| v.into_owned()), + byte_range: self.byte_range, + date_range: self.date_range.map(|v| v.into_owned()), + has_discontinuity: self.has_discontinuity, + program_date_time: self.program_date_time.map(|v| v.into_owned()), + duration: self.duration.into_owned(), + uri: Cow::Owned(self.uri.into_owned()), + } + } } -impl MediaSegmentBuilder { +impl<'a> MediaSegmentBuilder<'a> { /// Pushes an [`ExtXKey`] tag. - pub fn push_key>(&mut self, value: VALUE) -> &mut Self { + pub fn push_key>>(&mut self, value: VALUE) -> &mut Self { if let Some(keys) = &mut self.keys { keys.push(value.into()); } else { @@ -201,7 +224,7 @@ impl MediaSegmentBuilder { } } -impl fmt::Display for MediaSegment { +impl<'a> fmt::Display for MediaSegment<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // NOTE: self.keys will be printed by the `MediaPlaylist` to prevent redundance. @@ -231,7 +254,7 @@ impl fmt::Display for MediaSegment { } } -impl RequiredVersion for MediaSegment { +impl<'a> RequiredVersion for MediaSegment<'a> { fn required_version(&self) -> ProtocolVersion { required_version![ self.keys, @@ -251,8 +274,8 @@ impl RequiredVersion for MediaSegment { } } -impl Decryptable for MediaSegment { - fn keys(&self) -> Vec<&DecryptionKey> { +impl<'a> Decryptable<'a> for MediaSegment<'a> { + fn keys(&self) -> Vec<&DecryptionKey<'a>> { // self.keys.iter().filter_map(ExtXKey::as_ref).collect() } diff --git a/src/tags/basic/m3u.rs b/src/tags/basic/m3u.rs index 03bbf09..4913745 100644 --- a/src/tags/basic/m3u.rs +++ b/src/tags/basic/m3u.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -28,10 +28,10 @@ impl fmt::Display for ExtM3u { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX) } } -impl FromStr for ExtM3u { - type Err = Error; +impl TryFrom<&str> for ExtM3u { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { tag(input, Self::PREFIX)?; Ok(Self) } @@ -49,8 +49,8 @@ mod test { #[test] fn test_parser() { - assert_eq!("#EXTM3U".parse::().unwrap(), ExtM3u); - assert!("#EXTM2U".parse::().is_err()); + assert_eq!(ExtM3u::try_from("#EXTM3U").unwrap(), ExtM3u); + assert!(ExtM3u::try_from("#EXTM2U").is_err()); } #[test] diff --git a/src/tags/basic/version.rs b/src/tags/basic/version.rs index 6345cf7..ccb3d94 100644 --- a/src/tags/basic/version.rs +++ b/src/tags/basic/version.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -67,10 +67,10 @@ impl From for ExtXVersion { fn from(value: ProtocolVersion) -> Self { Self(value) } } -impl FromStr for ExtXVersion { - type Err = Error; +impl TryFrom<&str> for ExtXVersion { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { let version = tag(input, Self::PREFIX)?.parse()?; Ok(Self::new(version)) } @@ -92,7 +92,7 @@ mod test { #[test] fn test_parser() { assert_eq!( - "#EXT-X-VERSION:6".parse::().unwrap(), + ExtXVersion::try_from("#EXT-X-VERSION:6").unwrap(), ExtXVersion::new(ProtocolVersion::V6) ); } diff --git a/src/tags/master_playlist/media.rs b/src/tags/master_playlist/media.rs index a6aa8a5..e77fa8e 100644 --- a/src/tags/master_playlist/media.rs +++ b/src/tags/master_playlist/media.rs @@ -1,5 +1,6 @@ +use std::borrow::Cow; +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use derive_builder::Builder; use shorthand::ShortHand; @@ -21,7 +22,7 @@ use crate::{Error, RequiredVersion}; #[shorthand(enable(must_use, into))] #[builder(setter(into))] #[builder(build_fn(validate = "Self::validate"))] -pub struct ExtXMedia { +pub struct ExtXMedia<'a> { /// The [`MediaType`] associated with this tag. /// /// ### Note @@ -31,24 +32,7 @@ pub struct ExtXMedia { pub media_type: MediaType, /// An `URI` to a [`MediaPlaylist`]. /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel"); - /// # assert_eq!(media.uri(), None); - /// - /// media.set_uri(Some("https://www.example.com/stream1.m3u8")); - /// - /// assert_eq!( - /// media.uri(), - /// Some(&"https://www.example.com/stream1.m3u8".to_string()) - /// ); - /// ``` - /// - /// # Note + /// ### Note /// /// - This field is required, if the [`ExtXMedia::media_type`] is /// [`MediaType::Subtitles`]. @@ -64,65 +48,39 @@ pub struct ExtXMedia { /// [`VariantStream::ExtXStreamInf`]: /// crate::tags::VariantStream::ExtXStreamInf #[builder(setter(strip_option), default)] - uri: Option, + uri: Option>, /// The identifier that specifies the group to which the rendition /// belongs. /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel"); - /// - /// media.set_group_id("ag2"); - /// - /// assert_eq!(media.group_id(), &"ag2".to_string()); - /// ``` - /// - /// # Note + /// ### Note /// /// This field is required. - group_id: String, + group_id: Cow<'a, str>, /// The name of the primary language used in the rendition. /// The value has to conform to [`RFC5646`]. /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::tags::ExtXMedia; - /// use hls_m3u8::types::MediaType; - /// - /// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel"); - /// - /// media.set_language(Some("en")); - /// - /// assert_eq!(media.language(), Some(&"en".to_string())); - /// ``` - /// - /// # Note + /// ### Note /// /// This field is optional. /// /// [`RFC5646`]: https://tools.ietf.org/html/rfc5646 #[builder(setter(strip_option), default)] - language: Option, + language: Option>, /// The name of a language associated with the rendition. /// An associated language is often used in a different role, than the /// language specified by the [`language`] field (e.g., written versus /// spoken, or a fallback dialect). /// - /// # Note + /// ### Note /// /// This field is optional. /// /// [`language`]: #method.language #[builder(setter(strip_option), default)] - assoc_language: Option, + assoc_language: Option>, /// A human-readable description of the rendition. /// - /// # Note + /// ### Note /// /// This field is required. /// @@ -130,7 +88,7 @@ pub struct ExtXMedia { /// that language. /// /// [`language`]: #method.language - name: String, + name: Cow<'a, str>, /// The value of the `default` flag. /// A value of `true` indicates, that the client should play /// this rendition of the content in the absence of information @@ -189,13 +147,13 @@ pub struct ExtXMedia { /// /// The characteristics field may include private UTIs. /// - /// # Note + /// ### Note /// /// This field is optional. /// /// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI #[builder(setter(strip_option), default)] - characteristics: Option, + characteristics: Option>, /// A count of audio channels indicating the maximum number of independent, /// simultaneous audio channels present in any [`MediaSegment`] in the /// rendition. @@ -214,7 +172,7 @@ pub struct ExtXMedia { pub channels: Option, } -impl ExtXMediaBuilder { +impl<'a> ExtXMediaBuilder<'a> { fn validate(&self) -> Result<(), String> { // A MediaType is always required! let media_type = self @@ -254,7 +212,7 @@ impl ExtXMediaBuilder { } } -impl ExtXMedia { +impl<'a> ExtXMedia<'a> { pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:"; /// Makes a new [`ExtXMedia`] tag with the associated [`MediaType`], the @@ -275,8 +233,8 @@ impl ExtXMedia { #[must_use] pub fn new(media_type: MediaType, group_id: T, name: K) -> Self where - T: Into, - K: Into, + T: Into>, + K: Into>, { Self { media_type, @@ -317,22 +275,47 @@ impl ExtXMedia { /// "public.accessibility.describes-music-and-sound" /// )) /// .build()?; - /// # Ok::<(), Box>(()) + /// # Ok::<(), String>(()) /// ``` #[must_use] - pub fn builder() -> ExtXMediaBuilder { ExtXMediaBuilder::default() } + #[inline] + pub fn builder() -> ExtXMediaBuilder<'a> { ExtXMediaBuilder::default() } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> ExtXMedia<'static> { + ExtXMedia { + media_type: self.media_type, + uri: self.uri.map(|v| Cow::Owned(v.into_owned())), + group_id: Cow::Owned(self.group_id.into_owned()), + language: self.language.map(|v| Cow::Owned(v.into_owned())), + assoc_language: self.assoc_language.map(|v| Cow::Owned(v.into_owned())), + name: Cow::Owned(self.name.into_owned()), + is_default: self.is_default, + is_autoselect: self.is_autoselect, + is_forced: self.is_forced, + instream_id: self.instream_id, + characteristics: self.characteristics.map(|v| Cow::Owned(v.into_owned())), + channels: self.channels, + } + } } /// This tag requires either `ProtocolVersion::V1` or if there is an /// `instream_id` it requires it's version. -impl RequiredVersion for ExtXMedia { +impl<'a> RequiredVersion for ExtXMedia<'a> { fn required_version(&self) -> ProtocolVersion { self.instream_id .map_or(ProtocolVersion::V1, |i| i.required_version()) } } -impl fmt::Display for ExtXMedia { +impl<'a> fmt::Display for ExtXMedia<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "TYPE={}", self.media_type)?; @@ -380,10 +363,10 @@ impl fmt::Display for ExtXMedia { } } -impl FromStr for ExtXMedia { - type Err = Error; +impl<'a> TryFrom<&'a str> for ExtXMedia<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &'a str) -> Result { let input = tag(input, Self::PREFIX)?; let mut builder = Self::builder(); @@ -455,7 +438,7 @@ mod test { #[test] fn test_parser() { $( - assert_eq!($struct, $str.parse().unwrap()); + assert_eq!($struct, TryFrom::try_from($str).unwrap()); )+ } } @@ -754,25 +737,28 @@ mod test { #[test] fn test_parser_error() { - assert!("".parse::().is_err()); - assert!("garbage".parse::().is_err()); + assert_eq!(ExtXMedia::try_from("").is_err(), true); + assert_eq!(ExtXMedia::try_from("garbage").is_err(), true); - assert!( - "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\"" - .parse::() - .is_err() + assert_eq!( + ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\"") + .is_err(), + true + ); + assert_eq!( + ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,INSTREAM-ID=CC1").is_err(), + true ); - assert!("#EXT-X-MEDIA:TYPE=AUDIO,INSTREAM-ID=CC1" - .parse::() - .is_err()); - assert!("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO" - .parse::() - .is_err()); + assert_eq!( + ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO").is_err(), + true + ); - assert!("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES" - .parse::() - .is_err()); + assert_eq!( + ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES").is_err(), + true + ); } #[test] diff --git a/src/tags/master_playlist/session_data.rs b/src/tags/master_playlist/session_data.rs index f7a52bf..937bf2f 100644 --- a/src/tags/master_playlist/session_data.rs +++ b/src/tags/master_playlist/session_data.rs @@ -1,5 +1,6 @@ +use std::borrow::Cow; +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use derive_builder::Builder; use shorthand::ShortHand; @@ -11,7 +12,7 @@ use crate::{Error, RequiredVersion}; /// The data of [`ExtXSessionData`]. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum SessionData { +pub enum SessionData<'a> { /// Contains the data identified by the [`ExtXSessionData::data_id`]. /// /// If a [`language`] is specified, this variant should contain a @@ -19,12 +20,28 @@ pub enum SessionData { /// /// [`data_id`]: ExtXSessionData::data_id /// [`language`]: ExtXSessionData::language - Value(String), + Value(Cow<'a, str>), /// An [`URI`], which points to a [`json`] file. /// /// [`json`]: https://tools.ietf.org/html/rfc8259 /// [`URI`]: https://tools.ietf.org/html/rfc3986 - Uri(String), + Uri(Cow<'a, str>), +} + +impl<'a> SessionData<'a> { + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> SessionData<'static> { + match self { + Self::Value(v) => SessionData::Value(Cow::Owned(v.into_owned())), + Self::Uri(v) => SessionData::Uri(Cow::Owned(v.into_owned())), + } + } } /// Allows arbitrary session data to be carried in a [`MasterPlaylist`]. @@ -33,7 +50,7 @@ pub enum SessionData { #[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] #[builder(setter(into))] #[shorthand(enable(must_use, into))] -pub struct ExtXSessionData { +pub struct ExtXSessionData<'a> { /// This should conform to a [reverse DNS] naming convention, such as /// `com.example.movie.title`. /// @@ -45,7 +62,7 @@ pub struct ExtXSessionData { /// This field is required. /// /// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation - data_id: String, + data_id: Cow<'a, str>, /// The [`SessionData`] associated with the /// [`data_id`](ExtXSessionData::data_id). /// @@ -53,7 +70,7 @@ pub struct ExtXSessionData { /// /// This field is required. #[shorthand(enable(skip))] - pub data: SessionData, + pub data: SessionData<'a>, /// The `language` attribute identifies the language of the [`SessionData`]. /// /// # Note @@ -62,11 +79,11 @@ pub struct ExtXSessionData { /// [RFC5646]. /// /// [RFC5646]: https://tools.ietf.org/html/rfc5646 - #[builder(setter(into, strip_option), default)] - language: Option, + #[builder(setter(strip_option), default)] + language: Option>, } -impl ExtXSessionData { +impl<'a> ExtXSessionData<'a> { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:"; /// Makes a new [`ExtXSessionData`] tag. @@ -83,7 +100,7 @@ impl ExtXSessionData { /// ); /// ``` #[must_use] - pub fn new>(data_id: T, data: SessionData) -> Self { + pub fn new>>(data_id: T, data: SessionData<'a>) -> Self { Self { data_id: data_id.into(), data, @@ -107,7 +124,7 @@ impl ExtXSessionData { /// # Ok::<(), String>(()) /// ``` #[must_use] - pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() } + pub fn builder() -> ExtXSessionDataBuilder<'a> { ExtXSessionDataBuilder::default() } /// Makes a new [`ExtXSessionData`] tag, with the given language. /// @@ -124,10 +141,10 @@ impl ExtXSessionData { /// ); /// ``` #[must_use] - pub fn with_language(data_id: T, data: SessionData, language: K) -> Self + pub fn with_language(data_id: T, data: SessionData<'a>, language: K) -> Self where - T: Into, - K: Into, + T: Into>, + K: Into>, { Self { data_id: data_id.into(), @@ -135,14 +152,29 @@ impl ExtXSessionData { language: Some(language.into()), } } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> ExtXSessionData<'static> { + ExtXSessionData { + data_id: Cow::Owned(self.data_id.into_owned()), + data: self.data.into_owned(), + language: self.language.map(|v| Cow::Owned(v.into_owned())), + } + } } /// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for ExtXSessionData { +impl<'a> RequiredVersion for ExtXSessionData<'a> { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } -impl fmt::Display for ExtXSessionData { +impl<'a> fmt::Display for ExtXSessionData<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "DATA-ID={}", quote(&self.data_id))?; @@ -160,10 +192,10 @@ impl fmt::Display for ExtXSessionData { } } -impl FromStr for ExtXSessionData { - type Err = Error; +impl<'a> TryFrom<&'a str> for ExtXSessionData<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &'a str) -> Result { let input = tag(input, Self::PREFIX)?; let mut data_id = None; @@ -228,28 +260,26 @@ mod test { #[test] fn test_parser() { $( - assert_eq!($struct, $str.parse().unwrap()); + assert_eq!($struct, TryFrom::try_from($str).unwrap()); )+ assert!( - concat!( + ExtXSessionData::try_from(concat!( "#EXT-X-SESSION-DATA:", "DATA-ID=\"foo\",", "LANGUAGE=\"baz\"" - ) - .parse::() + )) .is_err() ); assert!( - concat!( + ExtXSessionData::try_from(concat!( "#EXT-X-SESSION-DATA:", "DATA-ID=\"foo\",", "LANGUAGE=\"baz\",", "VALUE=\"VALUE\",", "URI=\"https://www.example.com/\"" - ) - .parse::() + )) .is_err() ); } @@ -300,11 +330,8 @@ mod test { #[test] fn test_required_version() { assert_eq!( - ExtXSessionData::new( - "com.example.lyrics", - SessionData::Uri("lyrics.json".to_string()) - ) - .required_version(), + ExtXSessionData::new("com.example.lyrics", SessionData::Uri("lyrics.json".into())) + .required_version(), ProtocolVersion::V1 ); } diff --git a/src/tags/master_playlist/session_key.rs b/src/tags/master_playlist/session_key.rs index 0534b0d..0895a9b 100644 --- a/src/tags/master_playlist/session_key.rs +++ b/src/tags/master_playlist/session_key.rs @@ -1,6 +1,5 @@ use core::convert::TryFrom; use std::fmt; -use std::str::FromStr; use derive_more::{AsMut, AsRef, From}; @@ -22,9 +21,9 @@ use crate::{Error, RequiredVersion}; /// [`MasterPlaylist`]: crate::MasterPlaylist /// [`ExtXKey`]: crate::tags::ExtXKey #[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ExtXSessionKey(pub DecryptionKey); +pub struct ExtXSessionKey<'a>(pub DecryptionKey<'a>); -impl ExtXSessionKey { +impl<'a> ExtXSessionKey<'a> { pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:"; /// Makes a new [`ExtXSessionKey`] tag. @@ -42,13 +41,24 @@ impl ExtXSessionKey { /// ``` #[must_use] #[inline] - pub const fn new(inner: DecryptionKey) -> Self { Self(inner) } + pub const fn new(inner: DecryptionKey<'a>) -> Self { Self(inner) } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + /// + /// [`Cow`]: std::borrow::Cow + #[must_use] + pub fn into_owned(self) -> ExtXSessionKey<'static> { ExtXSessionKey(self.0.into_owned()) } } -impl TryFrom for ExtXSessionKey { +impl<'a> TryFrom> for ExtXSessionKey<'a> { type Error = Error; - fn try_from(value: ExtXKey) -> Result { + fn try_from(value: ExtXKey<'a>) -> Result { if let ExtXKey(Some(inner)) = value { Ok(Self(inner)) } else { @@ -59,21 +69,21 @@ impl TryFrom for ExtXSessionKey { /// This tag requires the same [`ProtocolVersion`] that is returned by /// `DecryptionKey::required_version`. -impl RequiredVersion for ExtXSessionKey { +impl<'a> RequiredVersion for ExtXSessionKey<'a> { fn required_version(&self) -> ProtocolVersion { self.0.required_version() } } -impl fmt::Display for ExtXSessionKey { +impl<'a> fmt::Display for ExtXSessionKey<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0.to_string()) } } -impl FromStr for ExtXSessionKey { - type Err = Error; +impl<'a> TryFrom<&'a str> for ExtXSessionKey<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { - Ok(Self(DecryptionKey::from_str(tag(input, Self::PREFIX)?)?)) + fn try_from(input: &'a str) -> Result { + Ok(Self(DecryptionKey::try_from(tag(input, Self::PREFIX)?)?)) } } @@ -95,7 +105,7 @@ mod test { #[test] fn test_parser() { $( - assert_eq!($struct, $str.parse().unwrap()); + assert_eq!($struct, TryFrom::try_from($str).unwrap()); )+ } } diff --git a/src/tags/master_playlist/variant_stream.rs b/src/tags/master_playlist/variant_stream.rs index 64af9dd..19e6c76 100644 --- a/src/tags/master_playlist/variant_stream.rs +++ b/src/tags/master_playlist/variant_stream.rs @@ -1,6 +1,7 @@ +use core::convert::TryFrom; use core::fmt; use core::ops::Deref; -use core::str::FromStr; +use std::borrow::Cow; use crate::attribute::AttributePairs; use crate::tags::ExtXMedia; @@ -67,7 +68,7 @@ use crate::Error; /// [`PlaylistType`]: crate::types::PlaylistType /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -pub enum VariantStream { +pub enum VariantStream<'a> { /// The [`VariantStream::ExtXIFrame`] variant identifies a [`MediaPlaylist`] /// file containing the I-frames of a multimedia presentation. /// It stands alone, in that it does not apply to a particular URI in the @@ -85,14 +86,14 @@ pub enum VariantStream { /// /// [`MediaPlaylist`]: crate::MediaPlaylist /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly - uri: String, + uri: Cow<'a, str>, /// Some fields are shared between [`VariantStream::ExtXStreamInf`] and /// [`VariantStream::ExtXIFrame`]. /// /// # Note /// /// This field is optional. - stream_data: StreamData, + stream_data: StreamData<'a>, }, /// [`VariantStream::ExtXStreamInf`] specifies a [`VariantStream`], which is /// a set of renditions that can be combined to play the presentation. @@ -106,7 +107,7 @@ pub enum VariantStream { /// This field is required. /// /// [`MediaPlaylist`]: crate::MediaPlaylist - uri: String, + uri: Cow<'a, str>, /// The value is an unsigned float describing the maximum frame /// rate for all the video in the [`VariantStream`]. /// @@ -132,7 +133,7 @@ pub enum VariantStream { /// [`MasterPlaylist`]: crate::MasterPlaylist /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type /// [`MediaType::Audio`]: crate::types::MediaType::Audio - audio: Option, + audio: Option>, /// It indicates the set of subtitle renditions that can be used when /// playing the presentation. /// @@ -149,25 +150,25 @@ pub enum VariantStream { /// [`MasterPlaylist`]: crate::MasterPlaylist /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type /// [`MediaType::Subtitles`]: crate::types::MediaType::Subtitles - subtitles: Option, + subtitles: Option>, /// It indicates the set of closed-caption renditions that can be used /// when playing the presentation. /// /// # Note /// /// This field is optional. - closed_captions: Option, + closed_captions: Option>, /// Some fields are shared between [`VariantStream::ExtXStreamInf`] and /// [`VariantStream::ExtXIFrame`]. /// /// # Note /// /// This field is optional. - stream_data: StreamData, + stream_data: StreamData<'a>, }, } -impl VariantStream { +impl<'a> VariantStream<'a> { pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:"; @@ -203,7 +204,7 @@ impl VariantStream { /// )); /// ``` #[must_use] - pub fn is_associated(&self, media: &ExtXMedia) -> bool { + pub fn is_associated(&self, media: &ExtXMedia<'_>) -> bool { match &self { Self::ExtXIFrame { stream_data, .. } => { if let MediaType::Video = media.media_type { @@ -238,10 +239,45 @@ impl VariantStream { } } } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> VariantStream<'static> { + match self { + VariantStream::ExtXIFrame { uri, stream_data } => { + VariantStream::ExtXIFrame { + uri: Cow::Owned(uri.into_owned()), + stream_data: stream_data.into_owned(), + } + } + VariantStream::ExtXStreamInf { + uri, + frame_rate, + audio, + subtitles, + closed_captions, + stream_data, + } => { + VariantStream::ExtXStreamInf { + uri: Cow::Owned(uri.into_owned()), + frame_rate, + audio: audio.map(|v| Cow::Owned(v.into_owned())), + subtitles: subtitles.map(|v| Cow::Owned(v.into_owned())), + closed_captions: closed_captions.map(|v| v.into_owned()), + stream_data: stream_data.into_owned(), + } + } + } + } } /// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for VariantStream { +impl<'a> RequiredVersion for VariantStream<'a> { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } fn introduced_version(&self) -> ProtocolVersion { @@ -265,7 +301,7 @@ impl RequiredVersion for VariantStream { } } -impl fmt::Display for VariantStream { +impl<'a> fmt::Display for VariantStream<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::ExtXIFrame { uri, stream_data } => { @@ -306,10 +342,10 @@ impl fmt::Display for VariantStream { } } -impl FromStr for VariantStream { - type Err = Error; +impl<'a> TryFrom<&'a str> for VariantStream<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &'a str) -> Result { if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) { let uri = AttributePairs::new(input) .find_map(|(key, value)| { @@ -323,7 +359,7 @@ impl FromStr for VariantStream { Ok(Self::ExtXIFrame { uri, - stream_data: input.parse()?, + stream_data: StreamData::try_from(input)?, }) } else if let Ok(input) = tag(input, Self::PREFIX_EXTXSTREAMINF) { let mut lines = input.lines(); @@ -342,18 +378,20 @@ impl FromStr for VariantStream { "FRAME-RATE" => frame_rate = Some(value.parse()?), "AUDIO" => audio = Some(unquote(value)), "SUBTITLES" => subtitles = Some(unquote(value)), - "CLOSED-CAPTIONS" => closed_captions = Some(value.parse().unwrap()), + "CLOSED-CAPTIONS" => { + closed_captions = Some(ClosedCaptions::try_from(value).unwrap()) + } _ => {} } } Ok(Self::ExtXStreamInf { - uri: uri.to_string(), + uri: Cow::Borrowed(uri), frame_rate, audio, subtitles, closed_captions, - stream_data: first_line.parse()?, + stream_data: StreamData::try_from(first_line)?, }) } else { // TODO: custom error type? + attach input data @@ -366,8 +404,8 @@ impl FromStr for VariantStream { } } -impl Deref for VariantStream { - type Target = StreamData; +impl<'a> Deref for VariantStream<'a> { + type Target = StreamData<'a>; fn deref(&self) -> &Self::Target { match &self { @@ -378,7 +416,7 @@ impl Deref for VariantStream { } } -impl PartialEq<&VariantStream> for VariantStream { +impl<'a> PartialEq<&VariantStream<'a>> for VariantStream<'a> { fn eq(&self, other: &&Self) -> bool { self.eq(*other) } } @@ -386,7 +424,7 @@ impl PartialEq<&VariantStream> for VariantStream { mod tests { use super::*; use crate::types::InStreamId; - //use pretty_assertions::assert_eq; + use pretty_assertions::assert_eq; #[test] fn test_required_version() { diff --git a/src/tags/media_playlist/discontinuity_sequence.rs b/src/tags/media_playlist/discontinuity_sequence.rs index 9fcdde7..5934ab7 100644 --- a/src/tags/media_playlist/discontinuity_sequence.rs +++ b/src/tags/media_playlist/discontinuity_sequence.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -28,10 +28,10 @@ impl fmt::Display for ExtXDiscontinuitySequence { } } -impl FromStr for ExtXDiscontinuitySequence { - type Err = Error; +impl TryFrom<&str> for ExtXDiscontinuitySequence { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?; @@ -64,11 +64,11 @@ mod test { fn test_parser() { assert_eq!( ExtXDiscontinuitySequence(123), - "#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap() + ExtXDiscontinuitySequence::try_from("#EXT-X-DISCONTINUITY-SEQUENCE:123").unwrap() ); assert_eq!( - ExtXDiscontinuitySequence::from_str("#EXT-X-DISCONTINUITY-SEQUENCE:12A"), + ExtXDiscontinuitySequence::try_from("#EXT-X-DISCONTINUITY-SEQUENCE:12A"), Err(Error::parse_int("12A", "12A".parse::().expect_err(""))) ); } diff --git a/src/tags/media_playlist/end_list.rs b/src/tags/media_playlist/end_list.rs index b57e93d..db326d4 100644 --- a/src/tags/media_playlist/end_list.rs +++ b/src/tags/media_playlist/end_list.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -26,10 +26,10 @@ 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; +impl TryFrom<&str> for ExtXEndList { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { tag(input, Self::PREFIX)?; Ok(Self) } @@ -47,7 +47,10 @@ mod test { #[test] fn test_parser() { - assert_eq!(ExtXEndList, "#EXT-X-ENDLIST".parse().unwrap()); + assert_eq!( + ExtXEndList, + ExtXEndList::try_from("#EXT-X-ENDLIST").unwrap() + ); } #[test] diff --git a/src/tags/media_playlist/i_frames_only.rs b/src/tags/media_playlist/i_frames_only.rs index 3e7225f..cab582d 100644 --- a/src/tags/media_playlist/i_frames_only.rs +++ b/src/tags/media_playlist/i_frames_only.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -21,10 +21,10 @@ impl fmt::Display for ExtXIFramesOnly { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) } } -impl FromStr for ExtXIFramesOnly { - type Err = Error; +impl TryFrom<&str> for ExtXIFramesOnly { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { tag(input, Self::PREFIX)?; Ok(Self) } @@ -44,7 +44,12 @@ mod test { } #[test] - fn test_parser() { assert_eq!(ExtXIFramesOnly, "#EXT-X-I-FRAMES-ONLY".parse().unwrap(),) } + fn test_parser() { + assert_eq!( + ExtXIFramesOnly, + ExtXIFramesOnly::try_from("#EXT-X-I-FRAMES-ONLY").unwrap(), + ) + } #[test] fn test_required_version() { diff --git a/src/tags/media_playlist/media_sequence.rs b/src/tags/media_playlist/media_sequence.rs index 4e36f3a..e316bcc 100644 --- a/src/tags/media_playlist/media_sequence.rs +++ b/src/tags/media_playlist/media_sequence.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -26,10 +26,10 @@ impl fmt::Display for ExtXMediaSequence { } } -impl FromStr for ExtXMediaSequence { - type Err = Error; +impl TryFrom<&str> for ExtXMediaSequence { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?; @@ -62,7 +62,7 @@ mod test { fn test_parser() { assert_eq!( ExtXMediaSequence(123), - "#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap() + ExtXMediaSequence::try_from("#EXT-X-MEDIA-SEQUENCE:123").unwrap() ); } } diff --git a/src/tags/media_playlist/target_duration.rs b/src/tags/media_playlist/target_duration.rs index ad641c1..2cd66d1 100644 --- a/src/tags/media_playlist/target_duration.rs +++ b/src/tags/media_playlist/target_duration.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use std::time::Duration; use crate::types::ProtocolVersion; @@ -25,10 +25,10 @@ impl fmt::Display for ExtXTargetDuration { } } -impl FromStr for ExtXTargetDuration { - type Err = Error; +impl TryFrom<&str> for ExtXTargetDuration { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { let input = tag(input, Self::PREFIX)? .parse() .map_err(|e| Error::parse_int(input, e))?; @@ -62,7 +62,7 @@ mod test { fn test_parser() { assert_eq!( ExtXTargetDuration(Duration::from_secs(5)), - "#EXT-X-TARGETDURATION:5".parse().unwrap() + ExtXTargetDuration::try_from("#EXT-X-TARGETDURATION:5").unwrap() ); } } diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index 9bf5e52..2eb86ed 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use core::ops::{Add, AddAssign, Sub, SubAssign}; @@ -187,13 +187,13 @@ impl fmt::Display for ExtXByteRange { } } -impl FromStr for ExtXByteRange { - type Err = Error; +impl TryFrom<&str> for ExtXByteRange { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { let input = tag(input, Self::PREFIX)?; - Ok(Self(ByteRange::from_str(input)?)) + Ok(Self(ByteRange::try_from(input)?)) } } @@ -219,12 +219,12 @@ mod test { fn test_parser() { assert_eq!( ExtXByteRange::from(2..15), - "#EXT-X-BYTERANGE:13@2".parse().unwrap() + ExtXByteRange::try_from("#EXT-X-BYTERANGE:13@2").unwrap() ); assert_eq!( ExtXByteRange::from(..22), - "#EXT-X-BYTERANGE:22".parse().unwrap() + ExtXByteRange::try_from("#EXT-X-BYTERANGE:22").unwrap() ); } diff --git a/src/tags/media_segment/date_range.rs b/src/tags/media_segment/date_range.rs index 014dbea..bc37939 100644 --- a/src/tags/media_segment/date_range.rs +++ b/src/tags/media_segment/date_range.rs @@ -1,6 +1,7 @@ +use std::borrow::Cow; use std::collections::BTreeMap; +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use std::time::Duration; #[cfg(feature = "chrono")] @@ -18,13 +19,13 @@ use crate::{Error, RequiredVersion}; #[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[builder(setter(into))] #[shorthand(enable(must_use, into))] -pub struct ExtXDateRange { +pub struct ExtXDateRange<'a> { /// A string that uniquely identifies an [`ExtXDateRange`] in the playlist. /// /// ## Note /// /// This field is required. - id: String, + id: Cow<'a, str>, /// A client-defined string that specifies some set of attributes and their /// associated value semantics. All [`ExtXDateRange`]s with the same class /// attribute value must adhere to these semantics. @@ -33,7 +34,7 @@ pub struct ExtXDateRange { /// /// This field is optional. #[builder(setter(strip_option), default)] - class: Option, + class: Option>, /// The date at which the [`ExtXDateRange`] begins. /// /// ## Note @@ -56,7 +57,7 @@ pub struct ExtXDateRange { /// here. #[cfg(not(feature = "chrono"))] #[builder(setter(strip_option), default)] - start_date: Option, + start_date: Option>, /// The date at which the [`ExtXDateRange`] ends. It must be equal to or /// later than the value of the [`start-date`] attribute. /// @@ -79,7 +80,7 @@ pub struct ExtXDateRange { /// [`start-date`]: #method.start_date #[cfg(not(feature = "chrono"))] #[builder(setter(strip_option), default)] - end_date: Option, + end_date: Option>, /// The duration of the [`ExtXDateRange`]. A single instant in time (e.g., /// crossing a finish line) should be represented with a duration of 0. /// @@ -114,7 +115,7 @@ pub struct ExtXDateRange { /// /// This field is optional. #[builder(setter(strip_option), default)] - scte35_cmd: Option, + scte35_cmd: Option>, /// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and /// Telecommunications Engineers standard that describes the inline /// insertion of cue tones in mpeg-ts streams. @@ -131,7 +132,7 @@ pub struct ExtXDateRange { /// /// This field is optional. #[builder(setter(strip_option), default)] - scte35_out: Option, + scte35_out: Option>, /// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and /// Telecommunications Engineers standard that describes the inline /// insertion of cue tones in mpeg-ts streams. @@ -148,7 +149,7 @@ pub struct ExtXDateRange { /// /// This field is optional. #[builder(setter(strip_option), default)] - scte35_in: Option, + scte35_in: Option>, /// This field indicates that the [`ExtXDateRange::end_date`] is equal to /// the [`ExtXDateRange::start_date`] of the following range. /// @@ -179,30 +180,25 @@ pub struct ExtXDateRange { /// This field is optional. #[builder(default)] #[shorthand(enable(collection_magic), disable(set, get))] - pub client_attributes: BTreeMap, + pub client_attributes: BTreeMap, Value<'a>>, } -impl ExtXDateRangeBuilder { +impl<'a> ExtXDateRangeBuilder<'a> { /// Inserts a key value pair. - pub fn insert_client_attribute, V: Into>( + pub fn insert_client_attribute>, V: Into>>( &mut self, key: K, value: V, ) -> &mut Self { - if self.client_attributes.is_none() { - self.client_attributes = Some(BTreeMap::new()); - } + let attrs = self.client_attributes.get_or_insert_with(BTreeMap::new); + + attrs.insert(key.into(), value.into()); - if let Some(client_attributes) = &mut self.client_attributes { - client_attributes.insert(key.into(), value.into()); - } else { - unreachable!(); - } self } } -impl ExtXDateRange { +impl<'a> ExtXDateRange<'a> { pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; /// Makes a new [`ExtXDateRange`] tag. @@ -237,7 +233,7 @@ let date_range = ExtXDateRange::new("id", "2010-02-19T14:54:23.031+08:00"); "# )] #[must_use] - pub fn new, #[cfg(not(feature = "chrono"))] I: Into>( + pub fn new>, #[cfg(not(feature = "chrono"))] I: Into>>( id: T, #[cfg(feature = "chrono")] start_date: DateTime, #[cfg(not(feature = "chrono"))] start_date: I, @@ -316,18 +312,52 @@ let date_range = ExtXDateRange::builder() )] #[must_use] #[inline] - pub fn builder() -> ExtXDateRangeBuilder { ExtXDateRangeBuilder::default() } + pub fn builder() -> ExtXDateRangeBuilder<'a> { ExtXDateRangeBuilder::default() } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> ExtXDateRange<'static> { + ExtXDateRange { + id: Cow::Owned(self.id.into_owned()), + class: self.class.map(|v| Cow::Owned(v.into_owned())), + #[cfg(not(feature = "chrono"))] + start_date: self.start_date.map(|v| Cow::Owned(v.into_owned())), + #[cfg(feature = "chrono")] + start_date: self.start_date, + #[cfg(not(feature = "chrono"))] + end_date: self.end_date.map(|v| Cow::Owned(v.into_owned())), + #[cfg(feature = "chrono")] + end_date: self.end_date, + scte35_cmd: self.scte35_cmd.map(|v| Cow::Owned(v.into_owned())), + scte35_out: self.scte35_out.map(|v| Cow::Owned(v.into_owned())), + scte35_in: self.scte35_in.map(|v| Cow::Owned(v.into_owned())), + client_attributes: { + self.client_attributes + .into_iter() + .map(|(k, v)| (Cow::Owned(k.into_owned()), v.into_owned())) + .collect() + }, + duration: self.duration, + end_on_next: self.end_on_next, + planned_duration: self.planned_duration, + } + } } /// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for ExtXDateRange { +impl<'a> RequiredVersion for ExtXDateRange<'a> { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } -impl FromStr for ExtXDateRange { - type Err = Error; +impl<'a> TryFrom<&'a str> for ExtXDateRange<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &'a str) -> Result { let input = tag(input, Self::PREFIX)?; let mut id = None; @@ -398,7 +428,7 @@ impl FromStr for ExtXDateRange { )); } - client_attributes.insert(key.to_string(), value.parse()?); + client_attributes.insert(Cow::Borrowed(key), Value::try_from(value)?); } else { // [6.3.1. General Client Responsibilities] // > ignore any attribute/value pair with an @@ -451,7 +481,7 @@ impl FromStr for ExtXDateRange { } } -impl fmt::Display for ExtXDateRange { +impl<'a> fmt::Display for ExtXDateRange<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "ID={}", quote(&self.id))?; @@ -547,22 +577,20 @@ mod test { #[test] fn test_parser() { $( - assert_eq!($left, $right.parse().unwrap()); + assert_eq!($left, TryFrom::try_from($right).unwrap()); )* - assert!("#EXT-X-DATERANGE:END-ON-NEXT=NO" - .parse::() + assert!(ExtXDateRange::try_from("#EXT-X-DATERANGE:END-ON-NEXT=NO") .is_err()); - assert!("garbage".parse::().is_err()); - assert!("".parse::().is_err()); + assert!(ExtXDateRange::try_from("garbage").is_err()); + assert!(ExtXDateRange::try_from("").is_err()); - assert!(concat!( + assert!(ExtXDateRange::try_from(concat!( "#EXT-X-DATERANGE:", "ID=\"test_id\",", "START-DATE=\"2014-03-05T11:15:00Z\",", "END-ON-NEXT=YES" - ) - .parse::() + )) .is_err()); } } diff --git a/src/tags/media_segment/discontinuity.rs b/src/tags/media_segment/discontinuity.rs index 924bcd5..4274fe3 100644 --- a/src/tags/media_segment/discontinuity.rs +++ b/src/tags/media_segment/discontinuity.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -23,10 +23,10 @@ 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; +impl TryFrom<&str> for ExtXDiscontinuity { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &str) -> Result { tag(input, Self::PREFIX)?; Ok(Self) } @@ -46,7 +46,12 @@ mod test { } #[test] - fn test_parser() { assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap()) } + fn test_parser() { + assert_eq!( + ExtXDiscontinuity, + ExtXDiscontinuity::try_from("#EXT-X-DISCONTINUITY").unwrap() + ) + } #[test] fn test_required_version() { diff --git a/src/tags/media_segment/inf.rs b/src/tags/media_segment/inf.rs index 466497b..b8abcdd 100644 --- a/src/tags/media_segment/inf.rs +++ b/src/tags/media_segment/inf.rs @@ -1,5 +1,6 @@ +use std::borrow::Cow; +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use std::time::Duration; use derive_more::AsRef; @@ -12,13 +13,13 @@ use crate::{Error, RequiredVersion}; /// /// [`Media Segment`]: crate::media_segment::MediaSegment #[derive(AsRef, Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ExtInf { +pub struct ExtInf<'a> { #[as_ref] duration: Duration, - title: Option, + title: Option>, } -impl ExtInf { +impl<'a> ExtInf<'a> { pub(crate) const PREFIX: &'static str = "#EXTINF:"; /// Makes a new [`ExtInf`] tag. @@ -50,7 +51,7 @@ impl ExtInf { /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// ``` #[must_use] - pub fn with_title>(duration: Duration, title: T) -> Self { + pub fn with_title>>(duration: Duration, title: T) -> Self { Self { duration, title: Some(title.into()), @@ -101,10 +102,10 @@ impl ExtInf { /// /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// - /// assert_eq!(ext_inf.title(), &Some("title".to_string())); + /// assert_eq!(ext_inf.title(), &Some("title".into())); /// ``` #[must_use] - pub const fn title(&self) -> &Option { &self.title } + pub const fn title(&self) -> &Option> { &self.title } /// Sets the title of the associated media segment. /// @@ -118,17 +119,31 @@ impl ExtInf { /// /// ext_inf.set_title(Some("better title")); /// - /// assert_eq!(ext_inf.title(), &Some("better title".to_string())); + /// assert_eq!(ext_inf.title(), &Some("better title".into())); /// ``` - pub fn set_title(&mut self, value: Option) -> &mut Self { - self.title = value.map(|v| v.to_string()); + pub fn set_title>>(&mut self, value: Option) -> &mut Self { + self.title = value.map(|v| v.into()); self } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> ExtInf<'static> { + ExtInf { + duration: self.duration, + title: self.title.map(|v| Cow::Owned(v.into_owned())), + } + } } /// This tag requires [`ProtocolVersion::V1`], if the duration does not have /// nanoseconds, otherwise it requires [`ProtocolVersion::V3`]. -impl RequiredVersion for ExtInf { +impl<'a> RequiredVersion for ExtInf<'a> { fn required_version(&self) -> ProtocolVersion { if self.duration.subsec_nanos() == 0 { ProtocolVersion::V1 @@ -138,7 +153,7 @@ impl RequiredVersion for ExtInf { } } -impl fmt::Display for ExtInf { +impl<'a> fmt::Display for ExtInf<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "{},", self.duration.as_secs_f64())?; @@ -150,10 +165,10 @@ impl fmt::Display for ExtInf { } } -impl FromStr for ExtInf { - type Err = Error; +impl<'a> TryFrom<&'a str> for ExtInf<'a> { + type Error = Error; - fn from_str(input: &str) -> Result { + fn try_from(input: &'a str) -> Result { let mut input = tag(input, Self::PREFIX)?.splitn(2, ','); let duration = input.next().unwrap(); @@ -167,13 +182,13 @@ impl FromStr for ExtInf { .next() .map(str::trim) .filter(|value| !value.is_empty()) - .map(ToString::to_string); + .map(|v| Cow::Borrowed(v)); Ok(Self { duration, title }) } } -impl From for ExtInf { +impl<'a> From for ExtInf<'a> { fn from(value: Duration) -> Self { Self::new(value) } } @@ -206,32 +221,32 @@ mod test { fn test_parser() { // #EXTINF:,[] assert_eq!( - "#EXTINF:5".parse::<ExtInf>().unwrap(), + ExtInf::try_from("#EXTINF:5").unwrap(), ExtInf::new(Duration::from_secs(5)) ); assert_eq!( - "#EXTINF:5,".parse::<ExtInf>().unwrap(), + ExtInf::try_from("#EXTINF:5,").unwrap(), ExtInf::new(Duration::from_secs(5)) ); assert_eq!( - "#EXTINF:5.5".parse::<ExtInf>().unwrap(), + ExtInf::try_from("#EXTINF:5.5").unwrap(), ExtInf::new(Duration::from_millis(5500)) ); assert_eq!( - "#EXTINF:5.5,".parse::<ExtInf>().unwrap(), + ExtInf::try_from("#EXTINF:5.5,").unwrap(), ExtInf::new(Duration::from_millis(5500)) ); assert_eq!( - "#EXTINF:5.5,title".parse::<ExtInf>().unwrap(), + ExtInf::try_from("#EXTINF:5.5,title").unwrap(), ExtInf::with_title(Duration::from_millis(5500), "title") ); assert_eq!( - "#EXTINF:5,title".parse::<ExtInf>().unwrap(), + ExtInf::try_from("#EXTINF:5,title").unwrap(), ExtInf::with_title(Duration::from_secs(5), "title") ); - assert!("#EXTINF:".parse::<ExtInf>().is_err()); - assert!("#EXTINF:garbage".parse::<ExtInf>().is_err()); + assert!(ExtInf::try_from("#EXTINF:").is_err()); + assert!(ExtInf::try_from("#EXTINF:garbage").is_err()); } #[test] @@ -239,7 +254,7 @@ mod test { 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()) + &Some("title".into()) ); } diff --git a/src/tags/media_segment/key.rs b/src/tags/media_segment/key.rs index e201072..98e8cf4 100644 --- a/src/tags/media_segment/key.rs +++ b/src/tags/media_segment/key.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::{DecryptionKey, ProtocolVersion}; use crate::utils::tag; @@ -9,9 +9,9 @@ use crate::{Error, RequiredVersion}; /// /// An unencrypted segment should be marked with [`ExtXKey::empty`]. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub struct ExtXKey(pub Option<DecryptionKey>); +pub struct ExtXKey<'a>(pub Option<DecryptionKey<'a>>); -impl ExtXKey { +impl<'a> ExtXKey<'a> { pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:"; /// Constructs an [`ExtXKey`] tag. @@ -37,7 +37,7 @@ impl ExtXKey { /// ``` #[must_use] #[inline] - pub const fn new(inner: DecryptionKey) -> Self { Self(Some(inner)) } + pub const fn new(inner: DecryptionKey<'a>) -> Self { Self(Some(inner)) } /// Constructs an empty [`ExtXKey`], which signals that a segment is /// unencrypted. @@ -124,7 +124,7 @@ impl ExtXKey { /// let decryption_key: DecryptionKey = ExtXKey::empty().unwrap(); // panics /// ``` #[must_use] - pub fn unwrap(self) -> DecryptionKey { + pub fn unwrap(self) -> DecryptionKey<'a> { match self.0 { Some(v) => v, None => panic!("called `ExtXKey::unwrap()` on an empty key"), @@ -134,7 +134,7 @@ impl ExtXKey { /// Returns a reference to the underlying [`DecryptionKey`]. #[must_use] #[inline] - pub fn as_ref(&self) -> Option<&DecryptionKey> { self.0.as_ref() } + pub fn as_ref(&self) -> Option<&DecryptionKey<'a>> { self.0.as_ref() } /// Converts an [`ExtXKey`] into an `Option<DecryptionKey>`. /// @@ -160,7 +160,19 @@ impl ExtXKey { /// ``` #[must_use] #[inline] - pub fn into_option(self) -> Option<DecryptionKey> { self.0 } + pub fn into_option(self) -> Option<DecryptionKey<'a>> { self.0 } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + /// + /// [`Cow`]: std::borrow::Cow + #[must_use] + #[inline] + pub fn into_owned(self) -> ExtXKey<'static> { ExtXKey(self.0.map(|v| v.into_owned())) } } /// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or @@ -168,7 +180,7 @@ impl ExtXKey { /// specified. /// /// Otherwise [`ProtocolVersion::V1`] is required. -impl RequiredVersion for ExtXKey { +impl<'a> RequiredVersion for ExtXKey<'a> { fn required_version(&self) -> ProtocolVersion { self.0 .as_ref() @@ -176,33 +188,33 @@ impl RequiredVersion for ExtXKey { } } -impl FromStr for ExtXKey { - type Err = Error; +impl<'a> TryFrom<&'a str> for ExtXKey<'a> { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &'a str) -> Result<Self, Self::Error> { let input = tag(input, Self::PREFIX)?; if input.trim() == "METHOD=NONE" { Ok(Self(None)) } else { - Ok(DecryptionKey::from_str(input)?.into()) + Ok(DecryptionKey::try_from(input)?.into()) } } } -impl From<Option<DecryptionKey>> for ExtXKey { - fn from(value: Option<DecryptionKey>) -> Self { Self(value) } +impl<'a> From<Option<DecryptionKey<'a>>> for ExtXKey<'a> { + fn from(value: Option<DecryptionKey<'a>>) -> Self { Self(value) } } -impl From<DecryptionKey> for ExtXKey { - fn from(value: DecryptionKey) -> Self { Self(Some(value)) } +impl<'a> From<DecryptionKey<'a>> for ExtXKey<'a> { + fn from(value: DecryptionKey<'a>) -> Self { Self(Some(value)) } } -impl From<crate::tags::ExtXSessionKey> for ExtXKey { - fn from(value: crate::tags::ExtXSessionKey) -> Self { Self(Some(value.0)) } +impl<'a> From<crate::tags::ExtXSessionKey<'a>> for ExtXKey<'a> { + fn from(value: crate::tags::ExtXSessionKey<'a>) -> Self { Self(Some(value.0)) } } -impl fmt::Display for ExtXKey { +impl<'a> fmt::Display for ExtXKey<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; @@ -232,7 +244,7 @@ mod test { #[test] fn test_parser() { $( - assert_eq!($struct, $str.parse().unwrap()); + assert_eq!($struct, TryFrom::try_from($str).unwrap()); )+ assert_eq!( @@ -242,15 +254,15 @@ mod test { "http://www.example.com" ) ), - concat!( + ExtXKey::try_from(concat!( "#EXT-X-KEY:", "METHOD=AES-128,", "URI=\"http://www.example.com\",", "UNKNOWNTAG=abcd" - ).parse().unwrap(), + )).unwrap(), ); - assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::<ExtXKey>().is_err()); - assert!("garbage".parse::<ExtXKey>().is_err()); + assert!(ExtXKey::try_from("#EXT-X-KEY:METHOD=AES-128,URI=").is_err()); + assert!(ExtXKey::try_from("garbage").is_err()); } } } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index d1209b3..17e7130 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -1,5 +1,6 @@ +use std::borrow::Cow; +use std::convert::{TryFrom, TryInto}; use std::fmt; -use std::str::FromStr; use shorthand::ShortHand; @@ -33,18 +34,18 @@ use crate::{Decryptable, Error, RequiredVersion}; /// [`MediaPlaylist`]: crate::MediaPlaylist #[derive(ShortHand, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[shorthand(enable(must_use, into))] -pub struct ExtXMap { +pub struct ExtXMap<'a> { /// The `URI` that identifies a resource, that contains the media /// initialization section. - uri: String, + uri: Cow<'a, str>, /// The range of the media initialization section. #[shorthand(enable(copy))] range: Option<ByteRange>, #[shorthand(enable(skip))] - pub(crate) keys: Vec<ExtXKey>, + pub(crate) keys: Vec<ExtXKey<'a>>, } -impl ExtXMap { +impl<'a> ExtXMap<'a> { pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:"; /// Makes a new [`ExtXMap`] tag. @@ -55,7 +56,8 @@ impl ExtXMap { /// # use hls_m3u8::tags::ExtXMap; /// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin"); /// ``` - pub fn new<T: Into<String>>(uri: T) -> Self { + #[must_use] + pub fn new<T: Into<Cow<'a, str>>>(uri: T) -> Self { Self { uri: uri.into(), range: None, @@ -71,19 +73,35 @@ impl ExtXMap { /// # use hls_m3u8::tags::ExtXMap; /// use hls_m3u8::types::ByteRange; /// - /// ExtXMap::with_range("https://prod.mediaspace.com/init.bin", 2..11); + /// let map = ExtXMap::with_range("https://prod.mediaspace.com/init.bin", 2..11); /// ``` - pub fn with_range<I: Into<String>, B: Into<ByteRange>>(uri: I, range: B) -> Self { + #[must_use] + pub fn with_range<I: Into<Cow<'a, str>>, B: Into<ByteRange>>(uri: I, range: B) -> Self { Self { uri: uri.into(), range: Some(range.into()), keys: vec![], } } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> ExtXMap<'static> { + ExtXMap { + uri: Cow::Owned(self.uri.into_owned()), + range: self.range, + keys: self.keys.into_iter().map(|v| v.into_owned()).collect(), + } + } } -impl Decryptable for ExtXMap { - fn keys(&self) -> Vec<&DecryptionKey> { +impl<'a> Decryptable<'a> for ExtXMap<'a> { + fn keys(&self) -> Vec<&DecryptionKey<'a>> { // self.keys.iter().filter_map(ExtXKey::as_ref).collect() } @@ -97,7 +115,7 @@ impl Decryptable for ExtXMap { /// /// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly /// [`MediaPlaylist`]: crate::MediaPlaylist -impl RequiredVersion for ExtXMap { +impl<'a> RequiredVersion for ExtXMap<'a> { // this should return ProtocolVersion::V5, if it does not contain an // EXT-X-I-FRAMES-ONLY! // http://alexzambelli.com/blog/2016/05/04/understanding-hls-versions-and-client-compatibility/ @@ -106,7 +124,7 @@ impl RequiredVersion for ExtXMap { fn introduced_version(&self) -> ProtocolVersion { ProtocolVersion::V5 } } -impl fmt::Display for ExtXMap { +impl<'a> fmt::Display for ExtXMap<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; write!(f, "URI={}", quote(&self.uri))?; @@ -119,10 +137,10 @@ impl fmt::Display for ExtXMap { } } -impl FromStr for ExtXMap { - type Err = Error; +impl<'a> TryFrom<&'a str> for ExtXMap<'a> { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &'a str) -> Result<Self, Self::Error> { let input = tag(input, Self::PREFIX)?; let mut uri = None; @@ -132,7 +150,7 @@ impl FromStr for ExtXMap { match key { "URI" => uri = Some(unquote(value)), "BYTERANGE" => { - range = Some(unquote(value).parse()?); + range = Some(unquote(value).try_into()?); } _ => { // [6.3.1. General Client Responsibilities] @@ -174,18 +192,17 @@ mod test { fn test_parser() { assert_eq!( ExtXMap::new("foo"), - "#EXT-X-MAP:URI=\"foo\"".parse().unwrap() + ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\"").unwrap() ); assert_eq!( ExtXMap::with_range("foo", ByteRange::from(2..11)), - "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap() + ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"").unwrap() ); + assert_eq!( ExtXMap::with_range("foo", ByteRange::from(2..11)), - "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED" - .parse() - .unwrap() + ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED").unwrap() ); } @@ -200,6 +217,6 @@ mod test { #[test] fn test_decryptable() { - assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey>::new()); + assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey<'_>>::new()); } } diff --git a/src/tags/media_segment/program_date_time.rs b/src/tags/media_segment/program_date_time.rs index 9e123d5..fbbfdeb 100644 --- a/src/tags/media_segment/program_date_time.rs +++ b/src/tags/media_segment/program_date_time.rs @@ -1,5 +1,8 @@ +#[cfg(not(feature = "chrono"))] +use std::borrow::Cow; +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; +use std::marker::PhantomData; #[cfg(feature = "chrono")] use chrono::{DateTime, FixedOffset, SecondsFormat}; @@ -27,17 +30,18 @@ use crate::{Error, RequiredVersion}; #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "chrono", derive(Deref, DerefMut, Copy))] #[non_exhaustive] -pub struct ExtXProgramDateTime { +pub struct ExtXProgramDateTime<'a> { /// The date-time of the first sample of the associated media segment. #[cfg(feature = "chrono")] #[cfg_attr(feature = "chrono", deref_mut, deref)] pub date_time: DateTime<FixedOffset>, /// The date-time of the first sample of the associated media segment. #[cfg(not(feature = "chrono"))] - pub date_time: String, + pub date_time: Cow<'a, str>, + _p: PhantomData<&'a str>, } -impl ExtXProgramDateTime { +impl<'a> ExtXProgramDateTime<'a> { pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:"; /// Makes a new [`ExtXProgramDateTime`] tag. @@ -58,7 +62,12 @@ impl ExtXProgramDateTime { /// ``` #[must_use] #[cfg(feature = "chrono")] - pub const fn new(date_time: DateTime<FixedOffset>) -> Self { Self { date_time } } + pub const fn new(date_time: DateTime<FixedOffset>) -> Self { + Self { + date_time, + _p: PhantomData, + } + } /// Makes a new [`ExtXProgramDateTime`] tag. /// @@ -69,19 +78,37 @@ impl ExtXProgramDateTime { /// let program_date_time = ExtXProgramDateTime::new("2010-02-19T14:54:23.031+08:00"); /// ``` #[cfg(not(feature = "chrono"))] - pub fn new<T: Into<String>>(date_time: T) -> Self { + pub fn new<T: Into<Cow<'a, str>>>(date_time: T) -> Self { Self { date_time: date_time.into(), + _p: PhantomData, + } + } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> ExtXProgramDateTime<'static> { + ExtXProgramDateTime { + #[cfg(not(feature = "chrono"))] + date_time: Cow::Owned(self.date_time.into_owned()), + #[cfg(feature = "chrono")] + date_time: self.date_time, + _p: PhantomData, } } } /// This tag requires [`ProtocolVersion::V1`]. -impl RequiredVersion for ExtXProgramDateTime { +impl<'a> RequiredVersion for ExtXProgramDateTime<'a> { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } } -impl fmt::Display for ExtXProgramDateTime { +impl<'a> fmt::Display for ExtXProgramDateTime<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let date_time = { #[cfg(feature = "chrono")] @@ -97,10 +124,10 @@ impl fmt::Display for ExtXProgramDateTime { } } -impl FromStr for ExtXProgramDateTime { - type Err = Error; +impl<'a> TryFrom<&'a str> for ExtXProgramDateTime<'a> { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &'a str) -> Result<Self, Self::Error> { let input = tag(input, Self::PREFIX)?; Ok(Self::new({ @@ -163,8 +190,7 @@ mod test { "2010-02-19T14:54:23.031+08:00" } }), - "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00" - .parse::<ExtXProgramDateTime>() + ExtXProgramDateTime::try_from("#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00") .unwrap() ); } diff --git a/src/tags/shared/independent_segments.rs b/src/tags/shared/independent_segments.rs index 557d8f0..46dc4c7 100644 --- a/src/tags/shared/independent_segments.rs +++ b/src/tags/shared/independent_segments.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -25,10 +25,10 @@ impl fmt::Display for ExtXIndependentSegments { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) } } -impl FromStr for ExtXIndependentSegments { - type Err = Error; +impl TryFrom<&str> for ExtXIndependentSegments { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &str) -> Result<Self, Self::Error> { tag(input, Self::PREFIX)?; Ok(Self) } @@ -51,7 +51,7 @@ mod test { fn test_parser() { assert_eq!( ExtXIndependentSegments, - "#EXT-X-INDEPENDENT-SEGMENTS".parse().unwrap(), + ExtXIndependentSegments::try_from("#EXT-X-INDEPENDENT-SEGMENTS").unwrap(), ) } diff --git a/src/tags/shared/start.rs b/src/tags/shared/start.rs index c201fc7..e841d8f 100644 --- a/src/tags/shared/start.rs +++ b/src/tags/shared/start.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use shorthand::ShortHand; @@ -111,10 +111,10 @@ impl fmt::Display for ExtXStart { } } -impl FromStr for ExtXStart { - type Err = Error; +impl TryFrom<&str> for ExtXStart { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &str) -> Result<Self, Self::Error> { let input = tag(input, Self::PREFIX)?; let mut time_offset = None; @@ -176,19 +176,17 @@ mod test { fn test_parser() { assert_eq!( ExtXStart::new(Float::new(-1.23)), - "#EXT-X-START:TIME-OFFSET=-1.23".parse().unwrap(), + ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=-1.23").unwrap(), ); assert_eq!( ExtXStart::with_precise(Float::new(1.23), true), - "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(), + ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES").unwrap(), ); assert_eq!( ExtXStart::with_precise(Float::new(1.23), true), - "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG" - .parse() - .unwrap(), + ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG").unwrap(), ); } } diff --git a/src/traits.rs b/src/traits.rs index 95a17bf..9bfcf8f 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -6,8 +6,8 @@ use crate::types::{DecryptionKey, ProtocolVersion}; mod private { pub trait Sealed {} - impl Sealed for crate::MediaSegment {} - impl Sealed for crate::tags::ExtXMap {} + impl<'a> Sealed for crate::MediaSegment<'a> {} + impl<'a> Sealed for crate::tags::ExtXMap<'a> {} } /// Signals that a type or some of the asssociated data might need to be @@ -16,7 +16,7 @@ mod private { /// # Note /// /// You are not supposed to implement this trait, therefore it is "sealed". -pub trait Decryptable: private::Sealed { +pub trait Decryptable<'a>: private::Sealed { /// Returns all keys, associated with the type. /// /// # Example @@ -36,13 +36,13 @@ pub trait Decryptable: private::Sealed { /// } /// ``` #[must_use] - fn keys(&self) -> Vec<&DecryptionKey>; + fn keys(&self) -> Vec<&DecryptionKey<'a>>; /// Most of the time only a single key is provided, so instead of iterating /// through all keys, one might as well just get the first key. #[must_use] #[inline] - fn first_key(&self) -> Option<&DecryptionKey> { + fn first_key(&self) -> Option<&DecryptionKey<'a>> { <Self as Decryptable>::keys(self).first().copied() } diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index ca1cce1..084db4c 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -1,10 +1,10 @@ -use core::convert::TryInto; +use core::convert::{TryFrom, TryInto}; use core::fmt; use core::ops::{ Add, AddAssign, Bound, Range, RangeBounds, RangeInclusive, RangeTo, RangeToInclusive, Sub, SubAssign, }; -use core::str::FromStr; +use std::borrow::Cow; use shorthand::ShortHand; @@ -408,10 +408,10 @@ impl fmt::Display for ByteRange { } } -impl FromStr for ByteRange { - type Err = Error; +impl TryFrom<&str> for ByteRange { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &str) -> Result<Self, Self::Error> { let mut input = input.splitn(2, '@'); let length = input.next().unwrap(); @@ -431,6 +431,15 @@ impl FromStr for ByteRange { } } +impl<'a> TryFrom<Cow<'a, str>> for ByteRange { + type Error = Error; + + fn try_from(input: Cow<'a, str>) -> Result<Self, Self::Error> { + // + Self::try_from(input.as_ref()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -658,20 +667,20 @@ mod tests { #[test] fn test_parser() { - assert_eq!(ByteRange::from(2..22), "20@2".parse().unwrap()); + assert_eq!(ByteRange::from(2..22), ByteRange::try_from("20@2").unwrap()); - assert_eq!(ByteRange::from(..300), "300".parse().unwrap()); + assert_eq!(ByteRange::from(..300), ByteRange::try_from("300").unwrap()); assert_eq!( - ByteRange::from_str("a"), + ByteRange::try_from("a"), Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err())) ); assert_eq!( - ByteRange::from_str("1@a"), + ByteRange::try_from("1@a"), Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err())) ); - assert!("".parse::<ByteRange>().is_err()); + assert!(ByteRange::try_from("").is_err()); } } diff --git a/src/types/closed_captions.rs b/src/types/closed_captions.rs index 049a665..2712aa9 100644 --- a/src/types/closed_captions.rs +++ b/src/types/closed_captions.rs @@ -1,13 +1,13 @@ -use core::convert::Infallible; +use core::convert::{Infallible, TryFrom}; +use std::borrow::Cow; use std::fmt; -use std::str::FromStr; use crate::utils::{quote, unquote}; /// The identifier of a closed captions group or its absence. #[non_exhaustive] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub enum ClosedCaptions { +pub enum ClosedCaptions<'a> { /// It indicates the set of closed-caption renditions that can be used when /// playing the presentation. /// @@ -18,7 +18,7 @@ pub enum ClosedCaptions { /// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type /// [`MediaType::ClosedCaptions`]: crate::types::MediaType::ClosedCaptions - GroupId(String), + GroupId(Cow<'a, str>), /// This variant indicates that there are no closed captions in /// any [`VariantStream`] in the [`MasterPlaylist`], therefore all /// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a @@ -34,7 +34,7 @@ pub enum ClosedCaptions { None, } -impl ClosedCaptions { +impl<'a> ClosedCaptions<'a> { /// Creates a [`ClosedCaptions::GroupId`] with the provided [`String`]. /// /// # Example @@ -47,13 +47,29 @@ impl ClosedCaptions { /// ClosedCaptions::GroupId("vg1".into()) /// ); /// ``` - pub fn group_id<I: Into<String>>(value: I) -> Self { + #[inline] + #[must_use] + pub fn group_id<I: Into<Cow<'a, str>>>(value: I) -> Self { // Self::GroupId(value.into()) } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> ClosedCaptions<'static> { + match self { + Self::GroupId(id) => ClosedCaptions::GroupId(Cow::Owned(id.into_owned())), + Self::None => ClosedCaptions::None, + } + } } -impl<T: PartialEq<str>> PartialEq<T> for ClosedCaptions { +impl<'a, T: PartialEq<str>> PartialEq<T> for ClosedCaptions<'a> { fn eq(&self, other: &T) -> bool { match &self { Self::GroupId(value) => other.eq(value), @@ -62,7 +78,7 @@ impl<T: PartialEq<str>> PartialEq<T> for ClosedCaptions { } } -impl fmt::Display for ClosedCaptions { +impl<'a> fmt::Display for ClosedCaptions<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::GroupId(value) => write!(f, "{}", quote(value)), @@ -71,10 +87,10 @@ impl fmt::Display for ClosedCaptions { } } -impl FromStr for ClosedCaptions { - type Err = Infallible; +impl<'a> TryFrom<&'a str> for ClosedCaptions<'a> { + type Error = Infallible; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &'a str) -> Result<Self, Self::Error> { if input.trim() == "NONE" { Ok(Self::None) } else { @@ -102,12 +118,12 @@ mod tests { fn test_parser() { assert_eq!( ClosedCaptions::None, - "NONE".parse::<ClosedCaptions>().unwrap() + ClosedCaptions::try_from("NONE").unwrap() ); assert_eq!( ClosedCaptions::GroupId("value".into()), - "\"value\"".parse::<ClosedCaptions>().unwrap() + ClosedCaptions::try_from("\"value\"").unwrap() ); } } diff --git a/src/types/codecs.rs b/src/types/codecs.rs index 6b06ccf..0e44425 100644 --- a/src/types/codecs.rs +++ b/src/types/codecs.rs @@ -1,5 +1,6 @@ +use core::convert::TryFrom; use core::fmt; -use core::str::FromStr; +use std::borrow::Cow; use derive_more::{AsMut, AsRef, Deref, DerefMut}; @@ -25,11 +26,11 @@ use crate::Error; #[derive( AsMut, AsRef, Deref, DerefMut, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, )] -pub struct Codecs { - list: Vec<String>, +pub struct Codecs<'a> { + list: Vec<Cow<'a, str>>, } -impl Codecs { +impl<'a> Codecs<'a> { /// Makes a new (empty) [`Codecs`] struct. /// /// # Example @@ -41,9 +42,82 @@ impl Codecs { #[inline] #[must_use] pub const fn new() -> Self { Self { list: Vec::new() } } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> Codecs<'static> { + Codecs { + list: self + .list + .into_iter() + .map(|v| Cow::Owned(v.into_owned())) + .collect(), + } + } } -impl fmt::Display for Codecs { +impl<'a, T> From<Vec<T>> for Codecs<'a> +where + T: Into<Cow<'a, str>>, +{ + fn from(value: Vec<T>) -> Self { + Self { + list: value.into_iter().map(|v| v.into()).collect(), + } + } +} + +// TODO: this should be implemented with const generics in the future! +macro_rules! implement_from { + ($($size:expr),*) => { + $( + impl<'a> From<[&'a str; $size]> for Codecs<'a> { + fn from(value: [&'a str; $size]) -> Self { + Self { + list: { + let mut result = Vec::with_capacity($size); + + for i in 0..$size { + result.push(Cow::Borrowed(value[i])) + } + + result + }, + } + } + } + + impl<'a> From<&[&'a str; $size]> for Codecs<'a> { + fn from(value: &[&'a str; $size]) -> Self { + Self { + list: { + let mut result = Vec::with_capacity($size); + + for i in 0..$size { + result.push(Cow::Borrowed(value[i])) + } + + result + }, + } + } + } + )* + }; +} + +implement_from!( + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x20 +); + +impl<'a> fmt::Display for Codecs<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(codec) = self.list.iter().next() { write!(f, "{}", codec)?; @@ -56,20 +130,24 @@ impl fmt::Display for Codecs { Ok(()) } } -impl FromStr for Codecs { - type Err = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { +impl<'a> TryFrom<&'a str> for Codecs<'a> { + type Error = Error; + + fn try_from(input: &'a str) -> Result<Self, Self::Error> { Ok(Self { list: input.split(',').map(|s| s.into()).collect(), }) } } -impl<T: AsRef<str>, I: IntoIterator<Item = T>> From<I> for Codecs { - fn from(value: I) -> Self { - Self { - list: value.into_iter().map(|s| s.as_ref().to_string()).collect(), +impl<'a> TryFrom<Cow<'a, str>> for Codecs<'a> { + type Error = Error; + + fn try_from(input: Cow<'a, str>) -> Result<Self, Self::Error> { + match input { + Cow::Owned(o) => Ok(Codecs::try_from(o.as_str())?.into_owned()), + Cow::Borrowed(b) => Self::try_from(b), } } } @@ -86,7 +164,7 @@ mod tests { #[test] fn test_display() { assert_eq!( - Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"]).to_string(), + Codecs::from(["mp4a.40.2", "avc1.4d401e"]).to_string(), "mp4a.40.2,avc1.4d401e".to_string() ); } @@ -94,8 +172,8 @@ mod tests { #[test] fn test_parser() { assert_eq!( - Codecs::from_str("mp4a.40.2,avc1.4d401e").unwrap(), - Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"]) + Codecs::try_from("mp4a.40.2,avc1.4d401e").unwrap(), + Codecs::from(["mp4a.40.2", "avc1.4d401e"]) ); } } diff --git a/src/types/decryption_key.rs b/src/types/decryption_key.rs index c1cfbac..96496f5 100644 --- a/src/types/decryption_key.rs +++ b/src/types/decryption_key.rs @@ -1,5 +1,6 @@ +use std::borrow::Cow; +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use derive_builder::Builder; use shorthand::ShortHand; @@ -16,7 +17,7 @@ use crate::{Error, RequiredVersion}; #[builder(setter(into), build_fn(validate = "Self::validate"))] #[shorthand(enable(skip, must_use, into))] #[non_exhaustive] -pub struct DecryptionKey { +pub struct DecryptionKey<'a> { /// The encryption method, which has been used to encrypt the data. /// /// An [`EncryptionMethod::Aes128`] signals that the data is encrypted using @@ -45,7 +46,7 @@ pub struct DecryptionKey { /// This field is required. #[builder(setter(into, strip_option), default)] #[shorthand(disable(skip))] - pub(crate) uri: String, + pub(crate) uri: Cow<'a, str>, /// An initialization vector (IV) is a fixed size input that can be used /// along with a secret key for data encryption. /// @@ -80,7 +81,7 @@ pub struct DecryptionKey { pub versions: Option<KeyFormatVersions>, } -impl DecryptionKey { +impl<'a> DecryptionKey<'a> { /// Creates a new `DecryptionKey` from an uri pointing to the key data and /// an `EncryptionMethod`. /// @@ -94,7 +95,7 @@ impl DecryptionKey { /// ``` #[must_use] #[inline] - pub fn new<I: Into<String>>(method: EncryptionMethod, uri: I) -> Self { + pub fn new<I: Into<Cow<'a, str>>>(method: EncryptionMethod, uri: I) -> Self { Self { method, uri: uri.into(), @@ -125,7 +126,24 @@ impl DecryptionKey { /// ``` #[must_use] #[inline] - pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() } + pub fn builder() -> DecryptionKeyBuilder<'a> { DecryptionKeyBuilder::default() } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> DecryptionKey<'static> { + DecryptionKey { + method: self.method, + uri: Cow::Owned(self.uri.into_owned()), + iv: self.iv, + format: self.format, + versions: self.versions, + } + } } /// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or @@ -133,7 +151,7 @@ impl DecryptionKey { /// specified. /// /// Otherwise [`ProtocolVersion::V1`] is required. -impl RequiredVersion for DecryptionKey { +impl<'a> RequiredVersion for DecryptionKey<'a> { fn required_version(&self) -> ProtocolVersion { if self.format.is_some() || self.versions.is_some() { ProtocolVersion::V5 @@ -145,10 +163,10 @@ impl RequiredVersion for DecryptionKey { } } -impl FromStr for DecryptionKey { - type Err = Error; +impl<'a> TryFrom<&'a str> for DecryptionKey<'a> { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &'a str) -> Result<Self, Self::Error> { let mut method = None; let mut uri = None; let mut iv = None; @@ -190,7 +208,7 @@ impl FromStr for DecryptionKey { } } -impl fmt::Display for DecryptionKey { +impl<'a> fmt::Display for DecryptionKey<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?; @@ -212,7 +230,7 @@ impl fmt::Display for DecryptionKey { } } -impl DecryptionKeyBuilder { +impl<'a> DecryptionKeyBuilder<'a> { fn validate(&self) -> Result<(), String> { // a decryption key must contain a uri and a method if self.method.is_none() { @@ -243,19 +261,19 @@ mod test { #[test] fn test_parser() { $( - assert_eq!($struct, $str.parse().unwrap()); + assert_eq!($struct, TryFrom::try_from($str).unwrap()); )+ assert_eq!( DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"), - concat!( + DecryptionKey::try_from(concat!( "METHOD=AES-128,", "URI=\"http://www.example.com\",", "UNKNOWNTAG=abcd" - ).parse().unwrap(), + )).unwrap(), ); - assert!("METHOD=AES-128,URI=".parse::<DecryptionKey>().is_err()); - assert!("garbage".parse::<DecryptionKey>().is_err()); + assert!(DecryptionKey::try_from("METHOD=AES-128,URI=").is_err()); + assert!(DecryptionKey::try_from("garbage").is_err()); } } } diff --git a/src/types/playlist_type.rs b/src/types/playlist_type.rs index 2ad1916..8c5a838 100644 --- a/src/types/playlist_type.rs +++ b/src/types/playlist_type.rs @@ -1,5 +1,5 @@ +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::ProtocolVersion; use crate::utils::tag; @@ -43,10 +43,10 @@ impl fmt::Display for PlaylistType { } } -impl FromStr for PlaylistType { - type Err = Error; +impl TryFrom<&str> for PlaylistType { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &str) -> Result<Self, Self::Error> { let input = tag(input, Self::PREFIX)?; match input { "EVENT" => Ok(Self::Event), @@ -64,20 +64,18 @@ mod test { #[test] fn test_parser() { assert_eq!( - "#EXT-X-PLAYLIST-TYPE:VOD".parse::<PlaylistType>().unwrap(), + PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:VOD").unwrap(), PlaylistType::Vod, ); assert_eq!( - "#EXT-X-PLAYLIST-TYPE:EVENT" - .parse::<PlaylistType>() - .unwrap(), + PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:EVENT").unwrap(), PlaylistType::Event, ); - assert!("#EXT-X-PLAYLIST-TYPE:H".parse::<PlaylistType>().is_err()); + assert!(PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:H").is_err()); - assert!("garbage".parse::<PlaylistType>().is_err()); + assert!(PlaylistType::try_from("garbage").is_err()); } #[test] diff --git a/src/types/stream_data.rs b/src/types/stream_data.rs index 300aa25..28b5314 100644 --- a/src/types/stream_data.rs +++ b/src/types/stream_data.rs @@ -1,5 +1,6 @@ +use core::convert::TryFrom; use core::fmt; -use core::str::FromStr; +use std::borrow::Cow; use derive_builder::Builder; use shorthand::ShortHand; @@ -17,7 +18,7 @@ use crate::{Error, RequiredVersion}; #[builder(setter(strip_option))] #[builder(derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash))] #[shorthand(enable(must_use, into))] -pub struct StreamData { +pub struct StreamData<'a> { /// The peak segment bitrate of the [`VariantStream`] in bits per second. /// /// If all the [`MediaSegment`]s in a [`VariantStream`] have already been @@ -133,7 +134,7 @@ pub struct StreamData { /// crate::tags::VariantStream::ExtXStreamInf /// [RFC6381]: https://tools.ietf.org/html/rfc6381 #[builder(default, setter(into))] - codecs: Option<Codecs>, + codecs: Option<Codecs<'a>>, /// The resolution of the stream. /// /// # Example @@ -198,7 +199,7 @@ pub struct StreamData { /// let mut stream = StreamData::new(20); /// /// stream.set_video(Some("video_01")); - /// assert_eq!(stream.video(), Some(&"video_01".to_string())); + /// assert_eq!(stream.video(), Some(&"video_01".into())); /// ``` /// /// # Note @@ -210,10 +211,10 @@ pub struct StreamData { /// [`MasterPlaylist`]: crate::MasterPlaylist /// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type #[builder(default, setter(into))] - video: Option<String>, + video: Option<Cow<'a, str>>, } -impl StreamData { +impl<'a> StreamData<'a> { /// Creates a new [`StreamData`]. /// /// # Example @@ -253,10 +254,28 @@ impl StreamData { /// # Ok::<(), Box<dyn ::std::error::Error>>(()) /// ``` #[must_use] - pub fn builder() -> StreamDataBuilder { StreamDataBuilder::default() } + pub fn builder() -> StreamDataBuilder<'a> { StreamDataBuilder::default() } + + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> StreamData<'static> { + StreamData { + bandwidth: self.bandwidth, + average_bandwidth: self.average_bandwidth, + codecs: self.codecs.map(|v| v.into_owned()), + resolution: self.resolution, + hdcp_level: self.hdcp_level, + video: self.video.map(|v| Cow::Owned(v.into_owned())), + } + } } -impl fmt::Display for StreamData { +impl<'a> fmt::Display for StreamData<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "BANDWIDTH={}", self.bandwidth)?; @@ -279,10 +298,10 @@ impl fmt::Display for StreamData { } } -impl FromStr for StreamData { - type Err = Error; +impl<'a> TryFrom<&'a str> for StreamData<'a> { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &'a str) -> Result<Self, Self::Error> { let mut bandwidth = None; let mut average_bandwidth = None; let mut codecs = None; @@ -306,7 +325,7 @@ impl FromStr for StreamData { .map_err(|e| Error::parse_int(value, e))?, ) } - "CODECS" => codecs = Some(unquote(value).parse()?), + "CODECS" => codecs = Some(TryFrom::try_from(unquote(value))?), "RESOLUTION" => resolution = Some(value.parse()?), "HDCP-LEVEL" => { hdcp_level = Some(value.parse::<HdcpLevel>().map_err(Error::strum)?) @@ -334,7 +353,7 @@ impl FromStr for StreamData { } /// This struct requires [`ProtocolVersion::V1`]. -impl RequiredVersion for StreamData { +impl<'a> RequiredVersion for StreamData<'a> { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 } fn introduced_version(&self) -> ProtocolVersion { @@ -385,18 +404,17 @@ mod tests { assert_eq!( stream_data, - concat!( + StreamData::try_from(concat!( "BANDWIDTH=200,", "AVERAGE-BANDWIDTH=15,", "CODECS=\"mp4a.40.2,avc1.4d401e\",", "RESOLUTION=1920x1080,", "HDCP-LEVEL=TYPE-0,", "VIDEO=\"video\"" - ) - .parse() + )) .unwrap() ); - assert!("garbage".parse::<StreamData>().is_err()); + assert!(StreamData::try_from("garbage").is_err()); } } diff --git a/src/types/value.rs b/src/types/value.rs index e7e9563..58a2514 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -1,5 +1,6 @@ +use std::borrow::Cow; +use std::convert::TryFrom; use std::fmt; -use std::str::FromStr; use crate::types::Float; use crate::utils::{quote, unquote}; @@ -8,16 +9,33 @@ use crate::Error; /// A `Value`. #[non_exhaustive] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -pub enum Value { +pub enum Value<'a> { /// A `String`. - String(String), + String(Cow<'a, str>), /// A sequence of bytes. Hex(Vec<u8>), /// A floating point number, that's neither NaN nor infinite. Float(Float), } -impl fmt::Display for Value { +impl<'a> Value<'a> { + /// Makes the struct independent of its lifetime, by taking ownership of all + /// internal [`Cow`]s. + /// + /// # Note + /// + /// This is a relatively expensive operation. + #[must_use] + pub fn into_owned(self) -> Value<'static> { + match self { + Self::String(value) => Value::String(Cow::Owned(value.into_owned())), + Self::Hex(value) => Value::Hex(value), + Self::Float(value) => Value::Float(value), + } + } +} + +impl<'a> fmt::Display for Value<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Self::String(value) => write!(f, "{}", quote(value)), @@ -27,10 +45,10 @@ impl fmt::Display for Value { } } -impl FromStr for Value { - type Err = Error; +impl<'a> TryFrom<&'a str> for Value<'a> { + type Error = Error; - fn from_str(input: &str) -> Result<Self, Self::Err> { + fn try_from(input: &'a str) -> Result<Self, Self::Error> { if input.starts_with("0x") || input.starts_with("0X") { Ok(Self::Hex( hex::decode(input.trim_start_matches("0x").trim_start_matches("0X")) @@ -45,20 +63,16 @@ impl FromStr for Value { } } -impl<T: Into<Float>> From<T> for Value { +impl<T: Into<Float>> From<T> for Value<'static> { fn from(value: T) -> Self { Self::Float(value.into()) } } -impl From<Vec<u8>> for Value { +impl From<Vec<u8>> for Value<'static> { fn from(value: Vec<u8>) -> Self { Self::Hex(value) } } -impl From<String> for Value { - fn from(value: String) -> Self { Self::String(unquote(value)) } -} - -impl From<&str> for Value { - fn from(value: &str) -> Self { Self::String(unquote(value)) } +impl From<String> for Value<'static> { + fn from(value: String) -> Self { Self::String(Cow::Owned(unquote(&value).into_owned())) } } #[cfg(test)] @@ -70,7 +84,7 @@ mod tests { fn test_display() { assert_eq!(Value::Float(Float::new(1.1)).to_string(), "1.1".to_string()); assert_eq!( - Value::String("&str".to_string()).to_string(), + Value::String("&str".into()).to_string(), "\"&str\"".to_string() ); assert_eq!( @@ -81,23 +95,31 @@ mod tests { #[test] fn test_parser() { - assert_eq!(Value::Float(Float::new(1.1)), "1.1".parse().unwrap()); assert_eq!( - Value::String("&str".to_string()), - "\"&str\"".parse().unwrap() + Value::Float(Float::new(1.1)), + Value::try_from("1.1").unwrap() ); - assert_eq!(Value::Hex(vec![1, 2, 3]), "0x010203".parse().unwrap()); - assert_eq!(Value::Hex(vec![1, 2, 3]), "0X010203".parse().unwrap()); - assert!("0x010203Z".parse::<Value>().is_err()); + assert_eq!( + Value::String("&str".into()), + Value::try_from("\"&str\"").unwrap() + ); + assert_eq!( + Value::Hex(vec![1, 2, 3]), + Value::try_from("0x010203").unwrap() + ); + assert_eq!( + Value::Hex(vec![1, 2, 3]), + Value::try_from("0X010203").unwrap() + ); + assert!(Value::try_from("0x010203Z").is_err()); } #[test] fn test_from() { assert_eq!(Value::from(1_u8), Value::Float(Float::new(1.0))); - assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string())); assert_eq!( Value::from("&str".to_string()), - Value::String("&str".to_string()) + Value::String("&str".into()) ); assert_eq!(Value::from(vec![1, 2, 3]), Value::Hex(vec![1, 2, 3])); } diff --git a/src/utils.rs b/src/utils.rs index b7e628f..345fa63 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,7 @@ -use crate::Error; use core::iter; +use std::borrow::Cow; + +use crate::Error; /// This is an extension trait that adds the below method to `bool`. /// Those methods are already planned for the standard library, but are not @@ -67,12 +69,25 @@ pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> { /// /// 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: AsRef<str>>(value: T) -> String { - value - .as_ref() - .chars() - .filter(|c| *c != '"' && *c != '\n' && *c != '\r') - .collect() +pub(crate) fn unquote(value: &str) -> Cow<'_, str> { + if value.starts_with('"') && value.ends_with('"') { + let result = Cow::Borrowed(&value[1..value.len() - 1]); + + if result + .chars() + .find(|c| *c == '"' || *c == '\n' || *c == '\r') + .is_none() + { + return result; + } + } + + Cow::Owned( + value + .chars() + .filter(|c| *c != '"' && *c != '\n' && *c != '\r') + .collect(), + ) } /// Puts a string inside quotes. diff --git a/tests/master_playlist.rs b/tests/master_playlist.rs index 5a6cd4c..7b7abbb 100644 --- a/tests/master_playlist.rs +++ b/tests/master_playlist.rs @@ -1,3 +1,5 @@ +use std::convert::TryFrom; + use hls_m3u8::tags::{ExtXMedia, VariantStream}; use hls_m3u8::types::{MediaType, StreamData}; use hls_m3u8::MasterPlaylist; @@ -9,7 +11,7 @@ macro_rules! generate_tests { $( #[test] fn $fnname() { - assert_eq!($struct, $str.parse().unwrap()); + assert_eq!($struct, TryFrom::try_from($str).unwrap()); assert_eq!($struct.to_string(), $str.to_string()); } diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index b1fb799..9b433ac 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -3,6 +3,7 @@ //! //! TODO: the rest of the tests +use std::convert::TryFrom; use std::time::Duration; use hls_m3u8::tags::{ExtInf, ExtXByteRange}; @@ -15,7 +16,7 @@ macro_rules! generate_tests { $( #[test] fn $fnname() { - assert_eq!($struct, $str.parse().unwrap()); + assert_eq!($struct, TryFrom::try_from($str).unwrap()); assert_eq!($struct.to_string(), $str.to_string()); } diff --git a/tests/rfc8216.rs b/tests/rfc8216.rs index cec0e96..8e28e0b 100644 --- a/tests/rfc8216.rs +++ b/tests/rfc8216.rs @@ -1,4 +1,5 @@ // https://tools.ietf.org/html/rfc8216#section-8 +use std::convert::TryFrom; use std::time::Duration; use hls_m3u8::tags::{ExtInf, ExtXKey, ExtXMedia, VariantStream}; @@ -11,7 +12,7 @@ macro_rules! generate_tests { $( #[test] fn $fnname() { - assert_eq!($struct, $str.parse().unwrap()); + assert_eq!($struct, TryFrom::try_from($str).unwrap()); assert_eq!($struct.to_string(), $str.to_string()); }