1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-11-24 16:11:00 +00:00

more tests #25 + better docs #31

This commit is contained in:
Luro02 2019-09-22 20:33:40 +02:00
parent b197d5fbd7
commit 6b717f97c2
41 changed files with 2228 additions and 649 deletions

View file

@ -4,60 +4,13 @@ name: Rust
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- stable
os:
- ubuntu-latest
# execute cargo build
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- uses: actions-rs/cargo@v1
with:
command: build
arguments: --all-features
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- stable
os:
- ubuntu-latest
# execute cargo test
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- uses: actions-rs/cargo@v1
with:
command: test
rustfmt: rustfmt:
name: Rustfmt
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ matrix.rust }} toolchain: stable
override: true
- run: rustup component add rustfmt - run: rustup component add rustfmt
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
@ -65,20 +18,14 @@ jobs:
args: --all -- --check args: --all -- --check
clippy: clippy:
name: Clippy
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ matrix.rust }} toolchain: stable
override: true
- run: rustup component add clippy - run: rustup component add clippy
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: clippy command: clippy
args: -- -D warnings # args: -- -D warnings

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/target/ /target/
**/*.rs.bk **/*.rs.bk
Cargo.lock Cargo.lock
tarpaulin-report.html

View file

@ -1,5 +1,17 @@
language: rust language: rust
sudo: required
cache: cargo
before_cache: |
cargo install cargo-tarpaulin
cargo install cargo-update
cargo install-update --all
# before_cache:
# - rm -rf /home/travis/.cargo/registry
rust: rust:
- stable - stable
- beta - beta
@ -8,32 +20,14 @@ matrix:
allow_failures: allow_failures:
- rust: nightly - rust: nightly
env: script:
global: - cargo clean
- RUSTFLAGS="-C link-dead-code" - cargo build
- cargo test
addons:
apt:
packages:
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev
- cmake
- gcc
- binutils-dev
- libiberty-dev
after_success: | after_success: |
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && # this does require a -Z flag for Doctests, which is unstable!
tar xzf master.tar.gz && if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
cd kcov-master && cargo tarpaulin --run-types Tests Doctests --out Xml
mkdir build && bash <(curl -s https://codecov.io/bash)
cd build && fi
cmake .. &&
make &&
make install DESTDIR=../../kcov-build &&
cd ../.. &&
rm -rf kcov-master &&
for file in target/debug/hls_m3u8-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done &&
bash <(curl -s https://codecov.io/bash) &&
echo "Uploaded code coverage"

View file

@ -12,13 +12,15 @@ edition = "2018"
categories = ["parser"] categories = ["parser"]
[badges] [badges]
travis-ci = {repository = "sile/hls_m3u8"} travis-ci = { repository = "sile/hls_m3u8" }
codecov = {repository = "sile/hls_m3u8"} codecov = { repository = "sile/hls_m3u8" }
[dependencies] [dependencies]
failure = "0.1.5" failure = "0.1.5"
derive_builder = "0.7.2" derive_builder = "0.7.2"
chrono = "0.4.9" chrono = "0.4.9"
strum = { version = "0.16.0", features = ["derive"] }
derive_more = "0.15.0"
[dev-dependencies] [dev-dependencies]
clap = "2" clap = "2"

View file

@ -55,11 +55,14 @@ impl FromStr for AttributePairs {
let pair = split(line.trim(), '='); let pair = split(line.trim(), '=');
if pair.len() < 2 { if pair.len() < 2 {
return Err(Error::invalid_input()); continue;
} }
let key = pair[0].to_uppercase(); let key = pair[0].trim().to_uppercase();
let value = pair[1].to_string(); let value = pair[1].trim().to_string();
if value.is_empty() {
continue;
}
result.insert(key.trim().to_string(), value.trim().to_string()); result.insert(key.trim().to_string(), value.trim().to_string());
} }
@ -122,6 +125,11 @@ mod test {
let mut iterator = pairs.iter(); let mut iterator = pairs.iter();
assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3")); assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3"));
let mut pairs = AttributePairs::new();
pairs.insert("FOO".to_string(), "BAR".to_string());
assert_eq!("FOO=BAR,VAL".parse::<AttributePairs>().unwrap(), pairs);
} }
#[test] #[test]
@ -136,4 +144,18 @@ mod test {
let mut iterator = attrs.iter(); let mut iterator = attrs.iter();
assert!(iterator.any(|(k, v)| k == "key_02" && v == "value_02")); assert!(iterator.any(|(k, v)| k == "key_02" && v == "value_02"));
} }
#[test]
fn test_into_iter() {
let mut map = HashMap::new();
map.insert("k".to_string(), "v".to_string());
let mut attrs = AttributePairs::new();
attrs.insert("k".to_string(), "v".to_string());
assert_eq!(
attrs.into_iter().collect::<Vec<_>>(),
map.into_iter().collect::<Vec<_>>()
);
}
} }

View file

@ -1,4 +1,3 @@
use std::error;
use std::fmt; use std::fmt;
use failure::{Backtrace, Context, Fail}; use failure::{Backtrace, Context, Fail};
@ -118,13 +117,6 @@ impl From<Context<ErrorKind>> for Error {
} }
impl Error { impl Error {
pub(crate) fn unknown<T>(value: T) -> Self
where
T: error::Error,
{
Self::from(ErrorKind::UnknownError(value.to_string()))
}
pub(crate) fn missing_value<T: ToString>(value: T) -> Self { pub(crate) fn missing_value<T: ToString>(value: T) -> Self {
Self::from(ErrorKind::MissingValue(value.to_string())) Self::from(ErrorKind::MissingValue(value.to_string()))
} }
@ -218,3 +210,9 @@ impl From<::chrono::ParseError> for Error {
Error::chrono(value) Error::chrono(value)
} }
} }
impl From<::strum::ParseError> for Error {
fn from(value: ::strum::ParseError) -> Self {
Error::custom(value) // TODO!
}
}

View file

@ -1,9 +1,17 @@
#![forbid(unsafe_code)]
#![warn( #![warn(
//clippy::pedantic, //clippy::pedantic,
clippy::nursery, clippy::nursery,
clippy::cargo clippy::cargo
)] )]
#![warn(missing_docs)] #![allow(clippy::multiple_crate_versions)]
#![warn(
missing_docs,
missing_copy_implementations,
missing_debug_implementations,
trivial_casts, // TODO (needed?)
trivial_numeric_casts
)]
//! [HLS] m3u8 parser/generator. //! [HLS] m3u8 parser/generator.
//! //!
//! [HLS]: https://tools.ietf.org/html/rfc8216 //! [HLS]: https://tools.ietf.org/html/rfc8216

View file

@ -26,8 +26,7 @@ impl FromStr for Lines {
for l in input.lines() { for l in input.lines() {
let line = l.trim(); let line = l.trim();
// ignore empty lines if line.is_empty() {
if line.len() == 0 {
continue; continue;
} }
@ -39,7 +38,7 @@ impl FromStr for Lines {
continue; continue;
} else if line.starts_with("#EXT") { } else if line.starts_with("#EXT") {
Line::Tag(line.parse()?) Line::Tag(line.parse()?)
} else if line.starts_with("#") { } else if line.starts_with('#') {
continue; // ignore comments continue; // ignore comments
} else { } else {
// stream inf line needs special treatment // stream inf line needs special treatment

View file

@ -103,7 +103,7 @@ impl MasterPlaylistBuilder {
let required_version = self.required_version(); let required_version = self.required_version();
let specified_version = self let specified_version = self
.version_tag .version_tag
.unwrap_or(required_version.into()) .unwrap_or_else(|| required_version.into())
.version(); .version();
if required_version > specified_version { if required_version > specified_version {
@ -164,7 +164,7 @@ impl MasterPlaylistBuilder {
.flatten(), .flatten(),
) )
.max() .max()
.unwrap_or(ProtocolVersion::latest()) .unwrap_or_else(ProtocolVersion::latest)
} }
fn validate_stream_inf_tags(&self) -> crate::Result<()> { fn validate_stream_inf_tags(&self) -> crate::Result<()> {
@ -188,26 +188,25 @@ impl MasterPlaylistBuilder {
} }
} }
match t.closed_captions() { match t.closed_captions() {
Some(&ClosedCaptions::GroupId(ref group_id)) => { &Some(ClosedCaptions::GroupId(ref group_id)) => {
if !self.check_media_group(MediaType::ClosedCaptions, group_id) { if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
return Err(Error::unmatched_group(group_id)); return Err(Error::unmatched_group(group_id));
} }
} }
Some(&ClosedCaptions::None) => { &Some(ClosedCaptions::None) => {
has_none_closed_captions = true; has_none_closed_captions = true;
} }
None => {} None => {}
} }
} }
if has_none_closed_captions { if has_none_closed_captions
if !value && !value
.iter() .iter()
.all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) .all(|t| t.closed_captions() == &Some(ClosedCaptions::None))
{ {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
} }
}
Ok(()) Ok(())
} }

View file

@ -70,7 +70,7 @@ impl MediaPlaylistBuilder {
let required_version = self.required_version(); let required_version = self.required_version();
let specified_version = self let specified_version = self
.version_tag .version_tag
.unwrap_or(required_version.into()) .unwrap_or_else(|| required_version.into())
.version(); .version();
if required_version > specified_version { if required_version > specified_version {
@ -109,7 +109,7 @@ impl MediaPlaylistBuilder {
} }
}; };
if !(rounded_segment_duration <= max_segment_duration) { if rounded_segment_duration > max_segment_duration {
return Err(Error::custom(format!( return Err(Error::custom(format!(
"Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}", "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}",
segment_duration, segment_duration,
@ -122,7 +122,7 @@ impl MediaPlaylistBuilder {
// CHECK: `#EXT-X-BYTE-RANGE` // CHECK: `#EXT-X-BYTE-RANGE`
if let Some(tag) = s.byte_range_tag() { if let Some(tag) = s.byte_range_tag() {
if tag.to_range().start().is_none() { if tag.to_range().start().is_none() {
let last_uri = last_range_uri.ok_or(Error::invalid_input())?; let last_uri = last_range_uri.ok_or_else(Error::invalid_input)?;
if last_uri != s.uri() { if last_uri != s.uri() {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
@ -200,7 +200,7 @@ impl MediaPlaylistBuilder {
.unwrap_or(ProtocolVersion::V1) .unwrap_or(ProtocolVersion::V1)
})) }))
.max() .max()
.unwrap_or(ProtocolVersion::latest()) .unwrap_or_else(ProtocolVersion::latest)
} }
/// Adds a media segment to the resulting playlist. /// Adds a media segment to the resulting playlist.

View file

@ -50,7 +50,7 @@ impl ExtXVersion {
/// ProtocolVersion::V6 /// ProtocolVersion::V6
/// ); /// );
/// ``` /// ```
pub const fn version(&self) -> ProtocolVersion { pub const fn version(self) -> ProtocolVersion {
self.0 self.0
} }
} }

View file

@ -21,7 +21,7 @@ use crate::Error;
/// [Master Playlist]: crate::MasterPlaylist /// [Master Playlist]: crate::MasterPlaylist
/// [Media Playlist]: crate::MediaPlaylist /// [Media Playlist]: crate::MediaPlaylist
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3 /// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtXIFrameStreamInf { pub struct ExtXIFrameStreamInf {
uri: String, uri: String,
stream_inf: StreamInf, stream_inf: StreamInf,
@ -31,6 +31,12 @@ impl ExtXIFrameStreamInf {
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:"; pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
/// Makes a new [ExtXIFrameStreamInf] tag. /// Makes a new [ExtXIFrameStreamInf] tag.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXIFrameStreamInf;
/// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
/// ```
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self { pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
ExtXIFrameStreamInf { ExtXIFrameStreamInf {
uri: uri.to_string(), uri: uri.to_string(),
@ -43,7 +49,6 @@ impl ExtXIFrameStreamInf {
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::tags::ExtXIFrameStreamInf; /// # use hls_m3u8::tags::ExtXIFrameStreamInf;
/// #
/// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20); /// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
/// assert_eq!(stream.uri(), &"https://www.example.com".to_string()); /// assert_eq!(stream.uri(), &"https://www.example.com".to_string());
/// ``` /// ```
@ -91,13 +96,12 @@ impl FromStr for ExtXIFrameStreamInf {
let mut uri = None; let mut uri = None;
for (key, value) in input.parse::<AttributePairs>()? { for (key, value) in input.parse::<AttributePairs>()? {
match key.as_str() { if let "URI" = key.as_str() {
"URI" => uri = Some(unquote(value)), uri = Some(unquote(value));
_ => {}
} }
} }
let uri = uri.ok_or(Error::missing_value("URI"))?; let uri = uri.ok_or_else(|| Error::missing_value("URI"))?;
Ok(Self { Ok(Self {
uri, uri,
@ -140,6 +144,8 @@ mod test {
.unwrap(), .unwrap(),
ExtXIFrameStreamInf::new("foo", 1000) ExtXIFrameStreamInf::new("foo", 1000)
); );
assert!("garbage".parse::<ExtXIFrameStreamInf>().is_err());
} }
#[test] #[test]
@ -149,4 +155,22 @@ mod test {
ProtocolVersion::V1 ProtocolVersion::V1
); );
} }
#[test]
fn test_deref() {
assert_eq!(
ExtXIFrameStreamInf::new("https://www.example.com", 20).average_bandwidth(),
None
)
}
#[test]
fn test_deref_mut() {
assert_eq!(
ExtXIFrameStreamInf::new("https://www.example.com", 20)
.set_average_bandwidth(Some(4))
.average_bandwidth(),
Some(4)
)
}
} }

File diff suppressed because it is too large Load diff

View file

@ -61,7 +61,7 @@ impl ExtXSessionData {
/// ); /// );
/// ``` /// ```
pub fn new<T: ToString>(data_id: T, data: SessionData) -> Self { pub fn new<T: ToString>(data_id: T, data: SessionData) -> Self {
ExtXSessionData { Self {
data_id: data_id.to_string(), data_id: data_id.to_string(),
data, data,
language: None, language: None,
@ -107,7 +107,7 @@ impl ExtXSessionData {
/// ); /// );
/// ``` /// ```
pub fn with_language<T: ToString>(data_id: T, data: SessionData, language: T) -> Self { pub fn with_language<T: ToString>(data_id: T, data: SessionData, language: T) -> Self {
ExtXSessionData { Self {
data_id: data_id.to_string(), data_id: data_id.to_string(),
data, data,
language: Some(language.to_string()), language: Some(language.to_string()),
@ -256,13 +256,16 @@ impl fmt::Display for ExtXSessionData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", Self::PREFIX)?; write!(f, "{}", Self::PREFIX)?;
write!(f, "DATA-ID={}", quote(&self.data_id))?; write!(f, "DATA-ID={}", quote(&self.data_id))?;
match &self.data { match &self.data {
SessionData::Value(value) => write!(f, ",VALUE={}", quote(value))?, SessionData::Value(value) => write!(f, ",VALUE={}", quote(value))?,
SessionData::Uri(value) => write!(f, ",URI={}", quote(value))?, SessionData::Uri(value) => write!(f, ",URI={}", quote(value))?,
} }
if let Some(value) = &self.language { if let Some(value) = &self.language {
write!(f, ",LANGUAGE={}", quote(value))?; write!(f, ",LANGUAGE={}", quote(value))?;
} }
Ok(()) Ok(())
} }
} }
@ -291,7 +294,7 @@ impl FromStr for ExtXSessionData {
} }
} }
let data_id = data_id.ok_or(Error::missing_value("EXT-X-DATA-ID"))?; let data_id = data_id.ok_or_else(|| Error::missing_value("EXT-X-DATA-ID"))?;
let data = { let data = {
if let Some(value) = session_value { if let Some(value) = session_value {
if uri.is_some() { if uri.is_some() {
@ -306,7 +309,7 @@ impl FromStr for ExtXSessionData {
} }
}; };
Ok(ExtXSessionData { Ok(Self {
data_id, data_id,
data, data,
language, language,
@ -321,7 +324,10 @@ mod test {
#[test] #[test]
fn test_display() { fn test_display() {
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"".to_string(), "#EXT-X-SESSION-DATA:\
DATA-ID=\"com.example.lyrics\",\
URI=\"lyrics.json\""
.to_string(),
ExtXSessionData::new( ExtXSessionData::new(
"com.example.lyrics", "com.example.lyrics",
SessionData::Uri("lyrics.json".to_string()) SessionData::Uri("lyrics.json".to_string())
@ -330,8 +336,10 @@ mod test {
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ "#EXT-X-SESSION-DATA:\
VALUE=\"This is an example\",LANGUAGE=\"en\"" DATA-ID=\"com.example.title\",\
VALUE=\"This is an example\",\
LANGUAGE=\"en\""
.to_string(), .to_string(),
ExtXSessionData::with_language( ExtXSessionData::with_language(
"com.example.title", "com.example.title",
@ -342,8 +350,10 @@ mod test {
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ "#EXT-X-SESSION-DATA:\
VALUE=\"Este es un ejemplo\",LANGUAGE=\"es\"" DATA-ID=\"com.example.title\",\
VALUE=\"Este es un ejemplo\",\
LANGUAGE=\"es\""
.to_string(), .to_string(),
ExtXSessionData::with_language( ExtXSessionData::with_language(
"com.example.title", "com.example.title",
@ -354,17 +364,27 @@ mod test {
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\"".to_string(), "#EXT-X-SESSION-DATA:\
DATA-ID=\"foo\",\
VALUE=\"bar\""
.to_string(),
ExtXSessionData::new("foo", SessionData::Value("bar".into())).to_string() ExtXSessionData::new("foo", SessionData::Value("bar".into())).to_string()
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\"".to_string(), "#EXT-X-SESSION-DATA:\
DATA-ID=\"foo\",\
URI=\"bar\""
.to_string(),
ExtXSessionData::new("foo", SessionData::Uri("bar".into())).to_string() ExtXSessionData::new("foo", SessionData::Uri("bar".into())).to_string()
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"".to_string(), "#EXT-X-SESSION-DATA:\
DATA-ID=\"foo\",\
VALUE=\"bar\",\
LANGUAGE=\"baz\""
.to_string(),
ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz")
.to_string() .to_string()
); );
@ -373,7 +393,9 @@ mod test {
#[test] #[test]
fn test_parser() { fn test_parser() {
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"" "#EXT-X-SESSION-DATA:\
DATA-ID=\"com.example.lyrics\",\
URI=\"lyrics.json\""
.parse::<ExtXSessionData>() .parse::<ExtXSessionData>()
.unwrap(), .unwrap(),
ExtXSessionData::new( ExtXSessionData::new(
@ -383,8 +405,10 @@ mod test {
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ "#EXT-X-SESSION-DATA:\
LANGUAGE=\"en\", VALUE=\"This is an example\"" DATA-ID=\"com.example.title\",\
LANGUAGE=\"en\",\
VALUE=\"This is an example\""
.parse::<ExtXSessionData>() .parse::<ExtXSessionData>()
.unwrap(), .unwrap(),
ExtXSessionData::with_language( ExtXSessionData::with_language(
@ -395,8 +419,10 @@ mod test {
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\ "#EXT-X-SESSION-DATA:\
LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\"" DATA-ID=\"com.example.title\",\
LANGUAGE=\"es\",\
VALUE=\"Este es un ejemplo\""
.parse::<ExtXSessionData>() .parse::<ExtXSessionData>()
.unwrap(), .unwrap(),
ExtXSessionData::with_language( ExtXSessionData::with_language(
@ -407,25 +433,47 @@ mod test {
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\"" "#EXT-X-SESSION-DATA:\
DATA-ID=\"foo\",\
VALUE=\"bar\""
.parse::<ExtXSessionData>() .parse::<ExtXSessionData>()
.unwrap(), .unwrap(),
ExtXSessionData::new("foo", SessionData::Value("bar".into())) ExtXSessionData::new("foo", SessionData::Value("bar".into()))
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\"" "#EXT-X-SESSION-DATA:\
DATA-ID=\"foo\",\
URI=\"bar\""
.parse::<ExtXSessionData>() .parse::<ExtXSessionData>()
.unwrap(), .unwrap(),
ExtXSessionData::new("foo", SessionData::Uri("bar".into())) ExtXSessionData::new("foo", SessionData::Uri("bar".into()))
); );
assert_eq!( assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"" "#EXT-X-SESSION-DATA:\
DATA-ID=\"foo\",\
VALUE=\"bar\",\
LANGUAGE=\"baz\",\
UNKNOWN=TAG"
.parse::<ExtXSessionData>() .parse::<ExtXSessionData>()
.unwrap(), .unwrap(),
ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz") ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz")
); );
assert!("#EXT-X-SESSION-DATA:\
DATA-ID=\"foo\",\
LANGUAGE=\"baz\""
.parse::<ExtXSessionData>()
.is_err());
assert!("#EXT-X-SESSION-DATA:\
DATA-ID=\"foo\",\
LANGUAGE=\"baz\",\
VALUE=\"VALUE\",\
URI=\"https://www.example.com/\""
.parse::<ExtXSessionData>()
.is_err());
} }
#[test] #[test]

View file

@ -100,9 +100,9 @@ mod test {
EncryptionMethod::Aes128, EncryptionMethod::Aes128,
"https://www.example.com/hls-key/key.bin", "https://www.example.com/hls-key/key.bin",
); );
key.set_iv([ key.set_iv(Some([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]); ]));
assert_eq!( assert_eq!(
key.to_string(), key.to_string(),
@ -129,9 +129,9 @@ mod test {
EncryptionMethod::Aes128, EncryptionMethod::Aes128,
"https://www.example.com/hls-key/key.bin", "https://www.example.com/hls-key/key.bin",
); );
key.set_iv([ key.set_iv(Some([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]); ]));
assert_eq!( assert_eq!(
"#EXT-X-SESSION-KEY:METHOD=AES-128,\ "#EXT-X-SESSION-KEY:METHOD=AES-128,\
@ -164,4 +164,36 @@ mod test {
ProtocolVersion::V1 ProtocolVersion::V1
); );
} }
#[test]
#[should_panic]
// ExtXSessionKey::new should panic, if the provided
// EncryptionMethod is None!
fn test_new_panic() {
ExtXSessionKey::new(EncryptionMethod::None, "");
}
#[test]
#[should_panic]
fn test_display_err() {
ExtXSessionKey(DecryptionKey::new(EncryptionMethod::None, "")).to_string();
}
#[test]
fn test_deref() {
let key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
assert_eq!(key.method(), EncryptionMethod::Aes128);
assert_eq!(key.uri(), &Some("https://www.example.com/".into()));
}
#[test]
fn test_deref_mut() {
let mut key = ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
key.set_method(EncryptionMethod::None);
assert_eq!(key.method(), EncryptionMethod::None);
key.set_uri(Some("https://www.github.com/"));
assert_eq!(key.uri(), &Some("https://www.github.com/".into()));
}
} }

View file

@ -12,7 +12,7 @@ use crate::Error;
/// [4.3.4.2. EXT-X-STREAM-INF] /// [4.3.4.2. EXT-X-STREAM-INF]
/// ///
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(PartialOrd, Debug, Clone, PartialEq, Eq)]
pub struct ExtXStreamInf { pub struct ExtXStreamInf {
uri: String, uri: String,
frame_rate: Option<DecimalFloatingPoint>, frame_rate: Option<DecimalFloatingPoint>,
@ -27,14 +27,13 @@ impl ExtXStreamInf {
/// Creates a new [ExtXStreamInf] tag. /// Creates a new [ExtXStreamInf] tag.
/// ///
/// # Examples /// # Example
/// ``` /// ```
/// # use hls_m3u8::tags::ExtXStreamInf; /// # use hls_m3u8::tags::ExtXStreamInf;
/// #
/// let stream = ExtXStreamInf::new("https://www.example.com/", 20); /// let stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// ``` /// ```
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self { pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
ExtXStreamInf { Self {
uri: uri.to_string(), uri: uri.to_string(),
frame_rate: None, frame_rate: None,
audio: None, audio: None,
@ -44,41 +43,160 @@ impl ExtXStreamInf {
} }
} }
/// Returns the `URI` that identifies the associated media playlist.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let stream = ExtXStreamInf::new("https://www.example.com/", 20);
///
/// assert_eq!(stream.uri(), &"https://www.example.com/".to_string());
/// ```
pub const fn uri(&self) -> &String {
&self.uri
}
/// Sets the `URI` that identifies the associated media playlist. /// Sets the `URI` that identifies the associated media playlist.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
///
/// stream.set_uri("https://www.google.com/");
/// assert_eq!(stream.uri(), &"https://www.google.com/".to_string());
/// ```
pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self { pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self {
self.uri = value.to_string(); self.uri = value.to_string();
self self
} }
/// Returns the `URI` that identifies the associated media playlist.
pub const fn uri(&self) -> &String {
&self.uri
}
/// Sets the maximum frame rate for all the video in the variant stream. /// Sets the maximum frame rate for all the video in the variant stream.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// # assert_eq!(stream.frame_rate(), None);
///
/// stream.set_frame_rate(Some(59.9));
/// assert_eq!(stream.frame_rate(), Some(59.9));
/// ```
pub fn set_frame_rate(&mut self, value: Option<f64>) -> &mut Self { pub fn set_frame_rate(&mut self, value: Option<f64>) -> &mut Self {
self.frame_rate = value.map(|v| v.into()); self.frame_rate = value.map(|v| v.into());
self self
} }
/// Returns the maximum frame rate for all the video in the variant stream. /// Returns the maximum frame rate for all the video in the variant stream.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// # assert_eq!(stream.frame_rate(), None);
///
/// stream.set_frame_rate(Some(59.9));
/// assert_eq!(stream.frame_rate(), Some(59.9));
/// ```
pub fn frame_rate(&self) -> Option<f64> { pub fn frame_rate(&self) -> Option<f64> {
self.frame_rate.map_or(None, |v| Some(v.as_f64())) self.frame_rate.map(|v| v.as_f64())
} }
/// Returns the group identifier for the audio in the variant stream. /// Returns the group identifier for the audio in the variant stream.
pub fn audio(&self) -> Option<&String> { ///
self.audio.as_ref() /// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// # assert_eq!(stream.audio(), &None);
///
/// stream.set_audio(Some("audio"));
/// assert_eq!(stream.audio(), &Some("audio".to_string()));
/// ```
pub const fn audio(&self) -> &Option<String> {
&self.audio
}
/// Sets the group identifier for the audio in the variant stream.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// # assert_eq!(stream.audio(), &None);
///
/// stream.set_audio(Some("audio"));
/// assert_eq!(stream.audio(), &Some("audio".to_string()));
/// ```
pub fn set_audio<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
self.audio = value.map(|v| v.into());
self
} }
/// Returns the group identifier for the subtitles in the variant stream. /// Returns the group identifier for the subtitles in the variant stream.
pub fn subtitles(&self) -> Option<&String> { ///
self.subtitles.as_ref() /// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// # assert_eq!(stream.subtitles(), &None);
///
/// stream.set_subtitles(Some("subs"));
/// assert_eq!(stream.subtitles(), &Some("subs".to_string()));
/// ```
pub const fn subtitles(&self) -> &Option<String> {
&self.subtitles
} }
/// Returns the value of `CLOSED-CAPTIONS` attribute. /// Sets the group identifier for the subtitles in the variant stream.
pub fn closed_captions(&self) -> Option<&ClosedCaptions> { ///
self.closed_captions.as_ref() /// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// # assert_eq!(stream.subtitles(), &None);
///
/// stream.set_subtitles(Some("subs"));
/// assert_eq!(stream.subtitles(), &Some("subs".to_string()));
/// ```
pub fn set_subtitles<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
self.subtitles = value.map(|v| v.into());
self
}
/// Returns the value of [ClosedCaptions] attribute.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// use hls_m3u8::types::ClosedCaptions;
///
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// # assert_eq!(stream.closed_captions(), &None);
///
/// stream.set_closed_captions(Some(ClosedCaptions::None));
/// assert_eq!(stream.closed_captions(), &Some(ClosedCaptions::None));
/// ```
pub const fn closed_captions(&self) -> &Option<ClosedCaptions> {
&self.closed_captions
}
/// Returns the value of [ClosedCaptions] attribute.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// use hls_m3u8::types::ClosedCaptions;
///
/// let mut stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// # assert_eq!(stream.closed_captions(), &None);
///
/// stream.set_closed_captions(Some(ClosedCaptions::None));
/// assert_eq!(stream.closed_captions(), &Some(ClosedCaptions::None));
/// ```
pub fn set_closed_captions(&mut self, value: Option<ClosedCaptions>) -> &mut Self {
self.closed_captions = value;
self
} }
} }
@ -113,8 +231,10 @@ impl FromStr for ExtXStreamInf {
fn from_str(input: &str) -> Result<Self, Self::Err> { fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut lines = input.lines(); let mut lines = input.lines();
let first_line = lines.next().ok_or(Error::missing_value("first_line"))?; let first_line = lines
let uri = lines.next().ok_or(Error::missing_value("URI"))?; .next()
.ok_or_else(|| Error::missing_value("first_line"))?;
let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?;
let input = tag(first_line, Self::PREFIX)?; let input = tag(first_line, Self::PREFIX)?;
@ -128,7 +248,7 @@ impl FromStr for ExtXStreamInf {
"FRAME-RATE" => frame_rate = Some((value.parse())?), "FRAME-RATE" => frame_rate = Some((value.parse())?),
"AUDIO" => audio = Some(unquote(value)), "AUDIO" => audio = Some(unquote(value)),
"SUBTITLES" => subtitles = Some(unquote(value)), "SUBTITLES" => subtitles = Some(unquote(value)),
"CLOSED-CAPTIONS" => closed_captions = Some((value.parse())?), "CLOSED-CAPTIONS" => closed_captions = Some(value.parse()?),
_ => {} _ => {}
} }
} }
@ -174,6 +294,14 @@ mod test {
); );
} }
#[test]
fn test_display() {
assert_eq!(
ExtXStreamInf::new("http://www.example.com/", 1000).to_string(),
"#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string()
);
}
#[test] #[test]
fn test_required_version() { fn test_required_version() {
assert_eq!( assert_eq!(
@ -183,10 +311,20 @@ mod test {
} }
#[test] #[test]
fn test_display() { fn test_deref() {
assert_eq!( assert_eq!(
ExtXStreamInf::new("http://www.example.com/", 1000).to_string(), ExtXStreamInf::new("http://www.example.com", 1000).bandwidth(),
"#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string() 1000
);
}
#[test]
fn test_deref_mut() {
assert_eq!(
ExtXStreamInf::new("http://www.example.com", 1000)
.set_bandwidth(1)
.bandwidth(),
1
); );
} }
} }

View file

@ -47,7 +47,7 @@ impl ExtXDiscontinuitySequence {
/// ///
/// assert_eq!(discontinuity_sequence.seq_num(), 5); /// assert_eq!(discontinuity_sequence.seq_num(), 5);
/// ``` /// ```
pub const fn seq_num(&self) -> u64 { pub const fn seq_num(self) -> u64 {
self.0 self.0
} }
@ -115,4 +115,12 @@ mod test {
"#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap() "#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap()
); );
} }
#[test]
fn test_seq_num() {
let mut sequence = ExtXDiscontinuitySequence::new(123);
assert_eq!(sequence.seq_num(), 123);
sequence.set_seq_num(1);
assert_eq!(sequence.seq_num(), 1);
}
} }

View file

@ -45,7 +45,7 @@ impl ExtXMediaSequence {
/// ///
/// assert_eq!(media_sequence.seq_num(), 5); /// assert_eq!(media_sequence.seq_num(), 5);
/// ``` /// ```
pub const fn seq_num(&self) -> u64 { pub const fn seq_num(self) -> u64 {
self.0 self.0
} }
@ -113,4 +113,12 @@ mod test {
"#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap() "#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap()
); );
} }
#[test]
fn test_seq_num() {
let mut sequence = ExtXMediaSequence::new(123);
assert_eq!(sequence.seq_num(), 123);
sequence.set_seq_num(1);
assert_eq!(sequence.seq_num(), 1);
}
} }

View file

@ -98,11 +98,11 @@ impl FromStr for ExtXByteRange {
let length = tokens[0].parse()?; let length = tokens[0].parse()?;
let start = { let start = {
let mut result = None;
if tokens.len() == 2 { if tokens.len() == 2 {
result = Some(tokens[1].parse()?); Some(tokens[1].parse()?)
} else {
None
} }
result
}; };
Ok(ExtXByteRange::new(length, start)) Ok(ExtXByteRange::new(length, start))

View file

@ -6,7 +6,7 @@ use std::time::Duration;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use crate::attribute::AttributePairs; use crate::attribute::AttributePairs;
use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion}; use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::{quote, tag, unquote}; use crate::utils::{quote, tag, unquote};
use crate::Error; use crate::Error;
@ -63,6 +63,36 @@ pub struct ExtXDateRange {
impl ExtXDateRange { impl ExtXDateRange {
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:"; pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
/// Makes a new [ExtXDateRange] tag.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXDateRange;
/// use chrono::{DateTime, FixedOffset};
/// use chrono::offset::TimeZone;
///
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
///
/// ExtXDateRange::new("id", FixedOffset::east(8 * HOURS_IN_SECS)
/// .ymd(2010, 2, 19)
/// .and_hms_milli(14, 54, 23, 31));
/// ```
pub fn new<T: ToString>(id: T, start_date: DateTime<FixedOffset>) -> Self {
Self {
id: id.to_string(),
class: None,
start_date,
end_date: None,
duration: None,
planned_duration: None,
scte35_cmd: None,
scte35_out: None,
scte35_in: None,
end_on_next: false,
client_attributes: BTreeMap::new(),
}
}
} }
impl RequiredVersion for ExtXDateRange { impl RequiredVersion for ExtXDateRange {
@ -82,15 +112,11 @@ impl fmt::Display for ExtXDateRange {
if let Some(value) = &self.end_date { if let Some(value) = &self.end_date {
write!(f, ",END-DATE={}", quote(value))?; write!(f, ",END-DATE={}", quote(value))?;
} }
if let Some(x) = self.duration { if let Some(value) = &self.duration {
write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?; write!(f, ",DURATION={}", value.as_secs_f64())?;
} }
if let Some(x) = self.planned_duration { if let Some(value) = &self.planned_duration {
write!( write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?;
f,
",PLANNED-DURATION={}",
DecimalFloatingPoint::from_duration(x)
)?;
} }
if let Some(value) = &self.scte35_cmd { if let Some(value) = &self.scte35_cmd {
write!(f, ",SCTE35-CMD={}", quote(value))?; write!(f, ",SCTE35-CMD={}", quote(value))?;
@ -137,12 +163,10 @@ impl FromStr for ExtXDateRange {
"START-DATE" => start_date = Some(unquote(value)), "START-DATE" => start_date = Some(unquote(value)),
"END-DATE" => end_date = Some(unquote(value).parse()?), "END-DATE" => end_date = Some(unquote(value).parse()?),
"DURATION" => { "DURATION" => {
let seconds: DecimalFloatingPoint = (value.parse())?; duration = Some(Duration::from_secs_f64(value.parse()?));
duration = Some(seconds.to_duration());
} }
"PLANNED-DURATION" => { "PLANNED-DURATION" => {
let seconds: DecimalFloatingPoint = (value.parse())?; planned_duration = Some(Duration::from_secs_f64(value.parse()?));
planned_duration = Some(seconds.to_duration());
} }
"SCTE35-CMD" => scte35_cmd = Some(unquote(value)), "SCTE35-CMD" => scte35_cmd = Some(unquote(value)),
"SCTE35-OUT" => scte35_out = Some(unquote(value)), "SCTE35-OUT" => scte35_out = Some(unquote(value)),
@ -164,17 +188,15 @@ impl FromStr for ExtXDateRange {
} }
} }
let id = id.ok_or(Error::missing_value("EXT-X-ID"))?; let id = id.ok_or_else(|| Error::missing_value("ID"))?;
let start_date = start_date let start_date = start_date
.ok_or(Error::missing_value("EXT-X-START-DATE"))? .ok_or_else(|| Error::missing_value("START-DATE"))?
.parse()?; .parse()?;
if end_on_next { if end_on_next && class.is_none() {
if class.is_none() {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
} Ok(Self {
Ok(ExtXDateRange {
id, id,
class, class,
start_date, start_date,
@ -193,7 +215,21 @@ impl FromStr for ExtXDateRange {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use chrono::offset::TimeZone;
#[test] // TODO; write some tests const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
fn it_works() {}
#[test]
fn test_required_version() {
assert_eq!(
ExtXDateRange::new(
"id",
FixedOffset::east(8 * HOURS_IN_SECS)
.ymd(2010, 2, 19)
.and_hms_milli(14, 54, 23, 31)
)
.required_version(),
ProtocolVersion::V1
);
}
} }

View file

@ -2,7 +2,7 @@ use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion}; use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
@ -39,7 +39,7 @@ impl ExtInf {
/// let ext_inf = ExtInf::new(Duration::from_secs(5)); /// let ext_inf = ExtInf::new(Duration::from_secs(5));
/// ``` /// ```
pub const fn new(duration: Duration) -> Self { pub const fn new(duration: Duration) -> Self {
ExtInf { Self {
duration, duration,
title: None, title: None,
} }
@ -55,7 +55,7 @@ impl ExtInf {
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title"); /// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
/// ``` /// ```
pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self { pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self {
ExtInf { Self {
duration, duration,
title: Some(title.to_string()), title: Some(title.to_string()),
} }
@ -170,17 +170,16 @@ impl FromStr for ExtInf {
fn from_str(input: &str) -> Result<Self, Self::Err> { fn from_str(input: &str) -> Result<Self, Self::Err> {
let input = tag(input, Self::PREFIX)?; let input = tag(input, Self::PREFIX)?;
dbg!(&input);
let tokens = input.splitn(2, ',').collect::<Vec<_>>(); let tokens = input.splitn(2, ',').collect::<Vec<_>>();
if tokens.len() == 0 { if tokens.is_empty() {
return Err(Error::custom(format!( return Err(Error::custom(format!(
"failed to parse #EXTINF tag, couldn't split input: {:?}", "failed to parse #EXTINF tag, couldn't split input: {:?}",
input input
))); )));
} }
let duration = tokens[0].parse::<DecimalFloatingPoint>()?.to_duration(); let duration = Duration::from_secs_f64(tokens[0].parse()?);
let title = { let title = {
if tokens.len() >= 2 { if tokens.len() >= 2 {
@ -194,7 +193,7 @@ impl FromStr for ExtInf {
} }
}; };
Ok(ExtInf { duration, title }) Ok(Self { duration, title })
} }
} }

View file

@ -2,7 +2,7 @@ use std::fmt;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::str::FromStr; use std::str::FromStr;
use crate::types::{DecryptionKey, EncryptionMethod, KeyFormatVersions}; use crate::types::{DecryptionKey, EncryptionMethod};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
@ -64,13 +64,13 @@ impl ExtXKey {
/// "#EXT-X-KEY:METHOD=NONE" /// "#EXT-X-KEY:METHOD=NONE"
/// ); /// );
/// ``` /// ```
pub fn empty() -> Self { pub const fn empty() -> Self {
Self(DecryptionKey { Self(DecryptionKey {
method: EncryptionMethod::None, method: EncryptionMethod::None,
uri: None, uri: None,
iv: None, iv: None,
key_format: None, key_format: None,
key_format_versions: KeyFormatVersions::new(), key_format_versions: None,
}) })
} }
@ -134,13 +134,13 @@ mod test {
); );
let mut key = ExtXKey::empty(); let mut key = ExtXKey::empty();
// it is expected, that all attributes will be ignored in an empty key! // it is expected, that all attributes will be ignored for an empty key!
key.set_key_format(Some(KeyFormat::Identity)); key.set_key_format(Some(KeyFormat::Identity));
key.set_iv([ key.set_iv(Some([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]); ]));
key.set_uri(Some("https://www.example.com")); key.set_uri(Some("https://www.example.com"));
key.set_key_format_versions(vec![1, 2, 3]); key.set_key_format_versions(Some(vec![1, 2, 3]));
assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string());
} }
@ -163,8 +163,8 @@ mod test {
EncryptionMethod::Aes128, EncryptionMethod::Aes128,
"https://www.example.com/hls-key/key.bin", "https://www.example.com/hls-key/key.bin",
); );
key.set_iv([ key.set_iv(Some([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]); ]));
} }
} }

View file

@ -93,7 +93,7 @@ impl FromStr for ExtXMap {
} }
} }
let uri = uri.ok_or(Error::missing_value("EXT-X-URI"))?; let uri = uri.ok_or_else(|| Error::missing_value("EXT-X-URI"))?;
Ok(ExtXMap { uri, range }) Ok(ExtXMap { uri, range })
} }
} }

View file

@ -8,7 +8,7 @@ use crate::Error;
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS] /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]
/// ///
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1 /// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct ExtXIndependentSegments; pub struct ExtXIndependentSegments;
impl ExtXIndependentSegments { impl ExtXIndependentSegments {

View file

@ -9,7 +9,7 @@ use crate::Error;
/// [4.3.5.2. EXT-X-START] /// [4.3.5.2. EXT-X-START]
/// ///
/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2 /// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(PartialOrd, Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExtXStart { pub struct ExtXStart {
time_offset: SignedDecimalFloatingPoint, time_offset: SignedDecimalFloatingPoint,
precise: bool, precise: bool,
@ -18,46 +18,99 @@ pub struct ExtXStart {
impl ExtXStart { impl ExtXStart {
pub(crate) const PREFIX: &'static str = "#EXT-X-START:"; pub(crate) const PREFIX: &'static str = "#EXT-X-START:";
/// Makes a new `ExtXStart` tag. /// Makes a new [ExtXStart] tag.
/// ///
/// # Panic /// # Panic
/// Panics if the time_offset value is infinite. /// Panics if the time_offset value is infinite.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStart;
/// ExtXStart::new(20.123456);
/// ```
pub fn new(time_offset: f64) -> Self { pub fn new(time_offset: f64) -> Self {
if time_offset.is_infinite() { Self {
panic!("EXT-X-START: Floating point value must be finite!"); time_offset: SignedDecimalFloatingPoint::new(time_offset),
}
ExtXStart {
time_offset: SignedDecimalFloatingPoint::new(time_offset).unwrap(),
precise: false, precise: false,
} }
} }
/// Makes a new `ExtXStart` tag with the given `precise` flag. /// Makes a new [ExtXStart] tag with the given `precise` flag.
/// ///
/// # Panic /// # Panic
/// Panics if the time_offset value is infinite. /// Panics if the time_offset value is infinite.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStart;
/// let start = ExtXStart::with_precise(20.123456, true);
/// assert_eq!(start.precise(), true);
/// ```
pub fn with_precise(time_offset: f64, precise: bool) -> Self { pub fn with_precise(time_offset: f64, precise: bool) -> Self {
if time_offset.is_infinite() { Self {
panic!("EXT-X-START: Floating point value must be finite!"); time_offset: SignedDecimalFloatingPoint::new(time_offset),
}
ExtXStart {
time_offset: SignedDecimalFloatingPoint::new(time_offset).unwrap(),
precise, precise,
} }
} }
/// Returns the time offset of the media segments in the playlist. /// Returns the time offset of the media segments in the playlist.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStart;
/// let start = ExtXStart::new(20.123456);
/// assert_eq!(start.time_offset(), 20.123456);
/// ```
pub const fn time_offset(&self) -> f64 { pub const fn time_offset(&self) -> f64 {
self.time_offset.as_f64() self.time_offset.as_f64()
} }
/// Sets the time offset of the media segments in the playlist.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStart;
/// let mut start = ExtXStart::new(20.123456);
/// # assert_eq!(start.time_offset(), 20.123456);
///
/// start.set_time_offset(1.0);
///
/// assert_eq!(start.time_offset(), 1.0);
/// ```
pub fn set_time_offset(&mut self, value: f64) -> &mut Self {
self.time_offset = SignedDecimalFloatingPoint::new(value);
self
}
/// Returns whether clients should not render media stream whose presentation times are /// Returns whether clients should not render media stream whose presentation times are
/// prior to the specified time offset. /// prior to the specified time offset.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStart;
/// let start = ExtXStart::with_precise(20.123456, true);
/// assert_eq!(start.precise(), true);
/// ```
pub const fn precise(&self) -> bool { pub const fn precise(&self) -> bool {
self.precise self.precise
} }
/// Sets the `precise` flag.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXStart;
/// let mut start = ExtXStart::new(20.123456);
/// # assert_eq!(start.precise(), false);
///
/// start.set_precise(true);
///
/// assert_eq!(start.precise(), true);
/// ```
pub fn set_precise(&mut self, value: bool) -> &mut Self {
self.precise = value;
self
}
} }
impl RequiredVersion for ExtXStart { impl RequiredVersion for ExtXStart {
@ -97,9 +150,9 @@ impl FromStr for ExtXStart {
} }
} }
let time_offset = time_offset.ok_or(Error::missing_value("EXT-X-TIME-OFFSET"))?; let time_offset = time_offset.ok_or_else(|| Error::missing_value("EXT-X-TIME-OFFSET"))?;
Ok(ExtXStart { Ok(Self {
time_offset, time_offset,
precise, precise,
}) })
@ -147,5 +200,12 @@ mod test {
ExtXStart::with_precise(1.23, true), ExtXStart::with_precise(1.23, true),
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(), "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(),
); );
assert_eq!(
ExtXStart::with_precise(1.23, true),
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG"
.parse()
.unwrap(),
);
} }
} }

View file

@ -16,11 +16,18 @@ pub struct ByteRange {
impl ByteRange { impl ByteRange {
/// Creates a new [ByteRange]. /// Creates a new [ByteRange].
///
/// # Example
/// ```
/// # use hls_m3u8::types::ByteRange;
/// ByteRange::new(22, Some(12));
/// ```
pub const fn new(length: usize, start: Option<usize>) -> Self { pub const fn new(length: usize, start: Option<usize>) -> Self {
Self { length, start } Self { length, start }
} }
/// Returns the length of the range. /// Returns the length of the range.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::ByteRange; /// # use hls_m3u8::types::ByteRange;
@ -32,6 +39,7 @@ impl ByteRange {
} }
/// Sets the length of the range. /// Sets the length of the range.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::ByteRange; /// # use hls_m3u8::types::ByteRange;
@ -48,6 +56,7 @@ impl ByteRange {
} }
/// Returns the start of the range. /// Returns the start of the range.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::ByteRange; /// # use hls_m3u8::types::ByteRange;
@ -59,6 +68,7 @@ impl ByteRange {
} }
/// Sets the start of the range. /// Sets the start of the range.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::ByteRange; /// # use hls_m3u8::types::ByteRange;
@ -97,13 +107,13 @@ impl FromStr for ByteRange {
let length = tokens[0].parse()?; let length = tokens[0].parse()?;
let start = { let start = {
let mut result = None;
if tokens.len() == 2 { if tokens.len() == 2 {
result = Some(tokens[1].parse()?); Some(tokens[1].parse()?)
} else {
None
} }
result
}; };
Ok(ByteRange::new(length, start)) Ok(Self::new(length, start))
} }
} }
@ -134,22 +144,30 @@ mod tests {
#[test] #[test]
fn test_parser() { fn test_parser() {
let byte_range = ByteRange { assert_eq!(
ByteRange {
length: 99999, length: 99999,
start: Some(2), start: Some(2),
}; },
assert_eq!(byte_range, "99999@2".parse::<ByteRange>().unwrap()); "99999@2".parse::<ByteRange>().unwrap()
);
let byte_range = ByteRange { assert_eq!(
ByteRange {
length: 99999, length: 99999,
start: Some(2), start: Some(2),
}; },
assert_eq!(byte_range, "99999@2".parse::<ByteRange>().unwrap()); "99999@2".parse::<ByteRange>().unwrap()
);
let byte_range = ByteRange { assert_eq!(
ByteRange {
length: 99999, length: 99999,
start: None, start: None,
}; },
assert_eq!(byte_range, "99999".parse::<ByteRange>().unwrap()); "99999".parse::<ByteRange>().unwrap()
);
assert!("".parse::<ByteRange>().is_err());
} }
} }

View file

@ -2,7 +2,7 @@ use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use crate::utils::{quote, unquote}; use crate::utils::{quote, unquote};
use crate::{Error, Result}; use crate::Error;
/// The identifier of a closed captions group or its absence. /// The identifier of a closed captions group or its absence.
/// ///
@ -10,7 +10,7 @@ use crate::{Error, Result};
/// ///
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum ClosedCaptions { pub enum ClosedCaptions {
GroupId(String), GroupId(String),
None, None,
@ -19,19 +19,20 @@ pub enum ClosedCaptions {
impl fmt::Display for ClosedCaptions { impl fmt::Display for ClosedCaptions {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self { match &self {
ClosedCaptions::GroupId(value) => write!(f, "{}", quote(value)), Self::GroupId(value) => write!(f, "{}", quote(value)),
ClosedCaptions::None => "NONE".fmt(f), Self::None => write!(f, "NONE"),
} }
} }
} }
impl FromStr for ClosedCaptions { impl FromStr for ClosedCaptions {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if s == "NONE" { fn from_str(input: &str) -> Result<Self, Self::Err> {
Ok(ClosedCaptions::None) if input.trim() == "NONE" {
Ok(Self::None)
} else { } else {
Ok(ClosedCaptions::GroupId(unquote(s))) Ok(Self::GroupId(unquote(input)))
} }
} }
} }
@ -42,21 +43,23 @@ mod tests {
#[test] #[test]
fn test_display() { fn test_display() {
let closed_captions = ClosedCaptions::None; assert_eq!(ClosedCaptions::None.to_string(), "NONE".to_string());
assert_eq!(closed_captions.to_string(), "NONE".to_string());
let closed_captions = ClosedCaptions::GroupId("value".into()); assert_eq!(
assert_eq!(closed_captions.to_string(), "\"value\"".to_string()); ClosedCaptions::GroupId("value".into()).to_string(),
"\"value\"".to_string()
);
} }
#[test] #[test]
fn test_parser() { fn test_parser() {
let closed_captions = ClosedCaptions::None;
assert_eq!(closed_captions, "NONE".parse::<ClosedCaptions>().unwrap());
let closed_captions = ClosedCaptions::GroupId("value".into());
assert_eq!( assert_eq!(
closed_captions, ClosedCaptions::None,
"NONE".parse::<ClosedCaptions>().unwrap()
);
assert_eq!(
ClosedCaptions::GroupId("value".into()),
"\"value\"".parse::<ClosedCaptions>().unwrap() "\"value\"".parse::<ClosedCaptions>().unwrap()
); );
} }

View file

@ -1,6 +1,7 @@
use std::fmt; use core::ops::Deref;
use std::str::FromStr; use core::str::FromStr;
use std::time::Duration;
use derive_more::Display;
use crate::Error; use crate::Error;
@ -8,77 +9,71 @@ use crate::Error;
/// ///
/// See: [4.2. Attribute Lists] /// See: [4.2. Attribute Lists]
/// ///
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 /// [4.2. Attribute Lists]:
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.2
#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display)]
pub(crate) struct DecimalFloatingPoint(f64); pub(crate) struct DecimalFloatingPoint(f64);
impl DecimalFloatingPoint { impl DecimalFloatingPoint {
/// Makes a new `DecimalFloatingPoint` instance. /// Makes a new [DecimalFloatingPoint] instance.
/// ///
/// # Errors /// # Errors
/// ///
/// The given value must have a positive sign and be finite, /// The given value must have a positive sign and be finite,
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. /// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
pub fn new(n: f64) -> crate::Result<Self> { pub fn new(value: f64) -> crate::Result<Self> {
if !n.is_sign_positive() || !n.is_finite() { if value.is_sign_negative() || value.is_infinite() {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
Ok(DecimalFloatingPoint(n)) Ok(Self(value))
} }
/// Converts `DecimalFloatingPoint` to `f64`. pub(crate) const fn from_f64_unchecked(value: f64) -> Self {
Self(value)
}
/// Converts [DecimalFloatingPoint] to [f64].
pub const fn as_f64(self) -> f64 { pub const fn as_f64(self) -> f64 {
self.0 self.0
} }
pub(crate) fn to_duration(self) -> Duration {
let secs = self.0 as u64;
let nanos = (self.0.fract() * 1_000_000_000.0) as u32;
Duration::new(secs, nanos)
}
pub(crate) fn from_duration(duration: Duration) -> Self {
let n =
(duration.as_secs() as f64) + (f64::from(duration.subsec_nanos()) / 1_000_000_000.0);
DecimalFloatingPoint(n)
}
}
impl From<u32> for DecimalFloatingPoint {
fn from(f: u32) -> Self {
DecimalFloatingPoint(f64::from(f))
}
} }
impl Eq for DecimalFloatingPoint {} impl Eq for DecimalFloatingPoint {}
impl fmt::Display for DecimalFloatingPoint { // this trait is implemented manually, so it doesn't construct a [DecimalFloatingPoint],
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // with a negative value.
self.0.fmt(f)
}
}
impl FromStr for DecimalFloatingPoint { impl FromStr for DecimalFloatingPoint {
type Err = Error; type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> { fn from_str(input: &str) -> Result<Self, Self::Err> {
if !input.chars().all(|c| c.is_digit(10) || c == '.') { Self::new(input.parse()?)
return Err(Error::invalid_input());
} }
let n = input.parse()?; }
DecimalFloatingPoint::new(n)
impl Deref for DecimalFloatingPoint {
type Target = f64;
fn deref(&self) -> &Self::Target {
&self.0
} }
} }
impl From<f64> for DecimalFloatingPoint { impl From<f64> for DecimalFloatingPoint {
fn from(value: f64) -> Self { fn from(value: f64) -> Self {
Self(value) let mut result = value;
// guard against the unlikely case of an infinite value...
if result.is_infinite() {
result = 0.0;
}
Self(result.abs())
} }
} }
impl From<f32> for DecimalFloatingPoint { impl From<f32> for DecimalFloatingPoint {
fn from(value: f32) -> Self { fn from(value: f32) -> Self {
Self(value.into()) (value as f64).into()
} }
} }
@ -86,6 +81,24 @@ impl From<f32> for DecimalFloatingPoint {
mod tests { mod tests {
use super::*; use super::*;
macro_rules! test_from {
( $($input:expr),* ) => {
use ::core::convert::From;
#[test]
fn test_from() {
$(
assert_eq!(
DecimalFloatingPoint::from($input),
DecimalFloatingPoint::new(1.0).unwrap(),
);
)*
}
}
}
test_from![1u8, 1u16, 1u32, 1.0f32, -1.0f32, 1.0f64, -1.0f64];
#[test] #[test]
pub fn test_display() { pub fn test_display() {
let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap(); let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap();
@ -108,5 +121,32 @@ mod tests {
decimal_floating_point, decimal_floating_point,
"4.1".parse::<DecimalFloatingPoint>().unwrap() "4.1".parse::<DecimalFloatingPoint>().unwrap()
); );
assert!("1#".parse::<DecimalFloatingPoint>().is_err());
assert!("-1.0".parse::<DecimalFloatingPoint>().is_err());
}
#[test]
fn test_new() {
assert!(DecimalFloatingPoint::new(::std::f64::INFINITY).is_err());
assert!(DecimalFloatingPoint::new(-1.0).is_err());
}
#[test]
fn test_as_f64() {
assert_eq!(DecimalFloatingPoint::new(1.0).unwrap().as_f64(), 1.0);
}
#[test]
fn test_from_inf() {
assert_eq!(
DecimalFloatingPoint::from(::std::f64::INFINITY),
DecimalFloatingPoint::new(0.0).unwrap()
);
}
#[test]
fn test_deref() {
assert_eq!(DecimalFloatingPoint::from(0.1).floor(), 0.0);
} }
} }

View file

@ -1,6 +1,7 @@
use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use derive_more::Display;
use crate::Error; use crate::Error;
/// Decimal resolution. /// Decimal resolution.
@ -8,7 +9,8 @@ use crate::Error;
/// See: [4.2. Attribute Lists] /// See: [4.2. Attribute Lists]
/// ///
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
#[display(fmt = "{}x{}", width, height)]
pub(crate) struct DecimalResolution { pub(crate) struct DecimalResolution {
width: usize, width: usize,
height: usize, height: usize,
@ -43,9 +45,10 @@ impl DecimalResolution {
} }
} }
impl fmt::Display for DecimalResolution { /// [DecimalResolution] can be constructed from a tuple; `(width, height)`.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { impl From<(usize, usize)> for DecimalResolution {
write!(f, "{}x{}", self.width, self.height) fn from(value: (usize, usize)) -> Self {
DecimalResolution::new(value.0, value.1)
} }
} }
@ -62,12 +65,9 @@ impl FromStr for DecimalResolution {
))); )));
} }
let width = tokens[0]; Ok(Self {
let height = tokens[1]; width: tokens[0].parse()?,
height: tokens[1].parse()?,
Ok(DecimalResolution {
width: width.parse()?,
height: height.parse()?,
}) })
} }
} }
@ -118,4 +118,12 @@ mod tests {
12 12
); );
} }
#[test]
fn test_from() {
assert_eq!(
DecimalResolution::from((1920, 1080)),
DecimalResolution::new(1920, 1080)
);
}
} }

View file

@ -12,7 +12,7 @@ use crate::utils::{quote, unquote};
use crate::Error; use crate::Error;
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)]
#[builder(setter(into))] #[builder(setter(into), build_fn(validate = "Self::validate"))]
/// [DecryptionKey] contains data, that is shared between [ExtXSessionKey] and [ExtXKey]. /// [DecryptionKey] contains data, that is shared between [ExtXSessionKey] and [ExtXKey].
/// ///
/// [ExtXSessionKey]: crate::tags::ExtXSessionKey /// [ExtXSessionKey]: crate::tags::ExtXSessionKey
@ -30,9 +30,18 @@ pub struct DecryptionKey {
/// A string that specifies how the key is /// A string that specifies how the key is
/// represented in the resource identified by the `URI`. /// represented in the resource identified by the `URI`.
pub(crate) key_format: Option<KeyFormat>, pub(crate) key_format: Option<KeyFormat>,
#[builder(setter(into), default)] #[builder(setter(into, strip_option), default)]
/// The `KEYFORMATVERSIONS` attribute. /// The [KeyFormatVersions] attribute.
pub(crate) key_format_versions: KeyFormatVersions, pub(crate) key_format_versions: Option<KeyFormatVersions>,
}
impl DecryptionKeyBuilder {
fn validate(&self) -> Result<(), String> {
if self.method != Some(EncryptionMethod::None) && self.uri.is_none() {
return Err(Error::custom("Missing URL").to_string());
}
Ok(())
}
} }
impl DecryptionKey { impl DecryptionKey {
@ -54,7 +63,7 @@ impl DecryptionKey {
uri: Some(uri.to_string()), uri: Some(uri.to_string()),
iv: None, iv: None,
key_format: None, key_format: None,
key_format_versions: KeyFormatVersions::new(), key_format_versions: None,
} }
} }
@ -103,13 +112,15 @@ impl DecryptionKey {
/// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string() /// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string()
/// ); /// );
/// ``` /// ```
pub fn set_method(&mut self, value: EncryptionMethod) { pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self {
self.method = value; self.method = value;
self
} }
/// Returns an `URI`, that specifies how to obtain the key. /// Returns an `URI`, that specifies how to obtain the key.
/// ///
/// This attribute is required, if the [EncryptionMethod] is not None. /// # Note
/// This attribute is required, if the [EncryptionMethod] is not `None`.
/// ///
/// # Example /// # Example
/// ``` /// ```
@ -152,14 +163,13 @@ impl DecryptionKey {
/// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string() /// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string()
/// ); /// );
/// ``` /// ```
pub fn set_uri<T: ToString>(&mut self, value: Option<T>) { pub fn set_uri<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
self.uri = value.map(|v| v.to_string()); self.uri = value.map(|v| v.to_string());
self
} }
/// Returns the IV (Initialization Vector) attribute. /// Returns the IV (Initialization Vector) attribute.
/// ///
/// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::DecryptionKey; /// # use hls_m3u8::types::DecryptionKey;
@ -170,9 +180,10 @@ impl DecryptionKey {
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
/// ///
/// key.set_iv([ /// # assert_eq!(key.iv(), None);
/// key.set_iv(Some([
/// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7
/// ]); /// ]));
/// ///
/// assert_eq!( /// assert_eq!(
/// key.iv(), /// key.iv(),
@ -189,8 +200,6 @@ impl DecryptionKey {
/// Sets the `IV` attribute. /// Sets the `IV` attribute.
/// ///
/// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::DecryptionKey; /// # use hls_m3u8::types::DecryptionKey;
@ -201,27 +210,26 @@ impl DecryptionKey {
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
/// ///
/// key.set_iv([ /// key.set_iv(Some([
/// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7 /// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7
/// ]); /// ]));
/// ///
/// assert_eq!( /// assert_eq!(
/// key.to_string(), /// key.to_string(),
/// "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string() /// "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string()
/// ); /// );
/// ``` /// ```
pub fn set_iv<T>(&mut self, value: T) pub fn set_iv<T>(&mut self, value: Option<T>) -> &mut Self
where where
T: Into<[u8; 16]>, T: Into<[u8; 16]>,
{ {
self.iv = Some(InitializationVector(value.into())); self.iv = value.map(|v| InitializationVector(v.into()));
self
} }
/// Returns a string that specifies how the key is /// Returns a string that specifies how the key is
/// represented in the resource identified by the `URI`. /// represented in the resource identified by the `URI`.
/// ///
/// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::DecryptionKey; /// # use hls_m3u8::types::DecryptionKey;
@ -243,9 +251,7 @@ impl DecryptionKey {
self.key_format self.key_format
} }
/// Sets the `KEYFORMAT` attribute. /// Sets the [KeyFormat] attribute.
///
/// This attribute is optional.
/// ///
/// # Example /// # Example
/// ``` /// ```
@ -264,14 +270,13 @@ impl DecryptionKey {
/// Some(KeyFormat::Identity) /// Some(KeyFormat::Identity)
/// ); /// );
/// ``` /// ```
pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) { pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) -> &mut Self {
self.key_format = value.map(|v| v.into()); self.key_format = value.map(|v| v.into());
self
} }
/// Returns the [KeyFormatVersions] attribute. /// Returns the [KeyFormatVersions] attribute.
/// ///
/// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::DecryptionKey; /// # use hls_m3u8::types::DecryptionKey;
@ -282,21 +287,19 @@ impl DecryptionKey {
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
/// ///
/// key.set_key_format_versions(vec![1, 2, 3, 4, 5]); /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5]));
/// ///
/// assert_eq!( /// assert_eq!(
/// key.key_format_versions(), /// key.key_format_versions(),
/// &KeyFormatVersions::from(vec![1, 2, 3, 4, 5]) /// &Some(KeyFormatVersions::from(vec![1, 2, 3, 4, 5]))
/// ); /// );
/// ``` /// ```
pub const fn key_format_versions(&self) -> &KeyFormatVersions { pub const fn key_format_versions(&self) -> &Option<KeyFormatVersions> {
&self.key_format_versions &self.key_format_versions
} }
/// Sets the [KeyFormatVersions] attribute. /// Sets the [KeyFormatVersions] attribute.
/// ///
/// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// # use hls_m3u8::types::DecryptionKey; /// # use hls_m3u8::types::DecryptionKey;
@ -307,21 +310,25 @@ impl DecryptionKey {
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
/// ///
/// key.set_key_format_versions(vec![1, 2, 3, 4, 5]); /// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5]));
/// ///
/// assert_eq!( /// assert_eq!(
/// key.to_string(), /// key.to_string(),
/// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string() /// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string()
/// ); /// );
/// ``` /// ```
pub fn set_key_format_versions<T: Into<KeyFormatVersions>>(&mut self, value: T) { pub fn set_key_format_versions<T: Into<KeyFormatVersions>>(
self.key_format_versions = value.into(); &mut self,
value: Option<T>,
) -> &mut Self {
self.key_format_versions = value.map(|v| v.into());
self
} }
} }
impl RequiredVersion for DecryptionKey { impl RequiredVersion for DecryptionKey {
fn required_version(&self) -> ProtocolVersion { fn required_version(&self) -> ProtocolVersion {
if self.key_format.is_some() || !self.key_format_versions.is_default() { if self.key_format.is_some() || self.key_format_versions.is_some() {
ProtocolVersion::V5 ProtocolVersion::V5
} else if self.iv.is_some() { } else if self.iv.is_some() {
ProtocolVersion::V2 ProtocolVersion::V2
@ -355,17 +362,17 @@ impl FromStr for DecryptionKey {
} }
} }
let method = method.ok_or(Error::missing_value("METHOD"))?; let method = method.ok_or_else(|| Error::missing_value("METHOD"))?;
if method != EncryptionMethod::None && uri.is_none() { if method != EncryptionMethod::None && uri.is_none() {
return Err(Error::missing_value("URI")); return Err(Error::missing_value("URI"));
} }
Ok(DecryptionKey { Ok(Self {
method, method,
uri, uri,
iv, iv,
key_format, key_format,
key_format_versions: key_format_versions.unwrap_or(KeyFormatVersions::new()), key_format_versions,
}) })
} }
} }
@ -386,8 +393,11 @@ impl fmt::Display for DecryptionKey {
if let Some(value) = &self.key_format { if let Some(value) = &self.key_format {
write!(f, ",KEYFORMAT={}", quote(value))?; write!(f, ",KEYFORMAT={}", quote(value))?;
} }
if !self.key_format_versions.is_default() {
write!(f, ",KEYFORMATVERSIONS={}", &self.key_format_versions)?; if let Some(key_format_versions) = &self.key_format_versions {
if !key_format_versions.is_default() {
write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?;
}
} }
Ok(()) Ok(())
} }
@ -410,6 +420,7 @@ mod test {
.key_format_versions(vec![1, 2, 3, 4, 5]) .key_format_versions(vec![1, 2, 3, 4, 5])
.build() .build()
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
key.to_string(), key.to_string(),
"METHOD=AES-128,\ "METHOD=AES-128,\
@ -419,7 +430,13 @@ mod test {
KEYFORMATVERSIONS=\"1/2/3/4/5\"\ KEYFORMATVERSIONS=\"1/2/3/4/5\"\
" "
.to_string() .to_string()
) );
assert!(DecryptionKey::builder().build().is_err());
assert!(DecryptionKey::builder()
.method(EncryptionMethod::Aes128)
.build()
.is_err());
} }
#[test] #[test]
@ -428,9 +445,9 @@ mod test {
EncryptionMethod::Aes128, EncryptionMethod::Aes128,
"https://www.example.com/hls-key/key.bin", "https://www.example.com/hls-key/key.bin",
); );
key.set_iv([ key.set_iv(Some([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]); ]));
assert_eq!( assert_eq!(
key.to_string(), key.to_string(),
@ -458,9 +475,9 @@ mod test {
EncryptionMethod::Aes128, EncryptionMethod::Aes128,
"https://www.example.com/hls-key/key.bin", "https://www.example.com/hls-key/key.bin",
); );
key.set_iv([ key.set_iv(Some([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]); ]));
assert_eq!( assert_eq!(
"METHOD=AES-128,\ "METHOD=AES-128,\
@ -472,9 +489,9 @@ mod test {
); );
let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"); let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com");
key.set_iv([ key.set_iv(Some([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]); ]));
key.set_key_format(Some(KeyFormat::Identity)); key.set_key_format(Some(KeyFormat::Identity));
assert_eq!( assert_eq!(
@ -485,7 +502,30 @@ mod test {
.parse::<DecryptionKey>() .parse::<DecryptionKey>()
.unwrap(), .unwrap(),
key key
) );
key.set_key_format_versions(Some(vec![1, 2, 3]));
assert_eq!(
"METHOD=AES-128,\
URI=\"http://www.example.com\",\
IV=0x10ef8f758ca555115584bb5b3c687f52,\
KEYFORMAT=\"identity\",\
KEYFORMATVERSIONS=\"1/2/3\""
.parse::<DecryptionKey>()
.unwrap(),
key
);
assert_eq!(
"METHOD=AES-128,\
URI=\"http://www.example.com\",\
UNKNOWNTAG=abcd"
.parse::<DecryptionKey>()
.unwrap(),
DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com")
);
assert!("METHOD=AES-128,URI=".parse::<DecryptionKey>().is_err());
assert!("garbage".parse::<DecryptionKey>().is_err());
} }
#[test] #[test]
@ -494,6 +534,29 @@ mod test {
DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/") DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/")
.required_version(), .required_version(),
ProtocolVersion::V1 ProtocolVersion::V1
) );
assert_eq!(
DecryptionKey::builder()
.method(EncryptionMethod::Aes128)
.uri("https://www.example.com/")
.key_format(KeyFormat::Identity)
.key_format_versions(vec![1, 2, 3])
.build()
.unwrap()
.required_version(),
ProtocolVersion::V5
);
assert_eq!(
DecryptionKey::builder()
.method(EncryptionMethod::Aes128)
.uri("https://www.example.com/")
.iv([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])
.build()
.unwrap()
.required_version(),
ProtocolVersion::V2
);
} }
} }

View file

@ -1,7 +1,4 @@
use std::fmt; use strum::{Display, EnumString};
use std::str::FromStr;
use crate::Error;
/// Encryption method. /// Encryption method.
/// ///
@ -9,7 +6,8 @@ use crate::Error;
/// ///
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4 /// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
pub enum EncryptionMethod { pub enum EncryptionMethod {
/// `None` means that [MediaSegment]s are not encrypted. /// `None` means that [MediaSegment]s are not encrypted.
/// ///
@ -26,6 +24,7 @@ pub enum EncryptionMethod {
/// [MediaSegment]: crate::MediaSegment /// [MediaSegment]: crate::MediaSegment
/// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf /// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
/// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652 /// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652
#[strum(serialize = "AES-128")]
Aes128, Aes128,
/// `SampleAes` means that the [MediaSegment]s /// `SampleAes` means that the [MediaSegment]s
/// contain media samples, such as audio or video, that are encrypted /// contain media samples, such as audio or video, that are encrypted
@ -48,32 +47,6 @@ pub enum EncryptionMethod {
SampleAes, SampleAes,
} }
impl fmt::Display for EncryptionMethod {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
EncryptionMethod::Aes128 => "AES-128".fmt(f),
EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
EncryptionMethod::None => "NONE".fmt(f),
}
}
}
impl FromStr for EncryptionMethod {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"AES-128" => Ok(EncryptionMethod::Aes128),
"SAMPLE-AES" => Ok(EncryptionMethod::SampleAes),
"NONE" => Ok(EncryptionMethod::None),
_ => Err(Error::custom(format!(
"Unknown encryption method: {:?}",
input
))),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -104,5 +77,7 @@ mod tests {
EncryptionMethod::None, EncryptionMethod::None,
"NONE".parse::<EncryptionMethod>().unwrap() "NONE".parse::<EncryptionMethod>().unwrap()
); );
assert!("unknown".parse::<EncryptionMethod>().is_err());
} }
} }

View file

@ -1,7 +1,4 @@
use std::fmt; use strum::{Display, EnumString};
use std::str::FromStr;
use crate::Error;
/// HDCP level. /// HDCP level.
/// ///
@ -9,33 +6,14 @@ use crate::Error;
/// ///
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
pub enum HdcpLevel { pub enum HdcpLevel {
#[strum(serialize = "TYPE-0")]
Type0, Type0,
None, None,
} }
impl fmt::Display for HdcpLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
HdcpLevel::Type0 => "TYPE-0".fmt(f),
HdcpLevel::None => "NONE".fmt(f),
}
}
}
impl FromStr for HdcpLevel {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"TYPE-0" => Ok(HdcpLevel::Type0),
"NONE" => Ok(HdcpLevel::None),
_ => Err(Error::custom(format!("Unknown HDCP level: {:?}", input))),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -56,5 +34,7 @@ mod tests {
let level = HdcpLevel::None; let level = HdcpLevel::None;
assert_eq!(level, "NONE".parse::<HdcpLevel>().unwrap()); assert_eq!(level, "NONE".parse::<HdcpLevel>().unwrap());
assert!("unk".parse::<HdcpLevel>().is_err());
} }
} }

View file

@ -1,7 +1,4 @@
use std::fmt; use strum::{Display, EnumString};
use std::str::FromStr;
use crate::Error;
/// Identifier of a rendition within the segments in a media playlist. /// Identifier of a rendition within the segments in a media playlist.
/// ///
@ -9,7 +6,8 @@ use crate::Error;
/// ///
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 /// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
#[strum(serialize_all = "UPPERCASE")]
pub enum InStreamId { pub enum InStreamId {
Cc1, Cc1,
Cc2, Cc2,
@ -80,17 +78,30 @@ pub enum InStreamId {
Service63, Service63,
} }
impl fmt::Display for InStreamId { #[cfg(test)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { mod tests {
format!("{:?}", self).to_uppercase().fmt(f) use super::*;
macro_rules! gen_tests {
( $($string:expr => $enum:expr),* ) => {
#[test]
fn test_display() {
$(
assert_eq!($enum.to_string(), $string.to_string());
)*
} }
}
impl FromStr for InStreamId { #[test]
type Err = Error; fn test_parser() {
$(
assert_eq!($enum, $string.parse::<InStreamId>().unwrap());
)*
assert!("invalid_input".parse::<InStreamId>().is_err());
}
};
}
fn from_str(input: &str) -> Result<Self, Self::Err> { gen_tests![
Ok(match input {
"CC1" => InStreamId::Cc1, "CC1" => InStreamId::Cc1,
"CC2" => InStreamId::Cc2, "CC2" => InStreamId::Cc2,
"CC3" => InStreamId::Cc3, "CC3" => InStreamId::Cc3,
@ -157,8 +168,6 @@ impl FromStr for InStreamId {
"SERVICE60" => InStreamId::Service60, "SERVICE60" => InStreamId::Service60,
"SERVICE61" => InStreamId::Service61, "SERVICE61" => InStreamId::Service61,
"SERVICE62" => InStreamId::Service62, "SERVICE62" => InStreamId::Service62,
"SERVICE63" => InStreamId::Service63, "SERVICE63" => InStreamId::Service63
_ => return Err(Error::custom(format!("Unknown instream id: {:?}", input))), ];
})
}
} }

View file

@ -13,7 +13,7 @@ use crate::Error;
pub struct InitializationVector(pub [u8; 16]); pub struct InitializationVector(pub [u8; 16]);
impl InitializationVector { impl InitializationVector {
/// Converts the initialization vector to a slice. /// Converts the [InitializationVector] to a slice.
pub const fn to_slice(&self) -> [u8; 16] { pub const fn to_slice(&self) -> [u8; 16] {
self.0 self.0
} }
@ -51,21 +51,106 @@ impl fmt::Display for InitializationVector {
impl FromStr for InitializationVector { impl FromStr for InitializationVector {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(input: &str) -> Result<Self, Self::Err> {
if !(s.starts_with("0x") || s.starts_with("0X")) { if !(input.starts_with("0x") || input.starts_with("0X")) {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
if s.len() - 2 != 32 { if input.len() - 2 != 32 {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
let mut v = [0; 16]; let mut result = [0; 16];
for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() { for (i, c) in input.as_bytes().chunks(2).skip(1).enumerate() {
let d = std::str::from_utf8(c).map_err(|e| Error::custom(e))?; let d = std::str::from_utf8(c).map_err(Error::custom)?;
let b = u8::from_str_radix(d, 16).map_err(|e| Error::custom(e))?; let b = u8::from_str_radix(d, 16).map_err(Error::custom)?;
v[i] = b; result[i] = b;
} }
Ok(InitializationVector(v)) Ok(Self(result))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display() {
assert_eq!(
"0x10ef8f758ca555115584bb5b3c687f52".to_string(),
InitializationVector([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
])
.to_string()
);
}
#[test]
fn test_parser() {
assert_eq!(
"0x10ef8f758ca555115584bb5b3c687f52"
.parse::<InitializationVector>()
.unwrap(),
InitializationVector([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
])
);
assert_eq!(
"0X10ef8f758ca555115584bb5b3c687f52"
.parse::<InitializationVector>()
.unwrap(),
InitializationVector([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
])
);
assert_eq!(
"0X10EF8F758CA555115584BB5B3C687F52"
.parse::<InitializationVector>()
.unwrap(),
InitializationVector([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
])
);
assert!("garbage".parse::<InitializationVector>().is_err());
assert!("0xgarbage".parse::<InitializationVector>().is_err());
assert!("0x12".parse::<InitializationVector>().is_err());
assert!("0X10EF8F758CA555115584BB5B3C687F5Z"
.parse::<InitializationVector>()
.is_err());
}
#[test]
fn test_as_ref() {
assert_eq!(
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).as_ref(),
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
);
}
#[test]
fn test_deref() {
assert_eq!(
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).deref(),
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
);
}
#[test]
fn test_from() {
assert_eq!(
InitializationVector::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
);
}
#[test]
fn test_to_slice() {
assert_eq!(
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).to_slice(),
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
);
} }
} }

View file

@ -54,6 +54,8 @@ mod tests {
assert_eq!(KeyFormat::Identity, quote("identity").parse().unwrap()); assert_eq!(KeyFormat::Identity, quote("identity").parse().unwrap());
assert_eq!(KeyFormat::Identity, "identity".parse().unwrap()); assert_eq!(KeyFormat::Identity, "identity".parse().unwrap());
assert!("garbage".parse::<KeyFormat>().is_err());
} }
#[test] #[test]

View file

@ -37,7 +37,7 @@ impl KeyFormatVersions {
/// Returns `true`, if [KeyFormatVersions] has the default value of `vec![1]`. /// Returns `true`, if [KeyFormatVersions] has the default value of `vec![1]`.
pub fn is_default(&self) -> bool { pub fn is_default(&self) -> bool {
self.0 == vec![1] || self.0.is_empty() self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty()
} }
} }
@ -66,7 +66,7 @@ impl FromStr for KeyFormatVersions {
fn from_str(input: &str) -> Result<Self, Self::Err> { fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut result = unquote(input) let mut result = unquote(input)
.split("/") .split('/')
.filter_map(|v| v.parse().ok()) .filter_map(|v| v.parse().ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -1,15 +1,9 @@
use std::fmt; use strum::{Display, EnumString};
use std::str::FromStr;
use crate::Error; /// Specifies the media type.
/// Media type.
///
/// See: [4.3.4.1. EXT-X-MEDIA]
///
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Ord, PartialOrd, Display, EnumString, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
pub enum MediaType { pub enum MediaType {
Audio, Audio,
Video, Video,
@ -17,29 +11,29 @@ pub enum MediaType {
ClosedCaptions, ClosedCaptions,
} }
impl fmt::Display for MediaType { #[cfg(test)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { mod tests {
match &self { use super::*;
MediaType::Audio => "AUDIO".fmt(f),
MediaType::Video => "VIDEO".fmt(f), #[test]
MediaType::Subtitles => "SUBTITLES".fmt(f), fn test_parser() {
MediaType::ClosedCaptions => "CLOSED-CAPTIONS".fmt(f), assert_eq!(MediaType::Audio, "AUDIO".parse().unwrap());
assert_eq!(MediaType::Video, "VIDEO".parse().unwrap());
assert_eq!(MediaType::Subtitles, "SUBTITLES".parse().unwrap());
assert_eq!(
MediaType::ClosedCaptions,
"CLOSED-CAPTIONS".parse().unwrap()
);
} }
}
} #[test]
fn test_display() {
impl FromStr for MediaType { assert_eq!(MediaType::Audio.to_string(), "AUDIO".to_string());
type Err = Error; assert_eq!(MediaType::Video.to_string(), "VIDEO".to_string());
assert_eq!(MediaType::Subtitles.to_string(), "SUBTITLES".to_string());
fn from_str(input: &str) -> Result<Self, Self::Err> { assert_eq!(
Ok(match input { MediaType::ClosedCaptions.to_string(),
"AUDIO" => MediaType::Audio, "CLOSED-CAPTIONS".to_string()
"VIDEO" => MediaType::Video, );
"SUBTITLES" => MediaType::Subtitles,
"CLOSED-CAPTIONS" => MediaType::ClosedCaptions,
_ => {
return Err(Error::invalid_input());
}
})
} }
} }

View file

@ -27,9 +27,12 @@ pub trait RequiredVersion {
fn required_version(&self) -> ProtocolVersion; fn required_version(&self) -> ProtocolVersion;
} }
/// [7. Protocol Version Compatibility] /// # [7. Protocol Version Compatibility]
/// The [ProtocolVersion] specifies, which m3u8 revision is required, to parse
/// a certain tag correctly.
/// ///
/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7 /// [7. Protocol Version Compatibility]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-7
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ProtocolVersion { pub enum ProtocolVersion {
@ -43,7 +46,13 @@ pub enum ProtocolVersion {
} }
impl ProtocolVersion { impl ProtocolVersion {
/// Returns the newest ProtocolVersion, that is supported by this library. /// Returns the newest [ProtocolVersion], that is supported by this library.
///
/// # Example
/// ```
/// # use hls_m3u8::types::ProtocolVersion;
/// assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7);
/// ```
pub const fn latest() -> Self { pub const fn latest() -> Self {
Self::V7 Self::V7
} }
@ -51,18 +60,15 @@ impl ProtocolVersion {
impl fmt::Display for ProtocolVersion { impl fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let n = {
match &self { match &self {
Self::V1 => 1, Self::V1 => write!(f, "1"),
Self::V2 => 2, Self::V2 => write!(f, "2"),
Self::V3 => 3, Self::V3 => write!(f, "3"),
Self::V4 => 4, Self::V4 => write!(f, "4"),
Self::V5 => 5, Self::V5 => write!(f, "5"),
Self::V6 => 6, Self::V6 => write!(f, "6"),
Self::V7 => 7, Self::V7 => write!(f, "7"),
} }
};
write!(f, "{}", n)
} }
} }
@ -115,5 +121,18 @@ mod tests {
assert_eq!(ProtocolVersion::V5, "5".parse().unwrap()); assert_eq!(ProtocolVersion::V5, "5".parse().unwrap());
assert_eq!(ProtocolVersion::V6, "6".parse().unwrap()); assert_eq!(ProtocolVersion::V6, "6".parse().unwrap());
assert_eq!(ProtocolVersion::V7, "7".parse().unwrap()); assert_eq!(ProtocolVersion::V7, "7".parse().unwrap());
assert_eq!(ProtocolVersion::V7, " 7 ".parse().unwrap());
assert!("garbage".parse::<ProtocolVersion>().is_err());
}
#[test]
fn test_default() {
assert_eq!(ProtocolVersion::default(), ProtocolVersion::V1);
}
#[test]
fn test_latest() {
assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7);
} }
} }

View file

@ -1,55 +1,108 @@
use std::fmt; use core::ops::Deref;
use std::str::FromStr; use derive_more::{Display, FromStr};
use crate::Error;
/// Signed decimal floating-point number. /// Signed decimal floating-point number.
/// ///
/// See: [4.2. Attribute Lists] /// See: [4.2. Attribute Lists]
/// ///
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2 /// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] #[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display, FromStr)]
pub(crate) struct SignedDecimalFloatingPoint(f64); pub(crate) struct SignedDecimalFloatingPoint(f64);
impl SignedDecimalFloatingPoint { impl SignedDecimalFloatingPoint {
/// Makes a new `SignedDecimalFloatingPoint` instance. /// Makes a new [SignedDecimalFloatingPoint] instance.
/// ///
/// # Errors /// # Panics
/// /// The given value must be finite, otherwise this function will panic!
/// The given value must be finite, pub fn new(value: f64) -> Self {
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`. if value.is_infinite() {
pub fn new(n: f64) -> crate::Result<Self> { panic!("Floating point value must be finite!");
if !n.is_finite() {
Err(Error::invalid_input())
} else {
Ok(SignedDecimalFloatingPoint(n))
} }
Self(value)
} }
/// Converts `DecimalFloatingPoint` to `f64`. pub(crate) const fn from_f64_unchecked(value: f64) -> Self {
Self(value)
}
/// Converts [DecimalFloatingPoint] to [f64].
pub const fn as_f64(self) -> f64 { pub const fn as_f64(self) -> f64 {
self.0 self.0
} }
} }
impl From<i32> for SignedDecimalFloatingPoint { impl Deref for SignedDecimalFloatingPoint {
fn from(f: i32) -> Self { type Target = f64;
SignedDecimalFloatingPoint(f64::from(f))
fn deref(&self) -> &Self::Target {
&self.0
} }
} }
impl Eq for SignedDecimalFloatingPoint {} impl Eq for SignedDecimalFloatingPoint {}
impl fmt::Display for SignedDecimalFloatingPoint { #[cfg(test)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { mod tests {
self.0.fmt(f) use super::*;
}
} macro_rules! test_from {
( $( $input:expr => $output:expr ),* ) => {
impl FromStr for SignedDecimalFloatingPoint { use ::core::convert::From;
type Err = Error;
#[test]
fn from_str(input: &str) -> Result<Self, Self::Err> { fn test_from() {
SignedDecimalFloatingPoint::new(input.parse().map_err(Error::parse_float_error)?) $(
assert_eq!(
$input,
$output,
);
)*
}
}
}
test_from![
SignedDecimalFloatingPoint::from(1u8) => SignedDecimalFloatingPoint::new(1.0),
SignedDecimalFloatingPoint::from(1i8) => SignedDecimalFloatingPoint::new(1.0),
SignedDecimalFloatingPoint::from(1u16) => SignedDecimalFloatingPoint::new(1.0),
SignedDecimalFloatingPoint::from(1i16) => SignedDecimalFloatingPoint::new(1.0),
SignedDecimalFloatingPoint::from(1u32) => SignedDecimalFloatingPoint::new(1.0),
SignedDecimalFloatingPoint::from(1i32) => SignedDecimalFloatingPoint::new(1.0),
SignedDecimalFloatingPoint::from(1.0f32) => SignedDecimalFloatingPoint::new(1.0),
SignedDecimalFloatingPoint::from(1.0f64) => SignedDecimalFloatingPoint::new(1.0)
];
#[test]
fn test_display() {
assert_eq!(
SignedDecimalFloatingPoint::new(1.0).to_string(),
1.0f64.to_string()
);
}
#[test]
#[should_panic]
fn test_new_panic() {
SignedDecimalFloatingPoint::new(::std::f64::INFINITY);
}
#[test]
fn test_parser() {
assert_eq!(
SignedDecimalFloatingPoint::new(1.0),
"1.0".parse::<SignedDecimalFloatingPoint>().unwrap()
);
assert!("garbage".parse::<SignedDecimalFloatingPoint>().is_err());
}
#[test]
fn test_as_f64() {
assert_eq!(SignedDecimalFloatingPoint::new(1.0).as_f64(), 1.0);
}
#[test]
fn test_deref() {
assert_eq!(SignedDecimalFloatingPoint::from(0.1).floor(), 0.0);
} }
} }

View file

@ -3,14 +3,13 @@ use std::str::FromStr;
use crate::attribute::AttributePairs; use crate::attribute::AttributePairs;
use crate::types::{DecimalResolution, HdcpLevel}; use crate::types::{DecimalResolution, HdcpLevel};
use crate::utils::parse_u64;
use crate::utils::{quote, unquote}; use crate::utils::{quote, unquote};
use crate::Error; use crate::Error;
/// [4.3.4.2. EXT-X-STREAM-INF] /// [4.3.4.2. EXT-X-STREAM-INF]
/// ///
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2 /// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
pub struct StreamInf { pub struct StreamInf {
bandwidth: u64, bandwidth: u64,
average_bandwidth: Option<u64>, average_bandwidth: Option<u64>,
@ -182,6 +181,8 @@ impl StreamInf {
/// ///
/// stream.set_resolution(1920, 1080); /// stream.set_resolution(1920, 1080);
/// assert_eq!(stream.resolution(), Some((1920, 1080))); /// assert_eq!(stream.resolution(), Some((1920, 1080)));
/// # stream.set_resolution(1280, 10);
/// # assert_eq!(stream.resolution(), Some((1280, 10)));
/// ``` /// ```
pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self { pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self {
if let Some(res) = &mut self.resolution { if let Some(res) = &mut self.resolution {
@ -259,8 +260,8 @@ impl FromStr for StreamInf {
for (key, value) in input.parse::<AttributePairs>()? { for (key, value) in input.parse::<AttributePairs>()? {
match key.as_str() { match key.as_str() {
"BANDWIDTH" => bandwidth = Some(parse_u64(value)?), "BANDWIDTH" => bandwidth = Some(value.parse::<u64>()?),
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(parse_u64(value)?), "AVERAGE-BANDWIDTH" => average_bandwidth = Some(value.parse::<u64>()?),
"CODECS" => codecs = Some(unquote(value)), "CODECS" => codecs = Some(unquote(value)),
"RESOLUTION" => resolution = Some(value.parse()?), "RESOLUTION" => resolution = Some(value.parse()?),
"HDCP-LEVEL" => hdcp_level = Some(value.parse()?), "HDCP-LEVEL" => hdcp_level = Some(value.parse()?),
@ -272,7 +273,7 @@ impl FromStr for StreamInf {
} }
} }
let bandwidth = bandwidth.ok_or(Error::missing_value("BANDWIDTH"))?; let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?;
Ok(Self { Ok(Self {
bandwidth, bandwidth,
@ -284,3 +285,66 @@ impl FromStr for StreamInf {
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display() {
let mut stream_inf = StreamInf::new(200);
stream_inf.set_average_bandwidth(Some(15));
stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
stream_inf.set_resolution(1920, 1080);
stream_inf.set_hdcp_level(Some(HdcpLevel::Type0));
stream_inf.set_video(Some("video"));
assert_eq!(
stream_inf.to_string(),
"BANDWIDTH=200,\
AVERAGE-BANDWIDTH=15,\
CODECS=\"mp4a.40.2,avc1.4d401e\",\
RESOLUTION=1920x1080,\
HDCP-LEVEL=TYPE-0,\
VIDEO=\"video\""
.to_string()
);
}
#[test]
fn test_parser() {
let mut stream_inf = StreamInf::new(200);
stream_inf.set_average_bandwidth(Some(15));
stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
stream_inf.set_resolution(1920, 1080);
stream_inf.set_hdcp_level(Some(HdcpLevel::Type0));
stream_inf.set_video(Some("video"));
assert_eq!(
stream_inf,
"BANDWIDTH=200,\
AVERAGE-BANDWIDTH=15,\
CODECS=\"mp4a.40.2,avc1.4d401e\",\
RESOLUTION=1920x1080,\
HDCP-LEVEL=TYPE-0,\
VIDEO=\"video\""
.parse()
.unwrap()
);
assert_eq!(
stream_inf,
"BANDWIDTH=200,\
AVERAGE-BANDWIDTH=15,\
CODECS=\"mp4a.40.2,avc1.4d401e\",\
RESOLUTION=1920x1080,\
HDCP-LEVEL=TYPE-0,\
VIDEO=\"video\",\
UNKNOWN=\"value\""
.parse()
.unwrap()
);
assert!("garbage".parse::<StreamInf>().is_err());
}
}

View file

@ -1,5 +1,26 @@
use crate::Error; use crate::Error;
macro_rules! impl_from {
( $($( $type:tt ),* => $target:path ),* ) => {
use ::core::convert::From;
$( // repeat $target
$( // repeat $type
impl From<$type> for $target {
fn from(value: $type) -> Self {
Self::from_f64_unchecked(value.into())
}
}
)*
)*
};
}
impl_from![
u8, u16, u32 => crate::types::DecimalFloatingPoint,
u8, i8, u16, i16, u32, i32, f32, f64 => crate::types::SignedDecimalFloatingPoint
];
pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> { pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
match s.as_ref() { match s.as_ref() {
"YES" => Ok(true), "YES" => Ok(true),
@ -8,11 +29,6 @@ pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
} }
} }
pub(crate) fn parse_u64<T: AsRef<str>>(s: T) -> crate::Result<u64> {
let n = s.as_ref().parse().map_err(Error::unknown)?; // TODO: Error::number
Ok(n)
}
/// According to the documentation the following characters are forbidden /// According to the documentation the following characters are forbidden
/// inside a quoted string: /// inside a quoted string:
/// - carriage return (`\r`) /// - carriage return (`\r`)
@ -61,14 +77,7 @@ mod tests {
fn test_parse_yes_or_no() { fn test_parse_yes_or_no() {
assert!(parse_yes_or_no("YES").unwrap()); assert!(parse_yes_or_no("YES").unwrap());
assert!(!parse_yes_or_no("NO").unwrap()); assert!(!parse_yes_or_no("NO").unwrap());
// TODO: test for error assert!(parse_yes_or_no("garbage").is_err());
}
#[test]
fn test_parse_u64() {
assert_eq!(parse_u64("1").unwrap(), 1);
assert_eq!(parse_u64("25").unwrap(), 25);
// TODO: test for error
} }
#[test] #[test]
@ -99,5 +108,7 @@ mod tests {
let input = tag(input, "A").unwrap(); let input = tag(input, "A").unwrap();
assert_eq!(input, "SampleString"); assert_eq!(input, "SampleString");
assert!(tag(input, "B").is_err());
} }
} }