From 81398f86cd7bbf4dfaca621ed2a8f6362c328971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Wed, 17 Nov 2021 14:10:18 +0200 Subject: [PATCH 01/13] Port to nom 7 Fixes https://github.com/rutgersc/m3u8-rs/issues/35 --- Cargo.toml | 2 +- src/parser.rs | 715 ++++++++++++++++++++++++++++++-------------------- tests/lib.rs | 2 +- 3 files changed, 436 insertions(+), 283 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4d9e65c..c7335ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ documentation = "https://rutgersc.github.io/doc/m3u8_rs/index.html" license = "MIT" [dependencies] -nom = { version = "5.1.0", optional = true } +nom = { version = "7", optional = true } [features] default = ["parser"] diff --git a/src/parser.rs b/src/parser.rs index abf45a6..5b2f80c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -83,14 +83,15 @@ extern crate nom; pub mod playlist; -use self::nom::character::complete::{digit1, multispace0, space0}; -use self::nom::character::complete::{line_ending, not_line_ending}; -use self::nom::combinator::map; +use self::nom::bytes::complete::{tag, take, is_not, is_a, take_while1, take_until}; +use self::nom::character::is_digit; +use self::nom::character::complete::{digit1, multispace0, space0, line_ending, not_line_ending, char, none_of}; +use self::nom::sequence::{delimited, preceded, tuple, pair, terminated}; +use self::nom::combinator::{map, map_res, opt, peek, eof, complete}; +use self::nom::multi::{fold_many0, many0}; +use self::nom::branch::alt; + use self::nom::IResult; -use self::nom::{ - alt, char, complete, delimited, do_parse, eof, is_a, is_not, many0, map, map_res, named, - none_of, opt, peek, tag, take, take_until, terminated, -}; use playlist::*; use std::collections::HashMap; use std::f32; @@ -221,55 +222,61 @@ pub fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> { is_master_opt } -named!(pub is_master_playlist_tag_line(&[u8]) -> Option<(bool, String)>, - do_parse!( - opt!(is_a!("\r\n")) - >> tag: opt!(alt!( - map!(tag!("#EXT-X-STREAM-INF"), |t| (true, t)) - | map!(tag!("#EXT-X-I-FRAME-STREAM-INF"), |t| (true, t)) - | map!(terminated!(tag!("#EXT-X-MEDIA"), is_not!("-")), |t| (true, t)) // terminated!() to prevent matching with #EXT-X-MEDIA-SEQUENCE for which we have a separate pattern below - | map!(tag!("#EXT-X-SESSION-KEY"), |t| (true, t)) - | map!(tag!("#EXT-X-SESSION-DATA"), |t| (true, t)) +pub fn is_master_playlist_tag_line(i: &[u8]) -> IResult<&[u8], Option<(bool, String)>> { + map( + tuple(( + opt(is_a("\r\n")), + opt(alt(( + map(tag("#EXT-X-STREAM-INF"), |t| (true, t)), + map(tag("#EXT-X-I-FRAME-STREAM-INF"), |t| (true, t)), + map(terminated(tag("#EXT-X-MEDIA"), is_not("-")), |t| (true, t)), // terminated() to prevent matching with #EXT-X-MEDIA-SEQUENCE for which we have a separate pattern below + map(tag("#EXT-X-SESSION-KEY"), |t| (true, t)), + map(tag("#EXT-X-SESSION-DATA"), |t| (true, t)), - | map!(tag!("#EXT-X-TARGETDURATION"), |t| (false, t)) - | map!(tag!("#EXT-X-MEDIA-SEQUENCE"), |t| (false, t)) - | map!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE"), |t| (false, t)) - | map!(tag!("#EXT-X-ENDLIST"), |t| (false, t)) - | map!(tag!("#EXT-X-PLAYLIST-TYPE"), |t| (false, t)) - | map!(tag!("#EXT-X-I-FRAMES-ONLY"), |t| (false, t)) + map(tag("#EXT-X-TARGETDURATION"), |t| (false, t)), + map(tag("#EXT-X-MEDIA-SEQUENCE"), |t| (false, t)), + map(tag("#EXT-X-DISCONTINUITY-SEQUENCE"), |t| (false, t)), + map(tag("#EXT-X-ENDLIST"), |t| (false, t)), + map(tag("#EXT-X-PLAYLIST-TYPE"), |t| (false, t)), + map(tag("#EXT-X-I-FRAMES-ONLY"), |t| (false, t)), - | map!(tag!("#EXTINF"), |t| (false, t)) - | map!(tag!("#EXT-X-BYTERANGE"), |t| (false, t)) - | map!(tag!("#EXT-X-DISCONTINUITY"), |t| (false, t)) - | map!(tag!("#EXT-X-KEY"), |t| (false, t)) - | map!(tag!("#EXT-X-MAP"), |t| (false, t)) - | map!(tag!("#EXT-X-PROGRAM-DATE-TIME"), |t| (false, t)) - | map!(tag!("#EXT-X-DATERANGE"), |t| (false, t)) - )) - >> consume_line - >> - ( { - tag.map(|(a,b)| (a, from_utf8_slice(b).unwrap())) - } ) - ) -); + map(tag("#EXTINF"), |t| (false, t)), + map(tag("#EXT-X-BYTERANGE"), |t| (false, t)), + map(tag("#EXT-X-DISCONTINUITY"), |t| (false, t)), + map(tag("#EXT-X-KEY"), |t| (false, t)), + map(tag("#EXT-X-MAP"), |t| (false, t)), + map(tag("#EXT-X-PROGRAM-DATE-TIME"), |t| (false, t)), + map(tag("#EXT-X-DATERANGE"), |t| (false, t)), + ))), + consume_line, + )), + |(_, tag, _)| tag.map(|(a,b)| (a, from_utf8_slice(b).unwrap())), + )(i) +} // ----------------------------------------------------------------------------------------------- // Master Playlist Tags // ----------------------------------------------------------------------------------------------- -pub fn parse_master_playlist_tags(input: &[u8]) -> IResult<&[u8], Vec> { - do_parse!( - input, - tags: many0!(complete!(do_parse!( - m: master_playlist_tag >> multispace0 >> (m) - ))) >> opt!(eof!()) - >> ({ - let mut tags_rev: Vec = tags; - tags_rev.reverse(); - tags_rev - }) - ) +pub fn parse_master_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec> { + map( + tuple(( + many0( + complete( + map( + pair(master_playlist_tag, multispace0), + |(tag, _)| tag, + ) + ) + ), + opt(eof), + )), + |(tags, _)| { + let mut tags_rev: Vec = tags; + tags_rev.reverse(); + tags_rev + }, + )(i) } /// Contains all the tags required to parse a master playlist. @@ -288,24 +295,27 @@ pub enum MasterPlaylistTag { Unknown(ExtTag), } -pub fn master_playlist_tag(input: &[u8]) -> IResult<&[u8], MasterPlaylistTag> { - alt!( - input, - map!(m3u_tag, MasterPlaylistTag::M3U) - | map!(version_tag, MasterPlaylistTag::Version) - | map!(variant_stream_tag, MasterPlaylistTag::VariantStream) - | map!(variant_i_frame_stream_tag, MasterPlaylistTag::VariantStream) - | map!(alternative_media_tag, MasterPlaylistTag::AlternativeMedia) - | map!(session_data_tag, MasterPlaylistTag::SessionData) - | map!(session_key_tag, MasterPlaylistTag::SessionKey) - | map!(start_tag, MasterPlaylistTag::Start) - | map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| { - MasterPlaylistTag::IndependentSegments - }) - | map!(ext_tag, MasterPlaylistTag::Unknown) - | map!(comment_tag, MasterPlaylistTag::Comment) - | map!(consume_line, MasterPlaylistTag::Uri) - ) +pub fn master_playlist_tag(i: &[u8]) -> IResult<&[u8], MasterPlaylistTag> { + // Don't accept empty inputs here + peek(take(1usize))(i)?; + + alt(( + map(m3u_tag, MasterPlaylistTag::M3U), + map(version_tag, MasterPlaylistTag::Version), + map(variant_stream_tag, MasterPlaylistTag::VariantStream), + map(variant_i_frame_stream_tag, MasterPlaylistTag::VariantStream), + map(alternative_media_tag, MasterPlaylistTag::AlternativeMedia), + map(session_data_tag, MasterPlaylistTag::SessionData), + map(session_key_tag, MasterPlaylistTag::SessionKey), + map(start_tag, MasterPlaylistTag::Start), + map( + tag("#EXT-X-INDEPENDENT-SEGMENTS"), + |_| MasterPlaylistTag::IndependentSegments, + ), + map(ext_tag, MasterPlaylistTag::Unknown), + map(comment_tag, MasterPlaylistTag::Comment), + map(consume_line, MasterPlaylistTag::Uri), + ))(i) } pub fn master_playlist_from_tags(mut tags: Vec) -> MasterPlaylist { @@ -349,48 +359,79 @@ pub fn master_playlist_from_tags(mut tags: Vec) -> MasterPlay master_playlist } -named!(pub variant_stream_tag, - do_parse!(tag!("#EXT-X-STREAM-INF:") >> attributes: key_value_pairs >> - ( VariantStream::from_hashmap(attributes, false))) -); +pub fn variant_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { + map( + pair( + tag("#EXT-X-STREAM-INF:"), + key_value_pairs, + ), + |(_, attributes)| VariantStream::from_hashmap(attributes, false), + )(i) +} -named!(pub variant_i_frame_stream_tag, - do_parse!( tag!("#EXT-X-I-FRAME-STREAM-INF:") >> attributes: key_value_pairs >> - ( VariantStream::from_hashmap(attributes, true))) -); +pub fn variant_i_frame_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { + map( + pair( + tag("#EXT-X-I-FRAME-STREAM-INF:"), + key_value_pairs, + ), + |(_, attributes)| VariantStream::from_hashmap(attributes, true), + )(i) +} -named!(pub alternative_media_tag, - do_parse!( tag!("#EXT-X-MEDIA:") >> attributes: key_value_pairs >> - ( AlternativeMedia::from_hashmap(attributes))) -); +pub fn alternative_media_tag(i: &[u8]) -> IResult<&[u8], AlternativeMedia> { + map( + pair( + tag("#EXT-X-MEDIA:"), + key_value_pairs, + ), + |(_, media)| AlternativeMedia::from_hashmap(media), + )(i) +} -named!(pub session_data_tag, - do_parse!( tag!("#EXT-X-SESSION-DATA:") >> - session_data: map_res!(key_value_pairs, |attrs| SessionData::from_hashmap(attrs)) >> - ( session_data)) -); +pub fn session_data_tag(i: &[u8]) -> IResult<&[u8], SessionData> { + map_res( + pair( + tag("#EXT-X-SESSION-DATA:"), + key_value_pairs, + ), + |(_, session_data)| SessionData::from_hashmap(session_data), + )(i) +} -named!(pub session_key_tag, - do_parse!( tag!("#EXT-X-SESSION-KEY:") >> session_key: map!(key, SessionKey) >> - ( session_key)) -); +pub fn session_key_tag(i: &[u8]) -> IResult<&[u8], SessionKey> { + map( + pair( + tag("#EXT-X-SESSION-KEY:"), + key, + ), + |(_, key)| SessionKey(key), + )(i) +} // ----------------------------------------------------------------------------------------------- // Media Playlist // ----------------------------------------------------------------------------------------------- -pub fn parse_media_playlist_tags(input: &[u8]) -> IResult<&[u8], Vec> { - do_parse!( - input, - tags: many0!(complete!(do_parse!( - m: media_playlist_tag >> multispace0 >> (m) - ))) >> opt!(eof!()) - >> ({ - let mut tags_rev: Vec = tags; - tags_rev.reverse(); - tags_rev - }) - ) +pub fn parse_media_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec> { + map( + tuple(( + many0( + complete( + map( + pair(media_playlist_tag, multispace0), + |(tag, _)| tag, + ) + ) + ), + opt(eof), + )), + |(tags, _)| { + let mut tags_rev: Vec = tags; + tags_rev.reverse(); + tags_rev + }, + )(i) } /// Contains all the tags required to parse a media playlist. @@ -410,37 +451,56 @@ pub enum MediaPlaylistTag { IndependentSegments, } -pub fn media_playlist_tag(input: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { - alt!( - input, - map!(m3u_tag, MediaPlaylistTag::M3U) - | map!(version_tag, MediaPlaylistTag::Version) - | map!( - do_parse!(tag!("#EXT-X-TARGETDURATION:") >> n: float >> (n)), - MediaPlaylistTag::TargetDuration - ) - | map!( - do_parse!(tag!("#EXT-X-MEDIA-SEQUENCE:") >> n: number >> (n)), - MediaPlaylistTag::MediaSequence - ) - | map!( - do_parse!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE:") >> n: number >> (n)), - MediaPlaylistTag::DiscontinuitySequence - ) - | map!( - do_parse!(tag!("#EXT-X-PLAYLIST-TYPE:") >> t: playlist_type >> (t)), - MediaPlaylistTag::PlaylistType - ) - | map!(tag!("#EXT-X-I-FRAMES-ONLY"), |_| { - MediaPlaylistTag::IFramesOnly - }) - | map!(start_tag, MediaPlaylistTag::Start) - | map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| { - MediaPlaylistTag::IndependentSegments - }) - | map!(tag!("#EXT-X-ENDLIST"), |_| MediaPlaylistTag::EndList) - | map!(media_segment_tag, MediaPlaylistTag::Segment) - ) +pub fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { + // Don't accept empty inputs here + peek(take(1usize))(i)?; + + alt(( + map(m3u_tag, MediaPlaylistTag::M3U), + map(version_tag, MediaPlaylistTag::Version), + map( + pair( + tag("#EXT-X-TARGETDURATION:"), + float, + ), + |(_, duration)| MediaPlaylistTag::TargetDuration(duration), + ), + map( + pair( + tag("#EXT-X-MEDIA-SEQUENCE:"), + number, + ), + |(_, sequence)| MediaPlaylistTag::MediaSequence(sequence), + ), + map( + pair( + tag("#EXT-X-DISCONTINUITY-SEQUENCE:"), + number, + ), + |(_, sequence)| MediaPlaylistTag::DiscontinuitySequence(sequence), + ), + map( + pair( + tag("#EXT-X-PLAYLIST-TYPE:"), + playlist_type, + ), + |(_, typ)| MediaPlaylistTag::PlaylistType(typ), + ), + map( + tag("#EXT-X-I-FRAMES-ONLY"), + |_| MediaPlaylistTag::IFramesOnly, + ), + map(start_tag, MediaPlaylistTag::Start), + map( + tag("#EXT-X-INDEPENDENT-SEGMENTS"), + |_| MediaPlaylistTag::IndependentSegments, + ), + map( + tag("#EXT-X-ENDLIST"), + |_| MediaPlaylistTag::EndList, + ), + map(media_segment_tag, MediaPlaylistTag::Segment), + ))(i) } pub fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylist { @@ -521,16 +581,15 @@ pub fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylis media_playlist } -named!(pub playlist_type, - map_res!( - do_parse!( - p: map_res!(is_not!("\r\n"), str::from_utf8) - >> take!(1) - >> (p) - ), - MediaPlaylistType::from_str - ) -); +pub fn playlist_type(i: &[u8]) -> IResult<&[u8], MediaPlaylistType> { + map_res( + tuple(( + map_res(is_not("\r\n"), str::from_utf8), + take(1usize), + )), + |(typ, _)| MediaPlaylistType::from_str(typ), + )(i) +} // ----------------------------------------------------------------------------------------------- // Media Segment @@ -551,175 +610,269 @@ pub enum SegmentTag { Uri(String), } -pub fn media_segment_tag(input: &[u8]) -> IResult<&[u8], SegmentTag> { - alt!( - input, - map!( - do_parse!(tag!("#EXTINF:") >> e: duration_title_tag >> (e)), - |(a, b)| SegmentTag::Extinf(a, b) - ) | map!( - do_parse!(tag!("#EXT-X-BYTERANGE:") >> r: byte_range_val >> (r)), - SegmentTag::ByteRange - ) | map!(tag!("#EXT-X-DISCONTINUITY"), |_| SegmentTag::Discontinuity) - | map!( - do_parse!(tag!("#EXT-X-KEY:") >> k: key >> (k)), - SegmentTag::Key - ) - | map!( - do_parse!(tag!("#EXT-X-MAP:") >> m: extmap >> (m)), - SegmentTag::Map - ) - | map!( - do_parse!(tag!("#EXT-X-PROGRAM-DATE-TIME:") >> t: consume_line >> (t)), - SegmentTag::ProgramDateTime - ) - | map!( - do_parse!(tag!("#EXT-X-DATE-RANGE:") >> t: consume_line >> (t)), - SegmentTag::DateRange - ) - | map!(ext_tag, SegmentTag::Unknown) - | map!(comment_tag, SegmentTag::Comment) - | map!(consume_line, SegmentTag::Uri) - ) +pub fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> { + alt(( + map( + pair( + tag("#EXTINF:"), + duration_title_tag, + ), + |(_, (duration, title))| SegmentTag::Extinf(duration, title), + ), + map( + pair( + tag("#EXT-X-BYTERANGE:"), + byte_range_val, + ), + |(_, range)| SegmentTag::ByteRange(range), + ), + map( + tag("#EXT-X-DISCONTINUITY"), + |_| SegmentTag::Discontinuity + ), + map( + pair( + tag("#EXT-X-KEY:"), + key, + ), + |(_, key)| SegmentTag::Key(key), + ), + map( + pair( + tag("#EXT-X-MAP:"), + extmap, + ), + |(_, map)| SegmentTag::Map(map), + ), + map( + pair( + tag("#EXT-X-PROGRAM-DATE-TIME:"), + consume_line, + ), + |(_, line)| SegmentTag::ProgramDateTime(line), + ), + map( + pair( + tag("#EXT-X-DATE-RANGE:"), + consume_line, + ), + |(_, line)| SegmentTag::DateRange(line), + ), + map(ext_tag, SegmentTag::Unknown), + map(comment_tag, SegmentTag::Comment), + map(consume_line, SegmentTag::Uri), + ))(i) } -named!(pub duration_title_tag<(f32, Option)>, - do_parse!( - duration: float - >> opt!(tag!(",")) - >> title: opt!(map_res!(is_not!("\r\n,"), from_utf8_slice)) - >> take!(1) - >> opt!(tag!(",")) - >> - (duration, title) - ) -); +pub fn duration_title_tag(i: &[u8]) -> IResult<&[u8], (f32, Option)> { + map( + tuple(( + float, + opt(char(',')), + opt( + map_res(is_not("\r\n,"), from_utf8_slice), + ), + take(1usize), + opt(char(',')), + )), + |(duration, _, title, _, _)| (duration, title), + )(i) +} -named!(pub key, map!(key_value_pairs, Key::from_hashmap)); +pub fn key(i: &[u8]) -> IResult<&[u8], Key> { + map( + key_value_pairs, + Key::from_hashmap, + )(i) +} -named!(pub extmap, map!(key_value_pairs, |attrs| Map { - uri: attrs.get("URI").cloned().unwrap_or_default(), - byte_range: attrs.get("BYTERANGE").map(|range| { - match byte_range_val(range.as_bytes()) { - IResult::Ok((_, br)) => br, - _ => panic!("Should not happen"), +pub fn extmap(i: &[u8]) -> IResult<&[u8], Map> { + map_res( + key_value_pairs, + |attrs| -> Result { + let uri = attrs.get("URI").cloned().unwrap_or_default(); + let byte_range = attrs.get("BYTERANGE").map(|range| + match byte_range_val(range.as_bytes()) { + IResult::Ok((_, range)) => Ok(range), + IResult::Err(_) => Err("invalid byte range"), + } + ).transpose()?; + + Ok(Map { + uri, + byte_range, + }) } - }), -})); + )(i) +} // ----------------------------------------------------------------------------------------------- // Basic tags // ----------------------------------------------------------------------------------------------- -named!(pub m3u_tag, - map_res!(tag!("#EXTM3U"), from_utf8_slice) -); +pub fn m3u_tag(i: &[u8]) -> IResult<&[u8], String> { + map_res( + tag("#EXTM3U"), + from_utf8_slice, + )(i) +} -named!(pub version_tag, - do_parse!( - tag!("#EXT-X-VERSION:") >> version: map_res!(digit1, str::from_utf8) >> - (version.parse().unwrap_or_default()) - ) -); +pub fn version_tag(i: &[u8]) -> IResult<&[u8], usize> { + map( + pair( + tag("#EXT-X-VERSION:"), + map_res(digit1, str::from_utf8), + ), + |(_, version)| { + version.parse().unwrap_or_default() + } + )(i) +} -named!(pub start_tag, - do_parse!(tag!("#EXT-X-START:") >> attributes:key_value_pairs >> - (Start::from_hashmap(attributes)) - ) -); +pub fn start_tag(i: &[u8]) -> IResult<&[u8], Start> { + map( + pair( + tag("#EXT-X-START:"), + key_value_pairs, + ), + |(_, attributes)| { + Start::from_hashmap(attributes) + } + )(i) +} -named!(pub ext_tag, - do_parse!( - tag!("#EXT-") - >> tag: map_res!(is_not!("\r\n:"), from_utf8_slice) - >> opt!(tag!(":")) - >> rest: opt!(map_res!(is_not!("\r\n"), from_utf8_slice)) - >> take!(1) - >> ( +pub fn ext_tag(i: &[u8]) -> IResult<&[u8], ExtTag> { + map( + tuple(( + tag("#EXT-"), + map_res(is_not("\r\n:"), from_utf8_slice), + opt(char(':')), + opt(map_res(is_not("\r\n"), from_utf8_slice)), + take(1usize), + )), + |(_, tag, _, rest, _)| { ExtTag { tag, rest } - ) - ) -); + } + )(i) +} -named!(pub comment_tag, - do_parse!( - tag!("#") >> text: map_res!(is_not!("\r\n"), from_utf8_slice) - >> take!(1) - >> (text) - ) -); +pub fn comment_tag(i: &[u8]) -> IResult<&[u8], String> { + map( + pair( + preceded( + char('#'), + map_res(is_not("\r\n"), from_utf8_slice), + ), + take(1usize), + ), + |(text, _)| text, + )(i) +} // ----------------------------------------------------------------------------------------------- // Util // ----------------------------------------------------------------------------------------------- -named!(pub key_value_pairs(&[u8]) -> HashMap, - map!( - many0!(do_parse!(space0 >> k:key_value_pair >> (k) )) - , - |pairs: Vec<(String, String)>| { - pairs.into_iter().collect() +pub fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap> { + fold_many0( + preceded(space0, key_value_pair), + HashMap::new, + |mut acc: HashMap<_, _>, (left, right)| { + acc.insert(left, right); + acc } - ) -); + )(i) +} -named!(pub key_value_pair(&[u8]) -> (String, String), - do_parse!( - peek!(none_of!("\r\n")) - >> left: map_res!(take_until!("="), from_utf8_slice) - >> take!(1) - >> right: alt!(quoted | unquoted) - >> opt!(char!(',')) - >> - (left, right) - ) -); +pub fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, String)> { + map( + tuple(( + peek(none_of("\r\n")), + map_res(take_until("="), from_utf8_slice), + char('='), + alt((quoted, unquoted)), + opt(char(',')), + )), + |(_, left, _, right, _)| { + (left, right) + } + )(i) +} -named!(pub quoted, - delimited!(char!('\"'), map_res!(is_not!("\""), from_utf8_slice), char!('\"')) -); +pub fn quoted(i: &[u8]) -> IResult<&[u8], String> { + delimited( + char('\"'), + map_res( + is_not("\""), + from_utf8_slice + ), + char('\"') + )(i) +} -named!(pub unquoted, - map_res!(is_not!(",\r\n"), from_utf8_slice) -); +pub fn unquoted(i: &[u8]) -> IResult<&[u8], String> { + map_res( + is_not(",\r\n"), + from_utf8_slice + )(i) +} -named!(pub consume_line, - do_parse!( - line: map_res!(not_line_ending, from_utf8_slice) - >> opt!(line_ending) - >> (line) - ) -); +pub fn consume_line(i: &[u8]) -> IResult<&[u8], String> { + map( + pair( + map_res(not_line_ending, from_utf8_slice), + opt(line_ending), + ), + |(line, _)| line, + )(i) +} -named!(pub number, - map_res!(map_res!(digit1, str::from_utf8), str::FromStr::from_str) -); +pub fn number(i: &[u8]) -> IResult<&[u8], i32> { + map_res(take_while1(is_digit), + |s| { + // Can't fail because we validated it above already + let s = str::from_utf8(s).unwrap(); + str::parse::(s) + })(i) +} -named!(pub byte_range_val, - do_parse!( - n: number - >> o: opt!(do_parse!(char!('@') >> n:number >> (n) )) >> - (ByteRange { length: n, offset: o }) - ) -); +pub fn byte_range_val(i: &[u8]) -> IResult<&[u8], ByteRange> { + map( + pair( + number, + opt( + preceded(char('@'), number) + ), + ), + |(n, o)| { + ByteRange { length: n, offset: o } + } + )(i) +} -named!(pub float, - do_parse!( - left: map_res!(digit1, str::from_utf8) - >> right_opt: opt!(do_parse!(char!('.') >> d:map_res!(digit1, str::from_utf8) >> (d) )) - >> - ( - match right_opt { +pub fn float(i: &[u8]) -> IResult<&[u8], f32> { + map_res( + pair( + take_while1(is_digit), + opt( + preceded(char('.'), take_while1(is_digit)) + ), + ), + |(left, right): (&[u8], Option<&[u8]>)| match right { Some(right) => { - let mut num = String::from(left); - num.push('.'); - num.push_str(right); - num.parse().unwrap() - }, - None => left.parse().unwrap(), - }) - ) -); + let n = &i[..(left.len() + right.len() + 1)]; + // Can't fail because we validated it above already + let n = str::from_utf8(n).unwrap(); + n.parse() + } + None => { + // Can't fail because we validated it above already + let left = str::from_utf8(left).unwrap(); + left.parse() + } + } + )(i) +} pub fn from_utf8_slice(s: &[u8]) -> Result { String::from_utf8(s.to_vec()) diff --git a/tests/lib.rs b/tests/lib.rs index d29492b..15df30f 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -5,7 +5,7 @@ extern crate nom; use m3u8_rs::playlist::*; use m3u8_rs::*; -use nom::*; +use nom::AsBytes; use std::collections::HashMap; use std::fs; use std::fs::File; From 0ed0ce51f85606bb8307568e4d67016fa0fe2c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Wed, 17 Nov 2021 14:37:55 +0200 Subject: [PATCH 02/13] Migrate to Rust 2018 Cleans up some noise. --- Cargo.toml | 1 + README.md | 6 ------ examples/simple.rs | 3 --- examples/with_nom_result.rs | 3 --- src/parser.rs | 25 +++++++++---------------- tests/lib.rs | 3 --- 6 files changed, 10 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c7335ca..e7c8668 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/rutgersc/m3u8-rs" description = "A library for parsing m3u8 files (Apple's HTTP Live Streaming (HLS) protocol)." documentation = "https://rutgersc.github.io/doc/m3u8_rs/index.html" license = "MIT" +edition = "2018" [dependencies] nom = { version = "7", optional = true } diff --git a/README.md b/README.md index 6001f2f..9e5f36d 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,6 @@ To use this library, add the following dependency to `Cargo.toml`: m3u8-rs = "1.0.6" ``` -And add the crate to `lib.rs` - -```rust -extern crate m3u8_rs; -``` - Also available on [crates.io](https://crates.io/crates/m3u8-rs) # Documentation diff --git a/examples/simple.rs b/examples/simple.rs index 3774d6b..7d53fc2 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,6 +1,3 @@ -extern crate m3u8_rs; -extern crate nom; - use m3u8_rs::playlist::Playlist; use std::io::Read; diff --git a/examples/with_nom_result.rs b/examples/with_nom_result.rs index cbe8d06..b795364 100644 --- a/examples/with_nom_result.rs +++ b/examples/with_nom_result.rs @@ -1,6 +1,3 @@ -extern crate m3u8_rs; -extern crate nom; - use m3u8_rs::playlist::Playlist; use std::io::Read; diff --git a/src/parser.rs b/src/parser.rs index 5b2f80c..0e67729 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6,8 +6,6 @@ //! Parsing a playlist and let the parser figure out if it's a media or master playlist. //! //! ``` -//! extern crate nom; -//! extern crate m3u8_rs; //! use m3u8_rs::playlist::Playlist; //! use nom::IResult; //! use std::io::Read; @@ -28,8 +26,6 @@ //! Parsing a master playlist directly //! //! ``` -//! extern crate nom; -//! extern crate m3u8_rs; //! use std::io::Read; //! use nom::IResult; //! @@ -48,7 +44,6 @@ //! Creating a playlist and writing it back to a vec/file //! //! ``` -//! extern crate m3u8_rs; //! use m3u8_rs::playlist::{MediaPlaylist, MediaPlaylistType, MediaSegment}; //! //! fn main() { @@ -79,20 +74,18 @@ //! //! ``` -extern crate nom; - pub mod playlist; -use self::nom::bytes::complete::{tag, take, is_not, is_a, take_while1, take_until}; -use self::nom::character::is_digit; -use self::nom::character::complete::{digit1, multispace0, space0, line_ending, not_line_ending, char, none_of}; -use self::nom::sequence::{delimited, preceded, tuple, pair, terminated}; -use self::nom::combinator::{map, map_res, opt, peek, eof, complete}; -use self::nom::multi::{fold_many0, many0}; -use self::nom::branch::alt; +use nom::bytes::complete::{tag, take, is_not, is_a, take_while1, take_until}; +use nom::character::is_digit; +use nom::character::complete::{digit1, multispace0, space0, line_ending, not_line_ending, char, none_of}; +use nom::sequence::{delimited, preceded, tuple, pair, terminated}; +use nom::combinator::{map, map_res, opt, peek, eof, complete}; +use nom::multi::{fold_many0, many0}; +use nom::branch::alt; -use self::nom::IResult; -use playlist::*; +use nom::IResult; +use crate::playlist::*; use std::collections::HashMap; use std::f32; use std::result::Result; diff --git a/tests/lib.rs b/tests/lib.rs index 15df30f..02982bd 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,8 +1,5 @@ #![allow(unused_variables, unused_imports, dead_code)] -extern crate m3u8_rs; -extern crate nom; - use m3u8_rs::playlist::*; use m3u8_rs::*; use nom::AsBytes; From a44c2a1a72b211e0e24fa3933461e7f7b1bd361f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Wed, 17 Nov 2021 15:59:31 +0200 Subject: [PATCH 03/13] Run parser through `cargo fmt` Now that we don't use the nom macros anymore this works properly. --- src/parser.rs | 332 ++++++++++++++++---------------------------------- 1 file changed, 108 insertions(+), 224 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 0e67729..4abd4e8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -76,16 +76,18 @@ pub mod playlist; -use nom::bytes::complete::{tag, take, is_not, is_a, take_while1, take_until}; -use nom::character::is_digit; -use nom::character::complete::{digit1, multispace0, space0, line_ending, not_line_ending, char, none_of}; -use nom::sequence::{delimited, preceded, tuple, pair, terminated}; -use nom::combinator::{map, map_res, opt, peek, eof, complete}; -use nom::multi::{fold_many0, many0}; use nom::branch::alt; +use nom::bytes::complete::{is_a, is_not, tag, take, take_until, take_while1}; +use nom::character::complete::{ + char, digit1, line_ending, multispace0, none_of, not_line_ending, space0, +}; +use nom::character::is_digit; +use nom::combinator::{complete, eof, map, map_res, opt, peek}; +use nom::multi::{fold_many0, many0}; +use nom::sequence::{delimited, pair, preceded, terminated, tuple}; -use nom::IResult; use crate::playlist::*; +use nom::IResult; use std::collections::HashMap; use std::f32; use std::result::Result; @@ -220,30 +222,28 @@ pub fn is_master_playlist_tag_line(i: &[u8]) -> IResult<&[u8], Option<(bool, Str tuple(( opt(is_a("\r\n")), opt(alt(( - map(tag("#EXT-X-STREAM-INF"), |t| (true, t)), - map(tag("#EXT-X-I-FRAME-STREAM-INF"), |t| (true, t)), - map(terminated(tag("#EXT-X-MEDIA"), is_not("-")), |t| (true, t)), // terminated() to prevent matching with #EXT-X-MEDIA-SEQUENCE for which we have a separate pattern below - map(tag("#EXT-X-SESSION-KEY"), |t| (true, t)), - map(tag("#EXT-X-SESSION-DATA"), |t| (true, t)), - - map(tag("#EXT-X-TARGETDURATION"), |t| (false, t)), - map(tag("#EXT-X-MEDIA-SEQUENCE"), |t| (false, t)), - map(tag("#EXT-X-DISCONTINUITY-SEQUENCE"), |t| (false, t)), - map(tag("#EXT-X-ENDLIST"), |t| (false, t)), - map(tag("#EXT-X-PLAYLIST-TYPE"), |t| (false, t)), - map(tag("#EXT-X-I-FRAMES-ONLY"), |t| (false, t)), - - map(tag("#EXTINF"), |t| (false, t)), - map(tag("#EXT-X-BYTERANGE"), |t| (false, t)), - map(tag("#EXT-X-DISCONTINUITY"), |t| (false, t)), - map(tag("#EXT-X-KEY"), |t| (false, t)), - map(tag("#EXT-X-MAP"), |t| (false, t)), - map(tag("#EXT-X-PROGRAM-DATE-TIME"), |t| (false, t)), - map(tag("#EXT-X-DATERANGE"), |t| (false, t)), + map(tag("#EXT-X-STREAM-INF"), |t| (true, t)), + map(tag("#EXT-X-I-FRAME-STREAM-INF"), |t| (true, t)), + map(terminated(tag("#EXT-X-MEDIA"), is_not("-")), |t| (true, t)), // terminated() to prevent matching with #EXT-X-MEDIA-SEQUENCE for which we have a separate pattern below + map(tag("#EXT-X-SESSION-KEY"), |t| (true, t)), + map(tag("#EXT-X-SESSION-DATA"), |t| (true, t)), + map(tag("#EXT-X-TARGETDURATION"), |t| (false, t)), + map(tag("#EXT-X-MEDIA-SEQUENCE"), |t| (false, t)), + map(tag("#EXT-X-DISCONTINUITY-SEQUENCE"), |t| (false, t)), + map(tag("#EXT-X-ENDLIST"), |t| (false, t)), + map(tag("#EXT-X-PLAYLIST-TYPE"), |t| (false, t)), + map(tag("#EXT-X-I-FRAMES-ONLY"), |t| (false, t)), + map(tag("#EXTINF"), |t| (false, t)), + map(tag("#EXT-X-BYTERANGE"), |t| (false, t)), + map(tag("#EXT-X-DISCONTINUITY"), |t| (false, t)), + map(tag("#EXT-X-KEY"), |t| (false, t)), + map(tag("#EXT-X-MAP"), |t| (false, t)), + map(tag("#EXT-X-PROGRAM-DATE-TIME"), |t| (false, t)), + map(tag("#EXT-X-DATERANGE"), |t| (false, t)), ))), consume_line, )), - |(_, tag, _)| tag.map(|(a,b)| (a, from_utf8_slice(b).unwrap())), + |(_, tag, _)| tag.map(|(a, b)| (a, from_utf8_slice(b).unwrap())), )(i) } @@ -254,14 +254,10 @@ pub fn is_master_playlist_tag_line(i: &[u8]) -> IResult<&[u8], Option<(bool, Str pub fn parse_master_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec> { map( tuple(( - many0( - complete( - map( - pair(master_playlist_tag, multispace0), - |(tag, _)| tag, - ) - ) - ), + many0(complete(map( + pair(master_playlist_tag, multispace0), + |(tag, _)| tag, + ))), opt(eof), )), |(tags, _)| { @@ -301,10 +297,9 @@ pub fn master_playlist_tag(i: &[u8]) -> IResult<&[u8], MasterPlaylistTag> { map(session_data_tag, MasterPlaylistTag::SessionData), map(session_key_tag, MasterPlaylistTag::SessionKey), map(start_tag, MasterPlaylistTag::Start), - map( - tag("#EXT-X-INDEPENDENT-SEGMENTS"), - |_| MasterPlaylistTag::IndependentSegments, - ), + map(tag("#EXT-X-INDEPENDENT-SEGMENTS"), |_| { + MasterPlaylistTag::IndependentSegments + }), map(ext_tag, MasterPlaylistTag::Unknown), map(comment_tag, MasterPlaylistTag::Comment), map(consume_line, MasterPlaylistTag::Uri), @@ -354,52 +349,35 @@ pub fn master_playlist_from_tags(mut tags: Vec) -> MasterPlay pub fn variant_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { map( - pair( - tag("#EXT-X-STREAM-INF:"), - key_value_pairs, - ), + pair(tag("#EXT-X-STREAM-INF:"), key_value_pairs), |(_, attributes)| VariantStream::from_hashmap(attributes, false), )(i) } pub fn variant_i_frame_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { map( - pair( - tag("#EXT-X-I-FRAME-STREAM-INF:"), - key_value_pairs, - ), + pair(tag("#EXT-X-I-FRAME-STREAM-INF:"), key_value_pairs), |(_, attributes)| VariantStream::from_hashmap(attributes, true), )(i) } pub fn alternative_media_tag(i: &[u8]) -> IResult<&[u8], AlternativeMedia> { - map( - pair( - tag("#EXT-X-MEDIA:"), - key_value_pairs, - ), - |(_, media)| AlternativeMedia::from_hashmap(media), - )(i) + map(pair(tag("#EXT-X-MEDIA:"), key_value_pairs), |(_, media)| { + AlternativeMedia::from_hashmap(media) + })(i) } pub fn session_data_tag(i: &[u8]) -> IResult<&[u8], SessionData> { map_res( - pair( - tag("#EXT-X-SESSION-DATA:"), - key_value_pairs, - ), + pair(tag("#EXT-X-SESSION-DATA:"), key_value_pairs), |(_, session_data)| SessionData::from_hashmap(session_data), )(i) } pub fn session_key_tag(i: &[u8]) -> IResult<&[u8], SessionKey> { - map( - pair( - tag("#EXT-X-SESSION-KEY:"), - key, - ), - |(_, key)| SessionKey(key), - )(i) + map(pair(tag("#EXT-X-SESSION-KEY:"), key), |(_, key)| { + SessionKey(key) + })(i) } // ----------------------------------------------------------------------------------------------- @@ -409,14 +387,10 @@ pub fn session_key_tag(i: &[u8]) -> IResult<&[u8], SessionKey> { pub fn parse_media_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec> { map( tuple(( - many0( - complete( - map( - pair(media_playlist_tag, multispace0), - |(tag, _)| tag, - ) - ) - ), + many0(complete(map( + pair(media_playlist_tag, multispace0), + |(tag, _)| tag, + ))), opt(eof), )), |(tags, _)| { @@ -452,46 +426,29 @@ pub fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { map(m3u_tag, MediaPlaylistTag::M3U), map(version_tag, MediaPlaylistTag::Version), map( - pair( - tag("#EXT-X-TARGETDURATION:"), - float, - ), + pair(tag("#EXT-X-TARGETDURATION:"), float), |(_, duration)| MediaPlaylistTag::TargetDuration(duration), ), map( - pair( - tag("#EXT-X-MEDIA-SEQUENCE:"), - number, - ), + pair(tag("#EXT-X-MEDIA-SEQUENCE:"), number), |(_, sequence)| MediaPlaylistTag::MediaSequence(sequence), ), map( - pair( - tag("#EXT-X-DISCONTINUITY-SEQUENCE:"), - number, - ), + pair(tag("#EXT-X-DISCONTINUITY-SEQUENCE:"), number), |(_, sequence)| MediaPlaylistTag::DiscontinuitySequence(sequence), ), map( - pair( - tag("#EXT-X-PLAYLIST-TYPE:"), - playlist_type, - ), + pair(tag("#EXT-X-PLAYLIST-TYPE:"), playlist_type), |(_, typ)| MediaPlaylistTag::PlaylistType(typ), ), - map( - tag("#EXT-X-I-FRAMES-ONLY"), - |_| MediaPlaylistTag::IFramesOnly, - ), + map(tag("#EXT-X-I-FRAMES-ONLY"), |_| { + MediaPlaylistTag::IFramesOnly + }), map(start_tag, MediaPlaylistTag::Start), - map( - tag("#EXT-X-INDEPENDENT-SEGMENTS"), - |_| MediaPlaylistTag::IndependentSegments, - ), - map( - tag("#EXT-X-ENDLIST"), - |_| MediaPlaylistTag::EndList, - ), + map(tag("#EXT-X-INDEPENDENT-SEGMENTS"), |_| { + MediaPlaylistTag::IndependentSegments + }), + map(tag("#EXT-X-ENDLIST"), |_| MediaPlaylistTag::EndList), map(media_segment_tag, MediaPlaylistTag::Segment), ))(i) } @@ -576,10 +533,7 @@ pub fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylis pub fn playlist_type(i: &[u8]) -> IResult<&[u8], MediaPlaylistType> { map_res( - tuple(( - map_res(is_not("\r\n"), str::from_utf8), - take(1usize), - )), + tuple((map_res(is_not("\r\n"), str::from_utf8), take(1usize))), |(typ, _)| MediaPlaylistType::from_str(typ), )(i) } @@ -606,49 +560,26 @@ pub enum SegmentTag { pub fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> { alt(( map( - pair( - tag("#EXTINF:"), - duration_title_tag, - ), + pair(tag("#EXTINF:"), duration_title_tag), |(_, (duration, title))| SegmentTag::Extinf(duration, title), ), map( - pair( - tag("#EXT-X-BYTERANGE:"), - byte_range_val, - ), + pair(tag("#EXT-X-BYTERANGE:"), byte_range_val), |(_, range)| SegmentTag::ByteRange(range), ), + map(tag("#EXT-X-DISCONTINUITY"), |_| SegmentTag::Discontinuity), + map(pair(tag("#EXT-X-KEY:"), key), |(_, key)| { + SegmentTag::Key(key) + }), + map(pair(tag("#EXT-X-MAP:"), extmap), |(_, map)| { + SegmentTag::Map(map) + }), map( - tag("#EXT-X-DISCONTINUITY"), - |_| SegmentTag::Discontinuity - ), - map( - pair( - tag("#EXT-X-KEY:"), - key, - ), - |(_, key)| SegmentTag::Key(key), - ), - map( - pair( - tag("#EXT-X-MAP:"), - extmap, - ), - |(_, map)| SegmentTag::Map(map), - ), - map( - pair( - tag("#EXT-X-PROGRAM-DATE-TIME:"), - consume_line, - ), + pair(tag("#EXT-X-PROGRAM-DATE-TIME:"), consume_line), |(_, line)| SegmentTag::ProgramDateTime(line), ), map( - pair( - tag("#EXT-X-DATE-RANGE:"), - consume_line, - ), + pair(tag("#EXT-X-DATE-RANGE:"), consume_line), |(_, line)| SegmentTag::DateRange(line), ), map(ext_tag, SegmentTag::Unknown), @@ -662,9 +593,7 @@ pub fn duration_title_tag(i: &[u8]) -> IResult<&[u8], (f32, Option)> { tuple(( float, opt(char(',')), - opt( - map_res(is_not("\r\n,"), from_utf8_slice), - ), + opt(map_res(is_not("\r\n,"), from_utf8_slice)), take(1usize), opt(char(',')), )), @@ -673,30 +602,22 @@ pub fn duration_title_tag(i: &[u8]) -> IResult<&[u8], (f32, Option)> { } pub fn key(i: &[u8]) -> IResult<&[u8], Key> { - map( - key_value_pairs, - Key::from_hashmap, - )(i) + map(key_value_pairs, Key::from_hashmap)(i) } pub fn extmap(i: &[u8]) -> IResult<&[u8], Map> { - map_res( - key_value_pairs, - |attrs| -> Result { - let uri = attrs.get("URI").cloned().unwrap_or_default(); - let byte_range = attrs.get("BYTERANGE").map(|range| - match byte_range_val(range.as_bytes()) { - IResult::Ok((_, range)) => Ok(range), - IResult::Err(_) => Err("invalid byte range"), - } - ).transpose()?; - - Ok(Map { - uri, - byte_range, + map_res(key_value_pairs, |attrs| -> Result { + let uri = attrs.get("URI").cloned().unwrap_or_default(); + let byte_range = attrs + .get("BYTERANGE") + .map(|range| match byte_range_val(range.as_bytes()) { + IResult::Ok((_, range)) => Ok(range), + IResult::Err(_) => Err("invalid byte range"), }) - } - )(i) + .transpose()?; + + Ok(Map { uri, byte_range }) + })(i) } // ----------------------------------------------------------------------------------------------- @@ -704,33 +625,20 @@ pub fn extmap(i: &[u8]) -> IResult<&[u8], Map> { // ----------------------------------------------------------------------------------------------- pub fn m3u_tag(i: &[u8]) -> IResult<&[u8], String> { - map_res( - tag("#EXTM3U"), - from_utf8_slice, - )(i) + map_res(tag("#EXTM3U"), from_utf8_slice)(i) } pub fn version_tag(i: &[u8]) -> IResult<&[u8], usize> { map( - pair( - tag("#EXT-X-VERSION:"), - map_res(digit1, str::from_utf8), - ), - |(_, version)| { - version.parse().unwrap_or_default() - } + pair(tag("#EXT-X-VERSION:"), map_res(digit1, str::from_utf8)), + |(_, version)| version.parse().unwrap_or_default(), )(i) } pub fn start_tag(i: &[u8]) -> IResult<&[u8], Start> { map( - pair( - tag("#EXT-X-START:"), - key_value_pairs, - ), - |(_, attributes)| { - Start::from_hashmap(attributes) - } + pair(tag("#EXT-X-START:"), key_value_pairs), + |(_, attributes)| Start::from_hashmap(attributes), )(i) } @@ -743,19 +651,14 @@ pub fn ext_tag(i: &[u8]) -> IResult<&[u8], ExtTag> { opt(map_res(is_not("\r\n"), from_utf8_slice)), take(1usize), )), - |(_, tag, _, rest, _)| { - ExtTag { tag, rest } - } + |(_, tag, _, rest, _)| ExtTag { tag, rest }, )(i) } pub fn comment_tag(i: &[u8]) -> IResult<&[u8], String> { map( pair( - preceded( - char('#'), - map_res(is_not("\r\n"), from_utf8_slice), - ), + preceded(char('#'), map_res(is_not("\r\n"), from_utf8_slice)), take(1usize), ), |(text, _)| text, @@ -773,7 +676,7 @@ pub fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap> { |mut acc: HashMap<_, _>, (left, right)| { acc.insert(left, right); acc - } + }, )(i) } @@ -786,70 +689,51 @@ pub fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, String)> { alt((quoted, unquoted)), opt(char(',')), )), - |(_, left, _, right, _)| { - (left, right) - } + |(_, left, _, right, _)| (left, right), )(i) } pub fn quoted(i: &[u8]) -> IResult<&[u8], String> { delimited( char('\"'), - map_res( - is_not("\""), - from_utf8_slice - ), - char('\"') + map_res(is_not("\""), from_utf8_slice), + char('\"'), )(i) } pub fn unquoted(i: &[u8]) -> IResult<&[u8], String> { - map_res( - is_not(",\r\n"), - from_utf8_slice - )(i) + map_res(is_not(",\r\n"), from_utf8_slice)(i) } pub fn consume_line(i: &[u8]) -> IResult<&[u8], String> { map( - pair( - map_res(not_line_ending, from_utf8_slice), - opt(line_ending), - ), + pair(map_res(not_line_ending, from_utf8_slice), opt(line_ending)), |(line, _)| line, )(i) } pub fn number(i: &[u8]) -> IResult<&[u8], i32> { - map_res(take_while1(is_digit), - |s| { - // Can't fail because we validated it above already - let s = str::from_utf8(s).unwrap(); - str::parse::(s) + map_res(take_while1(is_digit), |s| { + // Can't fail because we validated it above already + let s = str::from_utf8(s).unwrap(); + str::parse::(s) })(i) } pub fn byte_range_val(i: &[u8]) -> IResult<&[u8], ByteRange> { - map( - pair( - number, - opt( - preceded(char('@'), number) - ), - ), - |(n, o)| { - ByteRange { length: n, offset: o } + map(pair(number, opt(preceded(char('@'), number))), |(n, o)| { + ByteRange { + length: n, + offset: o, } - )(i) + })(i) } pub fn float(i: &[u8]) -> IResult<&[u8], f32> { map_res( pair( take_while1(is_digit), - opt( - preceded(char('.'), take_while1(is_digit)) - ), + opt(preceded(char('.'), take_while1(is_digit))), ), |(left, right): (&[u8], Option<&[u8]>)| match right { Some(right) => { @@ -863,7 +747,7 @@ pub fn float(i: &[u8]) -> IResult<&[u8], f32> { let left = str::from_utf8(left).unwrap(); left.parse() } - } + }, )(i) } From 65c295ee023db8c5bdb3b03575a50214be6fb9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Wed, 17 Nov 2021 19:14:58 +0200 Subject: [PATCH 04/13] Require each M3U8 playlist to start with the #EXTM3U8 tag The RFC requires this to be the very first line of every master/media playlist, and without this we would be parsing arbitrary text files as playlist without erroring out. See https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.1.1 Fixes https://github.com/rutgersc/m3u8-rs/issues/27 --- .../media-playlist-with-cues.m3u8 | 1 + .../media-playlist-with-discontinuity.m3u8 | 4 +--- src/parser.rs | 24 ++++++++++++------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/sample-playlists/media-playlist-with-cues.m3u8 b/sample-playlists/media-playlist-with-cues.m3u8 index b20902b..d47af77 100644 --- a/sample-playlists/media-playlist-with-cues.m3u8 +++ b/sample-playlists/media-playlist-with-cues.m3u8 @@ -1,3 +1,4 @@ +#EXTM3U #EXTINF:10, http://media.example.com/fileSequence7796.ts #EXTINF:6, diff --git a/sample-playlists/media-playlist-with-discontinuity.m3u8 b/sample-playlists/media-playlist-with-discontinuity.m3u8 index a570537..21c6c1f 100644 --- a/sample-playlists/media-playlist-with-discontinuity.m3u8 +++ b/sample-playlists/media-playlist-with-discontinuity.m3u8 @@ -1,5 +1,3 @@ -# https://developer.apple.com/library/ios/technotes/tn2288/_index.html -# #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-VERSION:3 @@ -12,4 +10,4 @@ ad1.ts #EXTINF:10.0, movieA.ts #EXTINF:10.0, -movieB.ts \ No newline at end of file +movieB.ts diff --git a/src/parser.rs b/src/parser.rs index 4abd4e8..562b129 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -157,7 +157,13 @@ pub fn parse_playlist_res(input: &[u8]) -> Result IResult<&[u8], MasterPlaylist> { - map(parse_master_playlist_tags, master_playlist_from_tags)(input) + map( + pair( + complete(pair(m3u_tag, multispace0)), + parse_master_playlist_tags, + ), + |(_, tags)| master_playlist_from_tags(tags), + )(input) } /// Parse input as a master playlist @@ -173,7 +179,13 @@ pub fn parse_master_playlist_res( /// Parse input as a media playlist pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> { - map(parse_media_playlist_tags, media_playlist_from_tags)(input) + map( + pair( + complete(pair(m3u_tag, multispace0)), + parse_media_playlist_tags, + ), + |(_, tags)| media_playlist_from_tags(tags), + )(input) } /// Parse input as a media playlist @@ -271,7 +283,6 @@ pub fn parse_master_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec IResult<&[u8], MasterPlaylistTag> { peek(take(1usize))(i)?; alt(( - map(m3u_tag, MasterPlaylistTag::M3U), map(version_tag, MasterPlaylistTag::Version), map(variant_stream_tag, MasterPlaylistTag::VariantStream), map(variant_i_frame_stream_tag, MasterPlaylistTag::VariantStream), @@ -404,7 +414,6 @@ pub fn parse_media_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec IResult<&[u8], MediaPlaylistTag> { peek(take(1usize))(i)?; alt(( - map(m3u_tag, MediaPlaylistTag::M3U), map(version_tag, MediaPlaylistTag::Version), map( pair(tag("#EXT-X-TARGETDURATION:"), float), @@ -624,8 +632,8 @@ pub fn extmap(i: &[u8]) -> IResult<&[u8], Map> { // Basic tags // ----------------------------------------------------------------------------------------------- -pub fn m3u_tag(i: &[u8]) -> IResult<&[u8], String> { - map_res(tag("#EXTM3U"), from_utf8_slice)(i) +pub fn m3u_tag(i: &[u8]) -> IResult<&[u8], ()> { + map(tag("#EXTM3U"), |_| ())(i) } pub fn version_tag(i: &[u8]) -> IResult<&[u8], usize> { From 4e6ac58d0cbd35b9dd2c36bea30198d6a5048d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Wed, 17 Nov 2021 19:22:28 +0200 Subject: [PATCH 05/13] Add tests for parsing non-playlist text and binary data These should fail to parse (and not panic), but previously the non-playlist text succeeded. --- tests/lib.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/lib.rs b/tests/lib.rs index 02982bd..c67b062 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -501,3 +501,29 @@ fn parsing_write_to_should_produce_the_same_structure() { ); } } + +// Failure on arbitrary text files that don't start with #EXTM3U8 + +#[test] +fn parsing_text_file_should_fail() { + let s = " +Lorem ipsum dolor sit amet, consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris +nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in +reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia +deserunt mollit anim id est laborum. + "; + let res = parse_master_playlist_res(s.as_bytes()); + + assert!(res.is_err()); +} + +#[test] +fn parsing_binary_data_should_fail_cleanly() { + let data = (0..1024).map(|i| (i % 255) as u8).collect::>(); + let res = parse_master_playlist_res(&data); + + assert!(res.is_err()); +} From 5500166f7409a45cf909b26b8221970e6de11a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Wed, 17 Nov 2021 19:32:24 +0200 Subject: [PATCH 06/13] Fix confusing #[path] usage and re-exports in lib.rs This has effectively the same behaviour now with fewer lines, less confusion and fewer compiler warnings about unused code. --- src/lib.rs | 2 -- src/parser.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8bad94a..609d710 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,5 @@ -#[path = "playlist.rs"] pub mod playlist; -#[path = "parser.rs"] #[cfg(feature = "parser")] mod parser; diff --git a/src/parser.rs b/src/parser.rs index 562b129..3439c4e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -74,8 +74,6 @@ //! //! ``` -pub mod playlist; - use nom::branch::alt; use nom::bytes::complete::{is_a, is_not, tag, take, take_until, take_while1}; use nom::character::complete::{ From a5d8358379457b0a9c91602dd0c1a7cfa8ab44f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Thu, 18 Nov 2021 14:49:50 +0200 Subject: [PATCH 07/13] Make most internal parser functions private And move parser internals tests into a test submodule of the parser. Also add actual assertions to various tests so they test something. --- src/parser.rs | 290 ++++++++++++++++++++++++++++++++++++++++++-------- tests/lib.rs | 152 -------------------------- 2 files changed, 247 insertions(+), 195 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 3439c4e..959eb8d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -145,11 +145,11 @@ pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> { /// Err(e) => println!("Error: {:?}", e) /// } /// ``` -pub fn parse_playlist_res(input: &[u8]) -> Result> { +pub fn parse_playlist_res(input: &[u8]) -> Result>> { let parse_result = parse_playlist(input); match parse_result { IResult::Ok((_, playlist)) => Ok(playlist), - _ => Err(parse_result), + IResult::Err(err) => Err(err), } } @@ -167,11 +167,11 @@ pub fn parse_master_playlist(input: &[u8]) -> IResult<&[u8], MasterPlaylist> { /// Parse input as a master playlist pub fn parse_master_playlist_res( input: &[u8], -) -> Result> { +) -> Result>> { let parse_result = parse_master_playlist(input); match parse_result { IResult::Ok((_, playlist)) => Ok(playlist), - _ => Err(parse_result), + IResult::Err(err) => Err(err), } } @@ -189,11 +189,11 @@ pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> { /// Parse input as a media playlist pub fn parse_media_playlist_res( input: &[u8], -) -> Result> { +) -> Result>> { let parse_result = parse_media_playlist(input); match parse_result { IResult::Ok((_, playlist)) => Ok(playlist), - _ => Err(parse_result), + IResult::Err(err) => Err(err), } } @@ -210,7 +210,7 @@ pub fn is_master_playlist(input: &[u8]) -> bool { /// - None: Unkown tag or empty line /// - Some(true, tagstring): Line contains a master playlist tag /// - Some(false, tagstring): Line contains a media playlist tag -pub fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> { +fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> { let mut is_master_opt = None; let mut current_input: &[u8] = input; @@ -227,7 +227,7 @@ pub fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> { is_master_opt } -pub fn is_master_playlist_tag_line(i: &[u8]) -> IResult<&[u8], Option<(bool, String)>> { +fn is_master_playlist_tag_line(i: &[u8]) -> IResult<&[u8], Option<(bool, String)>> { map( tuple(( opt(is_a("\r\n")), @@ -261,7 +261,7 @@ pub fn is_master_playlist_tag_line(i: &[u8]) -> IResult<&[u8], Option<(bool, Str // Master Playlist Tags // ----------------------------------------------------------------------------------------------- -pub fn parse_master_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec> { +fn parse_master_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec> { map( tuple(( many0(complete(map( @@ -280,7 +280,7 @@ pub fn parse_master_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec IResult<&[u8], MasterPlaylistTag> { +fn master_playlist_tag(i: &[u8]) -> IResult<&[u8], MasterPlaylistTag> { // Don't accept empty inputs here peek(take(1usize))(i)?; @@ -314,7 +314,7 @@ pub fn master_playlist_tag(i: &[u8]) -> IResult<&[u8], MasterPlaylistTag> { ))(i) } -pub fn master_playlist_from_tags(mut tags: Vec) -> MasterPlaylist { +fn master_playlist_from_tags(mut tags: Vec) -> MasterPlaylist { let mut master_playlist = MasterPlaylist::default(); while let Some(tag) = tags.pop() { @@ -355,34 +355,34 @@ pub fn master_playlist_from_tags(mut tags: Vec) -> MasterPlay master_playlist } -pub fn variant_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { +fn variant_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { map( pair(tag("#EXT-X-STREAM-INF:"), key_value_pairs), |(_, attributes)| VariantStream::from_hashmap(attributes, false), )(i) } -pub fn variant_i_frame_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { +fn variant_i_frame_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { map( pair(tag("#EXT-X-I-FRAME-STREAM-INF:"), key_value_pairs), |(_, attributes)| VariantStream::from_hashmap(attributes, true), )(i) } -pub fn alternative_media_tag(i: &[u8]) -> IResult<&[u8], AlternativeMedia> { +fn alternative_media_tag(i: &[u8]) -> IResult<&[u8], AlternativeMedia> { map(pair(tag("#EXT-X-MEDIA:"), key_value_pairs), |(_, media)| { AlternativeMedia::from_hashmap(media) })(i) } -pub fn session_data_tag(i: &[u8]) -> IResult<&[u8], SessionData> { +fn session_data_tag(i: &[u8]) -> IResult<&[u8], SessionData> { map_res( pair(tag("#EXT-X-SESSION-DATA:"), key_value_pairs), |(_, session_data)| SessionData::from_hashmap(session_data), )(i) } -pub fn session_key_tag(i: &[u8]) -> IResult<&[u8], SessionKey> { +fn session_key_tag(i: &[u8]) -> IResult<&[u8], SessionKey> { map(pair(tag("#EXT-X-SESSION-KEY:"), key), |(_, key)| { SessionKey(key) })(i) @@ -392,7 +392,7 @@ pub fn session_key_tag(i: &[u8]) -> IResult<&[u8], SessionKey> { // Media Playlist // ----------------------------------------------------------------------------------------------- -pub fn parse_media_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec> { +fn parse_media_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec> { map( tuple(( many0(complete(map( @@ -411,7 +411,7 @@ pub fn parse_media_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec IResult<&[u8], MediaPlaylistTag> { +fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { // Don't accept empty inputs here peek(take(1usize))(i)?; @@ -459,7 +458,7 @@ pub fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { ))(i) } -pub fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylist { +fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylist { let mut media_playlist = MediaPlaylist::default(); let mut next_segment = MediaSegment::empty(); let mut encryption_key = None; @@ -531,13 +530,12 @@ pub fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylis } _ => (), }, - _ => (), } } media_playlist } -pub fn playlist_type(i: &[u8]) -> IResult<&[u8], MediaPlaylistType> { +fn playlist_type(i: &[u8]) -> IResult<&[u8], MediaPlaylistType> { map_res( tuple((map_res(is_not("\r\n"), str::from_utf8), take(1usize))), |(typ, _)| MediaPlaylistType::from_str(typ), @@ -550,7 +548,7 @@ pub fn playlist_type(i: &[u8]) -> IResult<&[u8], MediaPlaylistType> { /// All possible media segment tags. #[derive(Debug)] -pub enum SegmentTag { +enum SegmentTag { Extinf(f32, Option), ByteRange(ByteRange), Discontinuity, @@ -563,7 +561,7 @@ pub enum SegmentTag { Uri(String), } -pub fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> { +fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> { alt(( map( pair(tag("#EXTINF:"), duration_title_tag), @@ -594,7 +592,7 @@ pub fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> { ))(i) } -pub fn duration_title_tag(i: &[u8]) -> IResult<&[u8], (f32, Option)> { +fn duration_title_tag(i: &[u8]) -> IResult<&[u8], (f32, Option)> { map( tuple(( float, @@ -607,11 +605,11 @@ pub fn duration_title_tag(i: &[u8]) -> IResult<&[u8], (f32, Option)> { )(i) } -pub fn key(i: &[u8]) -> IResult<&[u8], Key> { +fn key(i: &[u8]) -> IResult<&[u8], Key> { map(key_value_pairs, Key::from_hashmap)(i) } -pub fn extmap(i: &[u8]) -> IResult<&[u8], Map> { +fn extmap(i: &[u8]) -> IResult<&[u8], Map> { map_res(key_value_pairs, |attrs| -> Result { let uri = attrs.get("URI").cloned().unwrap_or_default(); let byte_range = attrs @@ -630,25 +628,25 @@ pub fn extmap(i: &[u8]) -> IResult<&[u8], Map> { // Basic tags // ----------------------------------------------------------------------------------------------- -pub fn m3u_tag(i: &[u8]) -> IResult<&[u8], ()> { +fn m3u_tag(i: &[u8]) -> IResult<&[u8], ()> { map(tag("#EXTM3U"), |_| ())(i) } -pub fn version_tag(i: &[u8]) -> IResult<&[u8], usize> { +fn version_tag(i: &[u8]) -> IResult<&[u8], usize> { map( pair(tag("#EXT-X-VERSION:"), map_res(digit1, str::from_utf8)), |(_, version)| version.parse().unwrap_or_default(), )(i) } -pub fn start_tag(i: &[u8]) -> IResult<&[u8], Start> { +fn start_tag(i: &[u8]) -> IResult<&[u8], Start> { map( pair(tag("#EXT-X-START:"), key_value_pairs), |(_, attributes)| Start::from_hashmap(attributes), )(i) } -pub fn ext_tag(i: &[u8]) -> IResult<&[u8], ExtTag> { +fn ext_tag(i: &[u8]) -> IResult<&[u8], ExtTag> { map( tuple(( tag("#EXT-"), @@ -661,7 +659,7 @@ pub fn ext_tag(i: &[u8]) -> IResult<&[u8], ExtTag> { )(i) } -pub fn comment_tag(i: &[u8]) -> IResult<&[u8], String> { +fn comment_tag(i: &[u8]) -> IResult<&[u8], String> { map( pair( preceded(char('#'), map_res(is_not("\r\n"), from_utf8_slice)), @@ -675,7 +673,7 @@ pub fn comment_tag(i: &[u8]) -> IResult<&[u8], String> { // Util // ----------------------------------------------------------------------------------------------- -pub fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap> { +fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap> { fold_many0( preceded(space0, key_value_pair), HashMap::new, @@ -686,7 +684,7 @@ pub fn key_value_pairs(i: &[u8]) -> IResult<&[u8], HashMap> { )(i) } -pub fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, String)> { +fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, String)> { map( tuple(( peek(none_of("\r\n")), @@ -699,7 +697,7 @@ pub fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, String)> { )(i) } -pub fn quoted(i: &[u8]) -> IResult<&[u8], String> { +fn quoted(i: &[u8]) -> IResult<&[u8], String> { delimited( char('\"'), map_res(is_not("\""), from_utf8_slice), @@ -707,18 +705,18 @@ pub fn quoted(i: &[u8]) -> IResult<&[u8], String> { )(i) } -pub fn unquoted(i: &[u8]) -> IResult<&[u8], String> { +fn unquoted(i: &[u8]) -> IResult<&[u8], String> { map_res(is_not(",\r\n"), from_utf8_slice)(i) } -pub fn consume_line(i: &[u8]) -> IResult<&[u8], String> { +fn consume_line(i: &[u8]) -> IResult<&[u8], String> { map( pair(map_res(not_line_ending, from_utf8_slice), opt(line_ending)), |(line, _)| line, )(i) } -pub fn number(i: &[u8]) -> IResult<&[u8], i32> { +fn number(i: &[u8]) -> IResult<&[u8], i32> { map_res(take_while1(is_digit), |s| { // Can't fail because we validated it above already let s = str::from_utf8(s).unwrap(); @@ -726,7 +724,7 @@ pub fn number(i: &[u8]) -> IResult<&[u8], i32> { })(i) } -pub fn byte_range_val(i: &[u8]) -> IResult<&[u8], ByteRange> { +fn byte_range_val(i: &[u8]) -> IResult<&[u8], ByteRange> { map(pair(number, opt(preceded(char('@'), number))), |(n, o)| { ByteRange { length: n, @@ -735,7 +733,7 @@ pub fn byte_range_val(i: &[u8]) -> IResult<&[u8], ByteRange> { })(i) } -pub fn float(i: &[u8]) -> IResult<&[u8], f32> { +fn float(i: &[u8]) -> IResult<&[u8], f32> { map_res( pair( take_while1(is_digit), @@ -757,6 +755,212 @@ pub fn float(i: &[u8]) -> IResult<&[u8], f32> { )(i) } -pub fn from_utf8_slice(s: &[u8]) -> Result { +fn from_utf8_slice(s: &[u8]) -> Result { String::from_utf8(s.to_vec()) } + +#[cfg(test)] +mod tests { + use super::*; + use nom::AsBytes; + + // ----------------------------------------------------------------------------------------------- + // Variant + + #[test] + fn variant_stream() { + let input = b"#EXT-X-STREAM-INF:BANDWIDTH=300000,CODECS=\"xxx\"\n"; + assert_eq!( + variant_stream_tag(input), + Result::Ok(( + "\n".as_bytes(), + VariantStream { + is_i_frame: false, + uri: "".into(), + bandwidth: "300000".into(), + average_bandwidth: None, + codecs: Some("xxx".into()), + resolution: None, + frame_rate: None, + hdcp_level: None, + audio: None, + video: None, + subtitles: None, + closed_captions: None, + } + )) + ); + } + + // ----------------------------------------------------------------------------------------------- + // Other + + #[test] + fn test_key_value_pairs_trailing_equals() { + assert_eq!( + key_value_pairs(b"BANDWIDTH=395000,CODECS=\"avc1.4d001f,mp4a.40.2\"\r\nrest="), + Result::Ok(( + "\r\nrest=".as_bytes(), + vec![("BANDWIDTH", "395000"), ("CODECS", "avc1.4d001f,mp4a.40.2")] + .into_iter() + .map(|(k, v)| (String::from(k), String::from(v))) + .collect::>(), + )), + ); + } + + #[test] + fn test_key_value_pairs_multiple_quoted_values() { + assert_eq!( + key_value_pairs(b"BANDWIDTH=86000,URI=\"low/iframe.m3u8\",PROGRAM-ID=1,RESOLUTION=\"1x1\",VIDEO=1\nrest"), + Result::Ok(( + "\nrest".as_bytes(), + vec![ + ("BANDWIDTH", "86000"), + ("URI", "low/iframe.m3u8"), + ("PROGRAM-ID", "1"), + ("RESOLUTION", "1x1"), + ("VIDEO", "1") + ].into_iter() + .map(|(k, v)| (String::from(k), String::from(v))) + .collect::>() + )) + ); + } + + #[test] + fn test_key_value_pairs_quotes() { + assert_eq!( + key_value_pairs(b"BANDWIDTH=300000,CODECS=\"avc1.42c015,mp4a.40.2\"\r\nrest"), + Result::Ok(( + "\r\nrest".as_bytes(), + vec![("BANDWIDTH", "300000"), ("CODECS", "avc1.42c015,mp4a.40.2")] + .into_iter() + .map(|(k, v)| (String::from(k), String::from(v))) + .collect::>() + )) + ); + } + + #[test] + fn test_key_value_pairs() { + assert_eq!( + key_value_pairs(b"BANDWIDTH=300000,RESOLUTION=22x22,VIDEO=1\r\nrest="), + Result::Ok(( + "\r\nrest=".as_bytes(), + vec![ + ("BANDWIDTH", "300000"), + ("RESOLUTION", "22x22"), + ("VIDEO", "1") + ] + .into_iter() + .map(|(k, v)| (String::from(k), String::from(v))) + .collect::>() + )) + ); + } + + #[test] + fn test_key_value_pair() { + assert_eq!( + key_value_pair(b"PROGRAM-ID=1,rest"), + Result::Ok(( + "rest".as_bytes(), + ("PROGRAM-ID".to_string(), "1".to_string()) + )) + ); + } + + #[test] + fn ext_with_value() { + assert_eq!( + ext_tag(b"#EXT-X-CUE-OUT:DURATION=30\nxxx"), + Result::Ok(( + b"xxx".as_bytes(), + ExtTag { + tag: "X-CUE-OUT".into(), + rest: Some("DURATION=30".into()) + } + )) + ); + } + + #[test] + fn ext_without_value() { + assert_eq!( + ext_tag(b"#EXT-X-CUE-IN\nxxx"), + Result::Ok(( + b"xxx".as_bytes(), + ExtTag { + tag: "X-CUE-IN".into(), + rest: None + } + )) + ); + } + + #[test] + fn comment() { + assert_eq!( + comment_tag(b"#Hello\nxxx"), + Result::Ok(("xxx".as_bytes(), "Hello".to_string())) + ); + } + + #[test] + fn quotes() { + assert_eq!( + quoted(b"\"value\"rest"), + Result::Ok(("rest".as_bytes(), "value".to_string())) + ); + } + + #[test] + fn consume_line_empty() { + let expected = Result::Ok(("rest".as_bytes(), "".to_string())); + let actual = consume_line(b"\r\nrest"); + assert_eq!(expected, actual); + } + + #[test] + fn consume_line_n() { + assert_eq!( + consume_line(b"before\nrest"), + Result::Ok(("rest".as_bytes(), "before".into())) + ); + } + + #[test] + fn consume_line_rn() { + assert_eq!( + consume_line(b"before\r\nrest"), + Result::Ok(("rest".as_bytes(), "before".into())) + ); + } + + #[test] + fn float_() { + assert_eq!( + float(b"33.22rest"), + Result::Ok(("rest".as_bytes(), 33.22f32)) + ); + } + + #[test] + fn float_no_decimal() { + assert_eq!(float(b"33rest"), Result::Ok(("rest".as_bytes(), 33f32))); + } + + #[test] + fn float_should_ignore_trailing_dot() { + assert_eq!(float(b"33.rest"), Result::Ok((".rest".as_bytes(), 33f32))); + } + + #[test] + fn parse_duration_title() { + assert_eq!( + duration_title_tag(b"2.002,title\nrest"), + Result::Ok(("rest".as_bytes(), (2.002f32, Some("title".to_string())))) + ); + } +} diff --git a/tests/lib.rs b/tests/lib.rs index c67b062..1d42a27 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -163,158 +163,6 @@ fn playlist_types() { } } -// ----------------------------------------------------------------------------------------------- -// Variant - -#[test] -fn variant_stream() { - let input = b"#EXT-X-STREAM-INF:BANDWIDTH=300000,CODECS=\"xxx\"\n"; - let result = variant_stream_tag(input); - println!("{:?}", result); -} - -// ----------------------------------------------------------------------------------------------- -// Other - -#[test] -fn test_key_value_pairs_trailing_equals() { - let res = key_value_pairs(b"BANDWIDTH=395000,CODECS=\"avc1.4d001f,mp4a.40.2\"\r\nrest="); - println!("{:?}\n\n", res); -} - -#[test] -fn test_key_value_pairs_multiple_quoted_values() { - assert_eq!( - key_value_pairs(b"BANDWIDTH=86000,URI=\"low/iframe.m3u8\",PROGRAM-ID=1,RESOLUTION=\"1x1\",VIDEO=1\nrest"), - Result::Ok(( - "\nrest".as_bytes(), - vec![ - ("BANDWIDTH".to_string(), "86000".to_string()), - ("URI".to_string(), "low/iframe.m3u8".to_string()), - ("PROGRAM-ID".to_string(), "1".to_string()), - ("RESOLUTION".to_string(), "1x1".to_string()), - ("VIDEO".to_string(), "1".to_string()) - ].into_iter().collect::>() - )) - ); -} - -#[test] -fn test_key_value_pairs_quotes() { - let res = key_value_pairs(b"BANDWIDTH=300000,CODECS=\"avc1.42c015,mp4a.40.2\"\r\nrest"); - println!("{:?}\n\n", res); -} - -#[test] -fn test_key_value_pairs() { - let res = key_value_pairs(b"BANDWIDTH=300000,RESOLUTION=22x22,VIDEO=1\r\nrest="); - println!("{:?}\n\n", res); -} - -#[test] -fn test_key_value_pair() { - assert_eq!( - key_value_pair(b"PROGRAM-ID=1,rest"), - Result::Ok(( - "rest".as_bytes(), - ("PROGRAM-ID".to_string(), "1".to_string()) - )) - ); -} - -#[test] -fn ext_with_value() { - assert_eq!( - ext_tag(b"#EXT-X-CUE-OUT:DURATION=30\nxxx"), - Result::Ok(( - b"xxx".as_bytes(), - ExtTag { - tag: "X-CUE-OUT".into(), - rest: Some("DURATION=30".into()) - } - )) - ); -} - -#[test] -fn ext_without_value() { - assert_eq!( - ext_tag(b"#EXT-X-CUE-IN\nxxx"), - Result::Ok(( - b"xxx".as_bytes(), - ExtTag { - tag: "X-CUE-IN".into(), - rest: None - } - )) - ); -} - -#[test] -fn comment() { - assert_eq!( - comment_tag(b"#Hello\nxxx"), - Result::Ok(("xxx".as_bytes(), "Hello".to_string())) - ); -} - -#[test] -fn quotes() { - assert_eq!( - quoted(b"\"value\"rest"), - Result::Ok(("rest".as_bytes(), "value".to_string())) - ); -} - -#[test] -fn consume_line_empty() { - let expected = Result::Ok(("rest".as_bytes(), "".to_string())); - let actual = consume_line(b"\r\nrest"); - assert_eq!(expected, actual); -} - -#[test] -fn consume_line_n() { - assert_eq!( - consume_line(b"before\nrest"), - Result::Ok(("rest".as_bytes(), "before".into())) - ); -} - -#[test] -fn consume_line_rn() { - assert_eq!( - consume_line(b"before\r\nrest"), - Result::Ok(("rest".as_bytes(), "before".into())) - ); -} - -#[test] -fn float_() { - assert_eq!( - float(b"33.22rest"), - Result::Ok(("rest".as_bytes(), 33.22f32)) - ); -} - -#[test] -fn float_no_decimal() { - assert_eq!(float(b"33rest"), Result::Ok(("rest".as_bytes(), 33f32))); -} - -#[test] -fn float_should_ignore_trailing_dot() { - assert_eq!(float(b"33.rest"), Result::Ok((".rest".as_bytes(), 33f32))); -} - -#[test] -fn parse_duration_title() { - assert_eq!( - duration_title_tag(b"2.002,title\nrest"), - Result::Ok(("rest".as_bytes(), (2.002f32, Some("title".to_string())))) - ); -} - // ----------------------------------------------------------------------------------------------- // Creating playlists From 336f11e1ba4a51e0814fdb8db029b2d4b28c8cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Thu, 18 Nov 2021 14:52:03 +0200 Subject: [PATCH 08/13] Remove useless `fn main()` from documentation examples --- src/parser.rs | 74 +++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 959eb8d..c3acae9 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -10,16 +10,14 @@ //! use nom::IResult; //! use std::io::Read; //! -//! fn main() { -//! let mut file = std::fs::File::open("playlist.m3u8").unwrap(); -//! let mut bytes: Vec = Vec::new(); -//! file.read_to_end(&mut bytes).unwrap(); +//! let mut file = std::fs::File::open("playlist.m3u8").unwrap(); +//! let mut bytes: Vec = Vec::new(); +//! file.read_to_end(&mut bytes).unwrap(); //! -//! match m3u8_rs::parse_playlist(&bytes) { -//! Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl), -//! Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl), -//! Result::Err(e) => panic!("Parsing error: \n{}", e), -//! } +//! match m3u8_rs::parse_playlist(&bytes) { +//! Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl), +//! Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl), +//! Result::Err(e) => panic!("Parsing error: \n{}", e), //! } //! ``` //! @@ -29,16 +27,13 @@ //! use std::io::Read; //! use nom::IResult; //! -//! fn main() { -//! let mut file = std::fs::File::open("masterplaylist.m3u8").unwrap(); -//! let mut bytes: Vec = Vec::new(); -//! file.read_to_end(&mut bytes).unwrap(); +//! let mut file = std::fs::File::open("masterplaylist.m3u8").unwrap(); +//! let mut bytes: Vec = Vec::new(); +//! file.read_to_end(&mut bytes).unwrap(); //! -//! if let Result::Ok((_, pl)) = m3u8_rs::parse_master_playlist(&bytes) { -//! println!("{:?}", pl); -//! } +//! if let Result::Ok((_, pl)) = m3u8_rs::parse_master_playlist(&bytes) { +//! println!("{:?}", pl); //! } -//! //! ``` //! //! Creating a playlist and writing it back to a vec/file @@ -46,32 +41,29 @@ //! ``` //! use m3u8_rs::playlist::{MediaPlaylist, MediaPlaylistType, MediaSegment}; //! -//! fn main() { -//! let playlist = MediaPlaylist { -//! version: 6, -//! target_duration: 3.0, -//! media_sequence: 338559, -//! discontinuity_sequence: 1234, -//! end_list: true, -//! playlist_type: Some(MediaPlaylistType::Vod), -//! segments: vec![ -//! MediaSegment { -//! uri: "20140311T113819-01-338559live.ts".into(), -//! duration: 2.002, -//! title: Some("title".into()), -//! ..Default::default() -//! }, -//! ], -//! ..Default::default() -//! }; +//! let playlist = MediaPlaylist { +//! version: 6, +//! target_duration: 3.0, +//! media_sequence: 338559, +//! discontinuity_sequence: 1234, +//! end_list: true, +//! playlist_type: Some(MediaPlaylistType::Vod), +//! segments: vec![ +//! MediaSegment { +//! uri: "20140311T113819-01-338559live.ts".into(), +//! duration: 2.002, +//! title: Some("title".into()), +//! ..Default::default() +//! }, +//! ], +//! ..Default::default() +//! }; //! -//! //let mut v: Vec = Vec::new(); -//! //playlist.write_to(&mut v).unwrap(); -//! -//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap(); -//! //playlist.write_to(&mut file).unwrap(); -//! } +//! //let mut v: Vec = Vec::new(); +//! //playlist.write_to(&mut v).unwrap(); //! +//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap(); +//! //playlist.write_to(&mut file).unwrap(); //! ``` use nom::branch::alt; From 7e62854e20b2b2708c8594456a523926a483fc10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Thu, 18 Nov 2021 14:52:38 +0200 Subject: [PATCH 09/13] Use `unwrap_or_default()` instead of `unwrap_or_else(Default::default)` --- src/playlist.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playlist.rs b/src/playlist.rs index 325efd1..3918936 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -229,7 +229,7 @@ impl AlternativeMedia { media_type: attrs .get("TYPE") .and_then(|s| AlternativeMediaType::from_str(s).ok()) - .unwrap_or_else(Default::default), + .unwrap_or_default(), uri: attrs.remove("URI"), group_id: attrs.remove("GROUP-ID").unwrap_or_else(String::new), language: attrs.remove("LANGUAGE"), From 3edf5d1c0f5e73a42fad6921c9e38aaaf1bb9643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Thu, 18 Nov 2021 14:54:46 +0200 Subject: [PATCH 10/13] Fix various minor clippy warnings --- examples/with_nom_result.rs | 7 ++++--- src/parser.rs | 1 + tests/lib.rs | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/with_nom_result.rs b/examples/with_nom_result.rs index b795364..ab266ee 100644 --- a/examples/with_nom_result.rs +++ b/examples/with_nom_result.rs @@ -9,7 +9,7 @@ fn main() { let parsed = m3u8_rs::parse_playlist(&bytes); let playlist = match parsed { - Result::Ok((i, playlist)) => playlist, + Result::Ok((_i, playlist)) => playlist, Result::Err(e) => panic!("Parsing error: \n{}", e), }; @@ -19,6 +19,7 @@ fn main() { } } +#[allow(unused)] fn main_alt() { let mut file = std::fs::File::open("playlist.m3u8").unwrap(); let mut bytes: Vec = Vec::new(); @@ -27,8 +28,8 @@ fn main_alt() { let parsed = m3u8_rs::parse_playlist(&bytes); match parsed { - Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl), - Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl), + Result::Ok((_i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl), + Result::Ok((_i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl), Result::Err(e) => panic!("Parsing error: \n{}", e), } } diff --git a/src/parser.rs b/src/parser.rs index c3acae9..e7f2a63 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -271,6 +271,7 @@ fn parse_master_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec } /// Contains all the tags required to parse a master playlist. +#[allow(clippy::large_enum_variant)] #[derive(Debug)] enum MasterPlaylistTag { Version(usize), diff --git a/tests/lib.rs b/tests/lib.rs index 1d42a27..3f244e1 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -21,7 +21,7 @@ fn all_sample_m3u_playlists() -> Vec { fn getm3u(path: &str) -> String { let mut buf = String::new(); - let mut file = fs::File::open(path).expect(&format!("Can't find m3u8: {}", path)); + let mut file = fs::File::open(path).unwrap_or_else(|_| panic!("Can't find m3u8: {}", path)); let u = file.read_to_string(&mut buf).expect("Can't read file"); buf } @@ -139,7 +139,7 @@ fn playlist_not_ending_in_newline_media() { fn playlist_type_is_master() { let input = get_sample_playlist("master.m3u8"); let result = is_master_playlist(input.as_bytes()); - assert_eq!(true, result); + assert!(result); } // #[test] From 51fcb70113dba9cd41303ec16ffcaf7a08b6fb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Thu, 18 Nov 2021 15:00:01 +0200 Subject: [PATCH 11/13] Re-export all types from the crate root and remove the playlist sub-module There's not much else in this crate and having it behind another module decreases visibility. --- examples/simple.rs | 2 +- examples/with_nom_result.rs | 2 +- src/lib.rs | 3 ++- tests/lib.rs | 1 - 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/simple.rs b/examples/simple.rs index 7d53fc2..d4d06d1 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,4 +1,4 @@ -use m3u8_rs::playlist::Playlist; +use m3u8_rs::Playlist; use std::io::Read; fn main() { diff --git a/examples/with_nom_result.rs b/examples/with_nom_result.rs index ab266ee..cb60a61 100644 --- a/examples/with_nom_result.rs +++ b/examples/with_nom_result.rs @@ -1,4 +1,4 @@ -use m3u8_rs::playlist::Playlist; +use m3u8_rs::Playlist; use std::io::Read; fn main() { diff --git a/src/lib.rs b/src/lib.rs index 609d710..f48840d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -pub mod playlist; +mod playlist; +pub use playlist::*; #[cfg(feature = "parser")] mod parser; diff --git a/tests/lib.rs b/tests/lib.rs index 3f244e1..3bfb798 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,6 +1,5 @@ #![allow(unused_variables, unused_imports, dead_code)] -use m3u8_rs::playlist::*; use m3u8_rs::*; use nom::AsBytes; use std::collections::HashMap; From 24328460644fd5bcd01d6231f0b6fb6051d18fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Thu, 18 Nov 2021 15:04:17 +0200 Subject: [PATCH 12/13] Move the crate docs to the root of the crate so they actually show up And also fix all the broken links while we're at it. --- src/lib.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++ src/parser.rs | 72 ++----------------------------------------------- src/playlist.rs | 43 ++++++++++------------------- 3 files changed, 83 insertions(+), 99 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f48840d..4d9b79e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,70 @@ +//! A library to parse m3u8 playlists [HTTP Live Streaming](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19). +//! +//! # Examples +//! +//! Parsing a playlist and let the parser figure out if it's a media or master playlist. +//! +//! ``` +//! use m3u8_rs::Playlist; +//! use nom::IResult; +//! use std::io::Read; +//! +//! let mut file = std::fs::File::open("playlist.m3u8").unwrap(); +//! let mut bytes: Vec = Vec::new(); +//! file.read_to_end(&mut bytes).unwrap(); +//! +//! match m3u8_rs::parse_playlist(&bytes) { +//! Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl), +//! Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl), +//! Result::Err(e) => panic!("Parsing error: \n{}", e), +//! } +//! ``` +//! +//! Parsing a master playlist directly +//! +//! ``` +//! use std::io::Read; +//! use nom::IResult; +//! +//! let mut file = std::fs::File::open("masterplaylist.m3u8").unwrap(); +//! let mut bytes: Vec = Vec::new(); +//! file.read_to_end(&mut bytes).unwrap(); +//! +//! if let Result::Ok((_, pl)) = m3u8_rs::parse_master_playlist(&bytes) { +//! println!("{:?}", pl); +//! } +//! ``` +//! +//! Creating a playlist and writing it back to a vec/file +//! +//! ``` +//! use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment}; +//! +//! let playlist = MediaPlaylist { +//! version: 6, +//! target_duration: 3.0, +//! media_sequence: 338559, +//! discontinuity_sequence: 1234, +//! end_list: true, +//! playlist_type: Some(MediaPlaylistType::Vod), +//! segments: vec![ +//! MediaSegment { +//! uri: "20140311T113819-01-338559live.ts".into(), +//! duration: 2.002, +//! title: Some("title".into()), +//! ..Default::default() +//! }, +//! ], +//! ..Default::default() +//! }; +//! +//! //let mut v: Vec = Vec::new(); +//! //playlist.write_to(&mut v).unwrap(); +//! +//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap(); +//! //playlist.write_to(&mut file).unwrap(); +//! ``` + mod playlist; pub use playlist::*; diff --git a/src/parser.rs b/src/parser.rs index e7f2a63..c877e4b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,71 +1,3 @@ -//! A library to parse m3u8 playlists (HTTP Live Streaming) [link] -//! (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19). -//! -//! # Examples -//! -//! Parsing a playlist and let the parser figure out if it's a media or master playlist. -//! -//! ``` -//! use m3u8_rs::playlist::Playlist; -//! use nom::IResult; -//! use std::io::Read; -//! -//! let mut file = std::fs::File::open("playlist.m3u8").unwrap(); -//! let mut bytes: Vec = Vec::new(); -//! file.read_to_end(&mut bytes).unwrap(); -//! -//! match m3u8_rs::parse_playlist(&bytes) { -//! Result::Ok((i, Playlist::MasterPlaylist(pl))) => println!("Master playlist:\n{:?}", pl), -//! Result::Ok((i, Playlist::MediaPlaylist(pl))) => println!("Media playlist:\n{:?}", pl), -//! Result::Err(e) => panic!("Parsing error: \n{}", e), -//! } -//! ``` -//! -//! Parsing a master playlist directly -//! -//! ``` -//! use std::io::Read; -//! use nom::IResult; -//! -//! let mut file = std::fs::File::open("masterplaylist.m3u8").unwrap(); -//! let mut bytes: Vec = Vec::new(); -//! file.read_to_end(&mut bytes).unwrap(); -//! -//! if let Result::Ok((_, pl)) = m3u8_rs::parse_master_playlist(&bytes) { -//! println!("{:?}", pl); -//! } -//! ``` -//! -//! Creating a playlist and writing it back to a vec/file -//! -//! ``` -//! use m3u8_rs::playlist::{MediaPlaylist, MediaPlaylistType, MediaSegment}; -//! -//! let playlist = MediaPlaylist { -//! version: 6, -//! target_duration: 3.0, -//! media_sequence: 338559, -//! discontinuity_sequence: 1234, -//! end_list: true, -//! playlist_type: Some(MediaPlaylistType::Vod), -//! segments: vec![ -//! MediaSegment { -//! uri: "20140311T113819-01-338559live.ts".into(), -//! duration: 2.002, -//! title: Some("title".into()), -//! ..Default::default() -//! }, -//! ], -//! ..Default::default() -//! }; -//! -//! //let mut v: Vec = Vec::new(); -//! //playlist.write_to(&mut v).unwrap(); -//! -//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap(); -//! //playlist.write_to(&mut file).unwrap(); -//! ``` - use nom::branch::alt; use nom::bytes::complete::{is_a, is_not, tag, take, take_until, take_while1}; use nom::character::complete::{ @@ -91,7 +23,7 @@ use std::string; /// /// ``` /// use std::io::Read; -/// use m3u8_rs::playlist::{Playlist}; +/// use m3u8_rs::Playlist; /// /// let mut file = std::fs::File::open("playlist.m3u8").unwrap(); /// let mut bytes: Vec = Vec::new(); @@ -122,7 +54,7 @@ pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> { /// # Examples /// /// ``` -/// use m3u8_rs::playlist::{Playlist}; +/// use m3u8_rs::Playlist; /// use std::io::Read; /// /// let mut file = std::fs::File::open("playlist.m3u8").unwrap(); diff --git a/src/playlist.rs b/src/playlist.rs index 3918936..5e02161 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -65,8 +65,7 @@ impl Playlist { // Master Playlist // ----------------------------------------------------------------------------------------------- -/// A [Master Playlist] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4) +/// A [Master Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4) /// provides a set of Variant Streams, each of which /// describes a different version of the same content. #[derive(Debug, Default, PartialEq, Clone)] @@ -116,11 +115,8 @@ impl MasterPlaylist { } } -/// [`#EXT-X-STREAM-INF: -/// `] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.2) -/// [`#EXT-X-I-FRAME-STREAM-INF:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.3) +/// [`#EXT-X-STREAM-INF: `](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.2) +/// [`#EXT-X-I-FRAME-STREAM-INF:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.3) /// /// A Variant Stream includes a Media Playlist that specifies media /// encoded at a particular bit rate, in a particular format, and at a @@ -197,8 +193,7 @@ impl VariantStream { } } -/// [`#EXT-X-MEDIA:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.1) +/// [`#EXT-X-MEDIA:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.1) /// /// The EXT-X-MEDIA tag is used to relate Media Playlists that contain /// alternative Renditions (Section 4.3.4.2.1) of the same content. For @@ -314,8 +309,7 @@ impl fmt::Display for AlternativeMediaType { } } -/// [`#EXT-X-SESSION-KEY:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5) +/// [`#EXT-X-SESSION-KEY:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5) /// The EXT-X-SESSION-KEY tag allows encryption keys from Media Playlists /// to be specified in a Master Playlist. This allows the client to /// preload these keys without having to read the Media Playlist(s) first. @@ -336,8 +330,7 @@ pub enum SessionDataField { Uri(String), } -/// [`#EXT-X-SESSION-DATA:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.4) +/// [`#EXT-X-SESSION-DATA:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.4) /// The EXT-X-SESSION-DATA tag allows arbitrary session data to be carried /// in a Master Playlist. #[derive(Debug, PartialEq, Clone)] @@ -399,8 +392,7 @@ impl SessionData { // Media Playlist // ----------------------------------------------------------------------------------------------- -/// A [Media Playlist] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3) +/// A [Media Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3) /// contains a list of Media Segments, which when played /// sequentially will play the multimedia presentation. #[derive(Debug, Default, PartialEq, Clone)] @@ -463,8 +455,7 @@ impl MediaPlaylist { } } -/// [`#EXT-X-PLAYLIST-TYPE:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5) +/// [`#EXT-X-PLAYLIST-TYPE:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5) #[derive(Debug, PartialEq, Clone)] pub enum MediaPlaylistType { Event, @@ -577,8 +568,7 @@ impl MediaSegment { } } -/// [`#EXT-X-KEY:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.4) +/// [`#EXT-X-KEY:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.4) /// /// Media Segments MAY be encrypted. The EXT-X-KEY tag specifies how to /// decrypt them. It applies to every Media Segment that appears between @@ -615,12 +605,10 @@ impl Key { } } -/// [`#EXT-X-MAP:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.5) +/// [`#EXT-X-MAP:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.5) /// /// The EXT-X-MAP tag specifies how to obtain the Media Initialization Section -/// [(Section 3)] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-3) +/// [(Section 3)](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-3) /// required to parse the applicable Media Segments. /// It applies to every Media Segment that appears after it in the /// Playlist until the next EXT-X-MAP tag or until the end of the @@ -642,8 +630,7 @@ impl Map { } } -/// [`#EXT-X-BYTERANGE:[@]`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.2) +/// [`#EXT-X-BYTERANGE:[@]`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.2) /// /// The EXT-X-BYTERANGE tag indicates that a Media Segment is a sub-range /// of the resource identified by its URI. It applies only to the next @@ -664,8 +651,7 @@ impl ByteRange { } } -/// [`#EXT-X-DATERANGE:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.7) +/// [`#EXT-X-DATERANGE:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.7) /// /// The EXT-X-DATERANGE tag associates a Date Range (i.e. a range of time /// defined by a starting and ending date) with a set of attribute / @@ -686,8 +672,7 @@ pub struct DateRange { // Rest // ----------------------------------------------------------------------------------------------- -/// [`#EXT-X-START:`] -/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.5.2) +/// [`#EXT-X-START:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.5.2) /// /// The EXT-X-START tag indicates a preferred point at which to start /// playing a Playlist. By default, clients SHOULD start playback at From 472618e1aa87b038e8f2cc20ddeeccd6e4144c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Thu, 18 Nov 2021 15:05:32 +0200 Subject: [PATCH 13/13] Update version to 3.0.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e7c8668..5ae15ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "m3u8-rs" -version = "2.1.0" +version = "3.0.0" authors = ["Rutger"] readme = "README.md" repository = "https://github.com/rutgersc/m3u8-rs"