diff --git a/src/media_playlist.rs b/src/media_playlist.rs index 0209226..f7855ed 100644 --- a/src/media_playlist.rs +++ b/src/media_playlist.rs @@ -153,8 +153,8 @@ impl MediaPlaylistBuilder { } // CHECK: `#EXT-X-BYTE-RANGE` - if let Some(tag) = s.byte_range() { - if tag.to_range().start().is_none() { + if let Some(range) = s.byte_range() { + if range.start().is_none() { let last_uri = last_range_uri.ok_or_else(Error::invalid_input)?; if last_uri != s.uri() { return Err(Error::invalid_input()); diff --git a/src/media_segment.rs b/src/media_segment.rs index f0b4145..804588a 100644 --- a/src/media_segment.rs +++ b/src/media_segment.rs @@ -120,7 +120,7 @@ mod tests { MediaSegment::builder() //.keys(vec![ExtXKey::empty()]) .map(ExtXMap::new("https://www.example.com/")) - .byte_range(ExtXByteRange::new(20, Some(5))) + .byte_range(ExtXByteRange::from(5..25)) //.date_range() // TODO! .discontinuity(ExtXDiscontinuity) .inf(ExtInf::new(Duration::from_secs(4))) diff --git a/src/tags/media_segment/byte_range.rs b/src/tags/media_segment/byte_range.rs index ce1b733..eecf458 100644 --- a/src/tags/media_segment/byte_range.rs +++ b/src/tags/media_segment/byte_range.rs @@ -1,39 +1,120 @@ use std::fmt; use std::str::FromStr; -use derive_more::{Deref, DerefMut}; +use core::ops::{Add, AddAssign, Sub, SubAssign}; + +use derive_more::{AsMut, AsRef, Deref, DerefMut, From}; use crate::types::{ByteRange, ProtocolVersion}; use crate::utils::tag; use crate::{Error, RequiredVersion}; -/// # [4.4.2.2. EXT-X-BYTERANGE] +/// Indicates that a [`MediaSegment`] is a sub-range of the resource identified +/// by its `URI`. /// -/// The [`ExtXByteRange`] tag indicates that a [`Media Segment`] is a sub-range -/// of the resource identified by its `URI`. +/// # Example /// -/// [`Media Segment`]: crate::MediaSegment -/// [4.4.2.2. EXT-X-BYTERANGE]: -/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.2 -#[derive(Deref, DerefMut, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// Constructing an [`ExtXByteRange`]: +/// +/// ``` +/// # use hls_m3u8::tags::ExtXByteRange; +/// assert_eq!(ExtXByteRange::from(22..55), ExtXByteRange::from(22..=54)); +/// ``` +/// +/// It is also possible to omit the start, in which case it assumes that the +/// [`ExtXByteRange`] starts at the byte after the end of the previous +/// [`ExtXByteRange`] or 0 if there is no previous one. +/// +/// ``` +/// # use hls_m3u8::tags::ExtXByteRange; +/// assert_eq!(ExtXByteRange::from(..55), ExtXByteRange::from(..=54)); +/// ``` +/// +/// [`MediaSegment`]: crate::MediaSegment +#[derive( + AsRef, AsMut, From, Deref, DerefMut, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, +)] +#[from(forward)] pub struct ExtXByteRange(ByteRange); impl ExtXByteRange { pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:"; - /// Makes a new [`ExtXByteRange`] tag. + /// Adds `num` to the `start` and `end` of the range. /// /// # Example /// /// ``` /// # use hls_m3u8::tags::ExtXByteRange; - /// let byte_range = ExtXByteRange::new(20, Some(5)); + /// let range = ExtXByteRange::from(10..22); + /// let nrange = range.saturating_add(5); + /// + /// assert_eq!(nrange.len(), range.len()); + /// assert_eq!(nrange.start(), range.start().map(|c| c + 5)); /// ``` - pub const fn new(length: usize, start: Option) -> Self { - Self(ByteRange::new(length, start)) - } + /// + /// # Overflow + /// + /// If the range is saturated it will not overflow and instead + /// stay at it's current value. + /// + /// ``` + /// # use hls_m3u8::tags::ExtXByteRange; + /// let range = ExtXByteRange::from(5..usize::max_value()); + /// + /// // this would cause the end to overflow + /// let nrange = range.saturating_add(1); + /// + /// // but the range remains unchanged + /// assert_eq!(range, nrange); + /// ``` + /// + /// # Note + /// + /// The length of the range will remain unchanged, + /// if the `start` is `Some`. + #[inline] + #[must_use] + pub fn saturating_add(self, num: usize) -> Self { Self(self.0.saturating_add(num)) } - /// Converts the [`ExtXByteRange`] to a [`ByteRange`]. + /// Subtracts `num` from the `start` and `end` of the range. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::tags::ExtXByteRange; + /// let range = ExtXByteRange::from(10..22); + /// let nrange = range.saturating_sub(5); + /// + /// assert_eq!(nrange.len(), range.len()); + /// assert_eq!(nrange.start(), range.start().map(|c| c - 5)); + /// ``` + /// + /// # Underflow + /// + /// If the range is saturated it will not underflow and instead stay + /// at it's current value. + /// + /// ``` + /// # use hls_m3u8::tags::ExtXByteRange; + /// let range = ExtXByteRange::from(0..10); + /// + /// // this would cause the start to underflow + /// let nrange = range.saturating_sub(1); + /// + /// // but the range remains unchanged + /// assert_eq!(range, nrange); + /// ``` + /// + /// # Note + /// + /// The length of the range will remain unchanged, + /// if the `start` is `Some`. + #[inline] + #[must_use] + pub fn saturating_sub(self, num: usize) -> Self { Self(self.0.saturating_sub(num)) } + + /// Returns a shared reference to the underlying [`ByteRange`]. /// /// # Example /// @@ -41,10 +122,14 @@ impl ExtXByteRange { /// # use hls_m3u8::tags::ExtXByteRange; /// use hls_m3u8::types::ByteRange; /// - /// let byte_range = ExtXByteRange::new(20, Some(5)); - /// let range: ByteRange = byte_range.to_range(); + /// assert_eq!( + /// ExtXByteRange::from(2..11).as_byte_range(), + /// &ByteRange::from(2..11) + /// ); /// ``` - pub const fn to_range(&self) -> ByteRange { self.0 } + #[inline] + #[must_use] + pub const fn as_byte_range(&self) -> &ByteRange { &self.0 } } /// This tag requires [`ProtocolVersion::V4`]. @@ -52,6 +137,48 @@ impl RequiredVersion for ExtXByteRange { fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V4 } } +impl Into for ExtXByteRange { + fn into(self) -> ByteRange { self.0 } +} + +impl Sub for ExtXByteRange +where + ByteRange: Sub, +{ + type Output = Self; + + #[must_use] + #[inline] + fn sub(self, rhs: T) -> Self::Output { Self(self.0.sub(rhs)) } +} + +impl SubAssign for ExtXByteRange +where + ByteRange: SubAssign, +{ + #[inline] + fn sub_assign(&mut self, other: T) { self.0.sub_assign(other); } +} + +impl Add for ExtXByteRange +where + ByteRange: Add, +{ + type Output = Self; + + #[must_use] + #[inline] + fn add(self, rhs: T) -> Self::Output { Self(self.0.add(rhs)) } +} + +impl AddAssign for ExtXByteRange +where + ByteRange: AddAssign, +{ + #[inline] + fn add_assign(&mut self, other: T) { self.0.add_assign(other); } +} + impl fmt::Display for ExtXByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", Self::PREFIX)?; @@ -77,57 +204,52 @@ mod test { #[test] fn test_display() { - let byte_range = ExtXByteRange::new(0, Some(5)); - assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:0@5".to_string()); - - let byte_range = ExtXByteRange::new(99999, Some(2)); assert_eq!( - byte_range.to_string(), - "#EXT-X-BYTERANGE:99999@2".to_string() + ExtXByteRange::from(2..15).to_string(), + "#EXT-X-BYTERANGE:13@2".to_string() ); - let byte_range = ExtXByteRange::new(99999, None); - assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:99999".to_string()); + assert_eq!( + ExtXByteRange::from(..22).to_string(), + "#EXT-X-BYTERANGE:22".to_string() + ); } #[test] fn test_parser() { - let byte_range = ExtXByteRange::new(99999, Some(2)); assert_eq!( - byte_range, - "#EXT-X-BYTERANGE:99999@2".parse::().unwrap() + ExtXByteRange::from(2..15), + "#EXT-X-BYTERANGE:13@2".parse().unwrap() ); - let byte_range = ExtXByteRange::new(99999, None); assert_eq!( - byte_range, - "#EXT-X-BYTERANGE:99999".parse::().unwrap() + ExtXByteRange::from(..22), + "#EXT-X-BYTERANGE:22".parse().unwrap() ); } #[test] fn test_deref() { - let byte_range = ExtXByteRange::new(0, Some(22)); + let byte_range = ExtXByteRange::from(0..22); - assert_eq!(byte_range.length(), 0); - assert_eq!(byte_range.start(), Some(22)); + assert_eq!(byte_range.len(), 22); + assert_eq!(byte_range.start(), Some(0)); } #[test] fn test_deref_mut() { - let mut byte_range = ExtXByteRange::new(0, Some(22)); + let mut byte_range = ExtXByteRange::from(10..110); - byte_range.set_length(100); byte_range.set_start(Some(50)); - assert_eq!(byte_range.length(), 100); + assert_eq!(byte_range.len(), 60); assert_eq!(byte_range.start(), Some(50)); } #[test] fn test_required_version() { assert_eq!( - ExtXByteRange::new(20, Some(5)).required_version(), + ExtXByteRange::from(5..20).required_version(), ProtocolVersion::V4 ); } diff --git a/src/tags/media_segment/map.rs b/src/tags/media_segment/map.rs index 1ab3a93..fa89c13 100644 --- a/src/tags/media_segment/map.rs +++ b/src/tags/media_segment/map.rs @@ -44,13 +44,10 @@ pub struct ExtXMap { /// # use hls_m3u8::tags::ExtXMap; /// use hls_m3u8::types::ByteRange; /// - /// let mut map = ExtXMap::with_range( - /// "https://prod.mediaspace.com/init.bin", - /// ByteRange::new(9, Some(2)), - /// ); + /// let mut map = ExtXMap::with_range("https://prod.mediaspace.com/init.bin", ..9); /// - /// map.set_range(Some(ByteRange::new(1, None))); - /// assert_eq!(map.range(), Some(ByteRange::new(1, None))); + /// map.set_range(Some(2..5)); + /// assert_eq!(map.range(), Some(ByteRange::from(2..5))); /// ``` #[shorthand(enable(copy))] range: Option, @@ -85,15 +82,12 @@ impl ExtXMap { /// # use hls_m3u8::tags::ExtXMap; /// use hls_m3u8::types::ByteRange; /// - /// let map = ExtXMap::with_range( - /// "https://prod.mediaspace.com/init.bin", - /// ByteRange::new(9, Some(2)), - /// ); + /// ExtXMap::with_range("https://prod.mediaspace.com/init.bin", 2..11); /// ``` - pub fn with_range>(uri: T, range: ByteRange) -> Self { + pub fn with_range, B: Into>(uri: I, range: B) -> Self { Self { uri: uri.into(), - range: Some(range), + range: Some(range.into()), keys: vec![], } } @@ -173,7 +167,7 @@ mod test { ); assert_eq!( - ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).to_string(), + ExtXMap::with_range("foo", ByteRange::from(2..11)).to_string(), "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".to_string(), ); } @@ -186,11 +180,11 @@ mod test { ); assert_eq!( - ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), + ExtXMap::with_range("foo", ByteRange::from(2..11)), "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap() ); assert_eq!( - ExtXMap::with_range("foo", ByteRange::new(9, Some(2))), + ExtXMap::with_range("foo", ByteRange::from(2..11)), "#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED" .parse() .unwrap() @@ -201,7 +195,7 @@ mod test { fn test_required_version() { assert_eq!(ExtXMap::new("foo").required_version(), ProtocolVersion::V6); assert_eq!( - ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).required_version(), + ExtXMap::with_range("foo", ByteRange::from(2..11)).required_version(), ProtocolVersion::V6 ); } diff --git a/src/types/byte_range.rs b/src/types/byte_range.rs index d5018e2..b0004b1 100644 --- a/src/types/byte_range.rs +++ b/src/types/byte_range.rs @@ -1,67 +1,394 @@ -use std::fmt; -use std::str::FromStr; +use core::convert::TryInto; +use core::fmt; +use core::ops::{ + Add, AddAssign, Bound, Range, RangeBounds, RangeInclusive, RangeTo, RangeToInclusive, Sub, + SubAssign, +}; +use core::str::FromStr; use shorthand::ShortHand; use crate::Error; -/// Byte range. +/// A range of bytes, which can be seen as either `..end` or `start..end`. /// -/// See: [4.3.2.2. EXT-X-BYTERANGE] +/// It can be constructed from `..end` and `start..end`: /// -/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 +/// ``` +/// use hls_m3u8::types::ByteRange; +/// +/// let range = ByteRange::from(10..20); +/// let range = ByteRange::from(..20); +/// ``` #[derive(ShortHand, Copy, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] -#[shorthand(enable(must_use))] +#[shorthand(enable(must_use, copy), disable(option_as_ref, set))] pub struct ByteRange { - /// The length of the range. + /// Returns the `start` of the [`ByteRange`], if there is one. /// /// # Example /// /// ``` /// # use hls_m3u8::types::ByteRange; - /// # - /// let mut range = ByteRange::new(20, Some(3)); - /// # assert_eq!(range.length(), 20); - /// - /// range.set_length(10); - /// assert_eq!(range.length(), 10); + /// assert_eq!(ByteRange::from(0..5).start(), Some(0)); + /// assert_eq!(ByteRange::from(..5).start(), None); /// ``` - length: usize, - /// The start of the range. - /// - /// # Example - /// - /// ``` - /// # use hls_m3u8::types::ByteRange; - /// # - /// let mut range = ByteRange::new(20, None); - /// # assert_eq!(range.start(), None); - /// - /// range.set_start(Some(3)); - /// assert_eq!(range.start(), Some(3)); - /// ``` - // - // this is a workaround until this issue is fixed: - // https://github.com/Luro02/shorthand/issues/20 - #[shorthand(enable(copy), disable(option_as_ref))] start: Option, + /// Returns the `end` of the [`ByteRange`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// assert_eq!(ByteRange::from(0..5).start(), Some(0)); + /// assert_eq!(ByteRange::from(..5).start(), None); + /// ``` + end: usize, } impl ByteRange { - /// Creates a new [`ByteRange`]. + /// Changes the length of the [`ByteRange`]. /// /// # Example /// /// ``` /// # use hls_m3u8::types::ByteRange; - /// ByteRange::new(22, Some(12)); + /// let mut range = ByteRange::from(0..5); + /// range.set_len(2); + /// + /// assert_eq!(range, ByteRange::from(0..2)); + /// + /// range.set_len(200); + /// assert_eq!(range, ByteRange::from(0..200)); /// ``` - pub const fn new(length: usize, start: Option) -> Self { Self { length, start } } + /// + /// # Note + /// + /// The `start` will not be changed. + pub fn set_len(&mut self, new_len: usize) { + // the new_len can be either greater or smaller than `self.len()`. + // if new_len is larger `checked_sub` will return `None` + if let Some(value) = self.len().checked_sub(new_len) { + self.end -= value; + } else { + self.end += new_len.saturating_sub(self.len()); + } + } + + /// Sets the `start` of the [`ByteRange`]. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// assert_eq!(ByteRange::from(0..5).set_start(Some(5)).start(), Some(5)); + /// assert_eq!(ByteRange::from(..5).set_start(Some(2)).start(), Some(2)); + /// ``` + /// + /// # Panics + /// + /// This function will panic, if the `new_start` is larger, than the + /// [`end`](ByteRange::end). + pub fn set_start(&mut self, new_start: Option) -> &mut Self { + if new_start.map_or(false, |s| s > self.end) { + panic!("attempt to make the start larger than the end"); + } + + self.start = new_start; + + self + } + + /// Adds `num` to the `start` and `end` of the range. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(10..22); + /// let nrange = range.saturating_add(5); + /// + /// assert_eq!(nrange.len(), range.len()); + /// assert_eq!(nrange.start(), range.start().map(|c| c + 5)); + /// ``` + /// + /// # Overflow + /// + /// If the range is saturated it will not overflow and instead stay + /// at it's current value. + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(5..usize::max_value()); + /// + /// // this would cause the end to overflow + /// let nrange = range.saturating_add(1); + /// + /// // but the range remains unchanged + /// assert_eq!(range, nrange); + /// ``` + /// + /// # Note + /// + /// The length of the range will remain unchanged, + /// if the `start` is `Some`. + #[must_use] + pub fn saturating_add(mut self, num: usize) -> Self { + if let Some(start) = self.start { + // add the number to the start + if let (Some(start), Some(end)) = (start.checked_add(num), self.end.checked_add(num)) { + self.start = Some(start); + self.end = end; + } else { + // it is ensured at construction that the start will never be larger than the + // end. This clause can therefore be only reached if the end overflowed. + // -> It is only possible to add `usize::max_value() - end` to the start. + if let Some(start) = start.checked_add(usize::max_value() - self.end) { + self.start = Some(start); + self.end = usize::max_value(); + } else { + // both end + start overflowed -> do not change anything + } + } + } else { + self.end = self.end.saturating_add(num); + } + + self + } + + /// Subtracts `num` from the `start` and `end` of the range. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(10..22); + /// let nrange = range.saturating_sub(5); + /// + /// assert_eq!(nrange.len(), range.len()); + /// assert_eq!(nrange.start(), range.start().map(|c| c - 5)); + /// ``` + /// + /// # Underflow + /// + /// If the range is saturated it will not underflow and instead stay + /// at it's current value. + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(0..10); + /// + /// // this would cause the start to underflow + /// let nrange = range.saturating_sub(1); + /// + /// // but the range remains unchanged + /// assert_eq!(range, nrange); + /// ``` + /// + /// # Note + /// + /// The length of the range will remain unchanged, + /// if the `start` is `Some`. + #[must_use] + pub fn saturating_sub(mut self, num: usize) -> Self { + if let Some(start) = self.start { + // subtract the number from the start + if let (Some(start), Some(end)) = (start.checked_sub(num), self.end.checked_sub(num)) { + self.start = Some(start); + self.end = end; + } else { + // it is ensured at construction that the start will never be larger, than the + // end so this clause will only be reached, if the start underflowed. + // -> can at most subtract `start` from `end` + if let Some(end) = self.end.checked_sub(start) { + self.start = Some(0); + self.end = end; + } else { + // both end + start underflowed + // -> do not change anything + } + } + } else { + self.end = self.end.saturating_sub(num); + } + + self + } + + /// Returns the length, which is calculated by subtracting the `end` from + /// the `start`. If the `start` is `None` a 0 is assumed. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(1..16); + /// + /// assert_eq!(range.len(), 15); + /// ``` + #[inline] + #[must_use] + pub fn len(&self) -> usize { self.end.saturating_sub(self.start.unwrap_or(0)) } + + /// Returns `true` if the length is zero. + /// + /// # Example + /// + /// ``` + /// # use hls_m3u8::types::ByteRange; + /// let range = ByteRange::from(12..12); + /// + /// assert_eq!(range.is_empty(), true); + /// ``` + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { self.len() == 0 } +} + +impl Sub for ByteRange { + type Output = Self; + + #[must_use] + #[inline] + fn sub(self, rhs: usize) -> Self::Output { + Self { + start: self.start.map(|lhs| lhs - rhs), + end: self.end - rhs, + } + } +} + +impl SubAssign for ByteRange { + #[inline] + fn sub_assign(&mut self, other: usize) { *self = >::sub(*self, other); } +} + +impl Add for ByteRange { + type Output = Self; + + #[must_use] + #[inline] + fn add(self, rhs: usize) -> Self::Output { + Self { + start: self.start.map(|lhs| lhs + rhs), + end: self.end + rhs, + } + } +} + +impl AddAssign for ByteRange { + #[inline] + fn add_assign(&mut self, other: usize) { *self = >::add(*self, other); } +} + +macro_rules! impl_from_ranges { + ( $( $type:tt ),* ) => { + $( + #[allow(trivial_numeric_casts, clippy::fallible_impl_from)] + impl From> for ByteRange { + fn from(range: Range<$type>) -> Self { + if range.start > range.end { + panic!("the range start must be smaller than the end"); + } + + Self { + start: Some(range.start as usize), + end: range.end as usize, + } + } + } + + #[allow(trivial_numeric_casts, clippy::fallible_impl_from)] + impl From> for ByteRange { + fn from(range: RangeInclusive<$type>) -> Self { + let (start, end) = range.into_inner(); + + if start > end { + panic!("the range start must be smaller than the end"); + } + + Self { + start: Some(start as usize), + end: (end as usize).saturating_add(1), + } + } + } + + #[allow(trivial_numeric_casts, clippy::fallible_impl_from)] + impl From> for ByteRange { + fn from(range: RangeTo<$type>) -> Self { + Self { + start: None, + end: range.end as usize, + } + } + } + + #[allow(trivial_numeric_casts, clippy::fallible_impl_from)] + impl From> for ByteRange { + fn from(range: RangeToInclusive<$type>) -> Self { + Self { + start: None, + end: (range.end as usize).saturating_add(1), + } + } + } + )* + } +} + +// TODO: replace with generics as soon as overlapping trait implementations are +// stable (`Into for usize` is reserved for upstream crates ._.) +impl_from_ranges![u64, u32, u16, u8, usize, i32]; + +#[must_use] +impl RangeBounds for ByteRange { + fn start_bound(&self) -> Bound<&usize> { + if let Some(start) = &self.start { + Bound::Included(start) + } else { + Bound::Unbounded + } + } + + #[inline] + fn end_bound(&self) -> Bound<&usize> { Bound::Excluded(&self.end) } +} + +/// This conversion will fail if the start of the [`ByteRange`] is `Some`. +impl TryInto> for ByteRange { + type Error = Error; + + fn try_into(self) -> Result, Self::Error> { + if self.start.is_some() { + return Err(Error::custom("A `RangeTo` (`..end`) does not have a start")); + } + + Ok(RangeTo { end: self.end }) + } +} + +/// This conversion will fail if the start of the [`ByteRange`] is `None`. +impl TryInto> for ByteRange { + type Error = Error; + + fn try_into(self) -> Result, Self::Error> { + if self.start.is_none() { + return Err(Error::custom( + "A `Range` (`start..end`) has to have a start.", + )); + } + + Ok(Range { + start: self.start.unwrap(), + end: self.end, + }) + } } impl fmt::Display for ByteRange { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.length)?; + write!(f, "{}", self.len())?; if let Some(value) = self.start { write!(f, "@{}", value)?; @@ -77,17 +404,20 @@ impl FromStr for ByteRange { fn from_str(input: &str) -> Result { let mut input = input.splitn(2, '@'); - let length = input - .next() - .ok_or_else(|| Error::custom("missing length"))?; - let length = length.parse().map_err(|e| Error::parse_int(length, e))?; + let length = input.next().unwrap(); + let length = length + .parse::() + .map_err(|e| Error::parse_int(length, e))?; let start = input .next() - .map(|v| v.parse().map_err(|e| Error::parse_int(v, e))) + .map(|v| v.parse::().map_err(|e| Error::parse_int(v, e))) .transpose()?; - Ok(Self::new(length, start)) + Ok(Self { + start, + end: start.unwrap_or(0) + length, + }) } } @@ -97,59 +427,239 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn test_display() { + #[should_panic = "the range start must be smaller than the end"] + fn test_from_range_panic() { let _ = ByteRange::from(6..0); } + + #[test] + #[should_panic = "the range start must be smaller than the end"] + fn test_from_range_inclusive_panic() { let _ = ByteRange::from(6..=0); } + + #[test] + fn test_from_ranges() { + assert_eq!(ByteRange::from(1..10), ByteRange::from(1..=9)); + assert_eq!(ByteRange::from(..10), ByteRange::from(..=9)); + } + + #[test] + fn test_range_bounds() { + assert_eq!(ByteRange::from(0..10).start_bound(), Bound::Included(&0)); + assert_eq!(ByteRange::from(..10).start_bound(), Bound::Unbounded); + + assert_eq!(ByteRange::from(0..10).end_bound(), Bound::Excluded(&10)); + assert_eq!(ByteRange::from(..10).end_bound(), Bound::Excluded(&10)); + } + + #[test] + fn test_try_into() { + assert_eq!(ByteRange::from(1..4).try_into(), Ok(1..4)); + assert_eq!(ByteRange::from(..4).try_into(), Ok(..4)); + + assert!(TryInto::>::try_into(ByteRange::from(1..4)).is_err()); + assert!(TryInto::>::try_into(ByteRange::from(..4)).is_err()); + } + + #[test] + fn test_add_assign() { + let mut range = ByteRange::from(5..10); + range += 5; + + assert_eq!(range, ByteRange::from(10..15)); + } + + #[test] + #[should_panic = "attempt to add with overflow"] + fn test_add_assign_panic() { + let mut range = ByteRange::from(4..usize::max_value()); + range += 5; + + unreachable!(); + } + + #[test] + fn test_sub_assign() { + let mut range = ByteRange::from(10..20); + range -= 5; + + assert_eq!(range, ByteRange::from(5..15)); + } + + #[test] + #[should_panic = "attempt to subtract with overflow"] + fn test_sub_assign_panic() { + let mut range = ByteRange::from(4..10); + range -= 5; + + unreachable!(); + } + + #[test] + #[should_panic = "attempt to make the start larger than the end"] + fn test_set_start() { let _ = ByteRange::from(4..10).set_start(Some(11)); } + + #[test] + fn test_add() { + // normal addition + assert_eq!(ByteRange::from(5..10) + 5, ByteRange::from(10..15)); + assert_eq!(ByteRange::from(..10) + 5, ByteRange::from(..15)); + + // adding 0 + assert_eq!(ByteRange::from(5..10) + 0, ByteRange::from(5..10)); + assert_eq!(ByteRange::from(..10) + 0, ByteRange::from(..10)); + } + + #[test] + #[should_panic = "attempt to add with overflow"] + fn test_add_panic() { let _ = ByteRange::from(usize::max_value()..usize::max_value()) + 1; } + + #[test] + fn test_sub() { + // normal subtraction + assert_eq!(ByteRange::from(5..10) - 4, ByteRange::from(1..6)); + assert_eq!(ByteRange::from(..10) - 4, ByteRange::from(..6)); + + // subtracting 0 + assert_eq!(ByteRange::from(0..0) - 0, ByteRange::from(0..0)); + assert_eq!(ByteRange::from(2..3) - 0, ByteRange::from(2..3)); + + assert_eq!(ByteRange::from(..0) - 0, ByteRange::from(..0)); + assert_eq!(ByteRange::from(..3) - 0, ByteRange::from(..3)); + } + + #[test] + #[should_panic = "attempt to subtract with overflow"] + fn test_sub_panic() { let _ = ByteRange::from(0..0) - 1; } + + #[test] + fn test_saturating_add() { + // normal addition assert_eq!( - ByteRange { - length: 0, - start: Some(5), - } - .to_string(), - "0@5".to_string() + ByteRange::from(5..10).saturating_add(5), + ByteRange::from(10..15) + ); + assert_eq!( + ByteRange::from(..10).saturating_add(5), + ByteRange::from(..15) + ); + + // adding 0 + assert_eq!( + ByteRange::from(6..11).saturating_add(0), + ByteRange::from(6..11) + ); + assert_eq!( + ByteRange::from(..11).saturating_add(0), + ByteRange::from(..11) ); assert_eq!( - ByteRange { - length: 99999, - start: Some(2), - } - .to_string(), - "99999@2".to_string() + ByteRange::from(0..0).saturating_add(0), + ByteRange::from(0..0) + ); + assert_eq!(ByteRange::from(..0).saturating_add(0), ByteRange::from(..0)); + + // overflow + assert_eq!( + ByteRange::from(usize::max_value()..usize::max_value()).saturating_add(1), + ByteRange::from(usize::max_value()..usize::max_value()) + ); + assert_eq!( + ByteRange::from(..usize::max_value()).saturating_add(1), + ByteRange::from(..usize::max_value()) ); assert_eq!( - ByteRange { - length: 99999, - start: None, - } - .to_string(), - "99999".to_string() + ByteRange::from(usize::max_value() - 5..usize::max_value()).saturating_add(1), + ByteRange::from(usize::max_value() - 5..usize::max_value()) + ); + + // overflow, but something can be added to the range: + assert_eq!( + ByteRange::from(usize::max_value() - 5..usize::max_value() - 3).saturating_add(4), + ByteRange::from(usize::max_value() - 2..usize::max_value()) + ); + + assert_eq!( + ByteRange::from(..usize::max_value() - 3).saturating_add(4), + ByteRange::from(..usize::max_value()) ); } + #[test] + fn test_saturating_sub() { + // normal subtraction + assert_eq!( + ByteRange::from(5..10).saturating_sub(4), + ByteRange::from(1..6) + ); + + // subtracting 0 + assert_eq!( + ByteRange::from(0..0).saturating_sub(0), + ByteRange::from(0..0) + ); + assert_eq!( + ByteRange::from(2..3).saturating_sub(0), + ByteRange::from(2..3) + ); + + // the start underflows + assert_eq!( + ByteRange::from(0..5).saturating_sub(4), + ByteRange::from(0..5) + ); + + // the start underflows, but one can still subtract something from it + assert_eq!( + ByteRange::from(1..5).saturating_sub(2), + ByteRange::from(0..4) + ); + + // both start and end underflow + assert_eq!( + ByteRange::from(1..3).saturating_sub(5), + ByteRange::from(0..2) + ); + + // both start + end are 0 + underflow + assert_eq!( + ByteRange::from(0..0).saturating_sub(1), + ByteRange::from(0..0) + ); + + // half open ranges: + assert_eq!(ByteRange::from(..6).saturating_sub(2), ByteRange::from(..4)); + assert_eq!(ByteRange::from(..5).saturating_sub(0), ByteRange::from(..5)); + assert_eq!(ByteRange::from(..0).saturating_sub(0), ByteRange::from(..0)); + + assert_eq!(ByteRange::from(..0).saturating_sub(1), ByteRange::from(..0)); + } + + #[test] + fn test_display() { + assert_eq!(ByteRange::from(0..5).to_string(), "5@0".to_string()); + + assert_eq!( + ByteRange::from(2..100001).to_string(), + "99999@2".to_string() + ); + + assert_eq!(ByteRange::from(..99999).to_string(), "99999".to_string()); + } + #[test] fn test_parser() { + assert_eq!(ByteRange::from(2..22), "20@2".parse().unwrap()); + + assert_eq!(ByteRange::from(..300), "300".parse().unwrap()); + assert_eq!( - ByteRange { - length: 99999, - start: Some(2), - }, - "99999@2".parse::().unwrap() + ByteRange::from_str("a"), + Err(Error::parse_int("a", "a".parse::().unwrap_err())) ); assert_eq!( - ByteRange { - length: 99999, - start: Some(2), - }, - "99999@2".parse::().unwrap() - ); - - assert_eq!( - ByteRange { - length: 99999, - start: None, - }, - "99999".parse::().unwrap() + ByteRange::from_str("1@a"), + Err(Error::parse_int("a", "a".parse::().unwrap_err())) ); assert!("".parse::().is_err()); diff --git a/tests/media_playlist.rs b/tests/media_playlist.rs index e597bdc..c43c131 100644 --- a/tests/media_playlist.rs +++ b/tests/media_playlist.rs @@ -13,19 +13,19 @@ fn test_media_playlist_with_byterange() { .segments(vec![ MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(10.0))) - .byte_range(ExtXByteRange::new(75232, Some(0))) + .byte_range(ExtXByteRange::from(0..75232)) .uri("video.ts") .build() .unwrap(), MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(10.0))) - .byte_range(ExtXByteRange::new(82112, Some(752321))) + .byte_range(ExtXByteRange::from(752321..82112 + 752321)) .uri("video.ts") .build() .unwrap(), MediaSegment::builder() .inf(ExtInf::new(Duration::from_secs_f64(10.0))) - .byte_range(ExtXByteRange::new(69864, None)) + .byte_range(ExtXByteRange::from(..69864)) .uri("video.ts") .build() .unwrap(),