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 crate::playlist::*; use nom::IResult; use std::collections::HashMap; use std::f32; use std::fmt; use std::result::Result; use std::str; use std::str::FromStr; use std::string; /// Parse an m3u8 playlist. /// /// # Examples /// /// ``` /// use std::io::Read; /// use m3u8_rs::Playlist; /// /// let mut file = std::fs::File::open("playlist.m3u8").unwrap(); /// let mut bytes: Vec = Vec::new(); /// file.read_to_end(&mut bytes).unwrap(); /// /// let parsed = m3u8_rs::parse_playlist(&bytes); /// /// let playlist = match parsed { /// Result::Ok((i, playlist)) => playlist, /// Result::Err(e) => panic!("Parsing error: \n{}", e), /// }; /// /// match playlist { /// Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{:?}", pl), /// Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{:?}", pl), /// } /// ``` pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> { m3u_tag(input)?; match is_master_playlist(input) { true => map(parse_master_playlist, Playlist::MasterPlaylist)(input), false => map(parse_media_playlist, Playlist::MediaPlaylist)(input), } } /// Parses an m3u8 playlist just like `parse_playlist`, except that this returns an [std::result::Result](std::result::Result) instead of a [nom::IResult](https://docs.rs/nom/1.2.3/nom/enum.IResult.html). /// However, since [nom::IResult](nom::IResult) is now an [alias to Result](https://github.com/Geal/nom/blob/master/doc/upgrading_to_nom_5.md), this is no longer needed. /// /// # Examples /// /// ``` /// use m3u8_rs::Playlist; /// 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(); /// /// let parsed = m3u8_rs::parse_playlist_res(&bytes); /// /// match parsed { /// Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl), /// Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl), /// Err(e) => println!("Error: {:?}", e) /// } /// ``` pub fn parse_playlist_res(input: &[u8]) -> Result>> { let parse_result = parse_playlist(input); match parse_result { IResult::Ok((_, playlist)) => Ok(playlist), IResult::Err(err) => Err(err), } } /// Parse input as a master playlist pub fn parse_master_playlist(input: &[u8]) -> IResult<&[u8], MasterPlaylist> { map( pair( complete(pair(m3u_tag, multispace0)), parse_master_playlist_tags, ), |(_, tags)| master_playlist_from_tags(tags), )(input) } /// Parse input as a master playlist pub fn parse_master_playlist_res( input: &[u8], ) -> Result>> { let parse_result = parse_master_playlist(input); match parse_result { IResult::Ok((_, playlist)) => Ok(playlist), IResult::Err(err) => Err(err), } } /// Parse input as a media playlist pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> { map( pair( complete(pair(m3u_tag, multispace0)), parse_media_playlist_tags, ), |(_, tags)| media_playlist_from_tags(tags), )(input) } /// Parse input as a media playlist pub fn parse_media_playlist_res( input: &[u8], ) -> Result>> { let parse_result = parse_media_playlist(input); match parse_result { IResult::Ok((_, playlist)) => Ok(playlist), IResult::Err(err) => Err(err), } } /// When a media tag or no master tag is found, this returns false. pub fn is_master_playlist(input: &[u8]) -> bool { // Assume it's not a master playlist contains_master_tag(input).map(|t| t.0).unwrap_or(false) } /// Scans input looking for either a master or media `#EXT` tag. /// /// Returns `Some(true/false)` when a master/media tag is found. Otherwise returns `None`. /// /// - None: Unkown tag or empty line /// - Some(true, tagstring): Line contains a master playlist tag /// - Some(false, tagstring): Line contains a media playlist tag fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> { let (input, _) = m3u_tag(input).ok()?; let mut is_master_opt = None; let mut current_input: &[u8] = input; while is_master_opt == None { match is_master_playlist_tag_line(current_input) { IResult::Ok((rest, result)) => { current_input = rest; is_master_opt = result; // result can be None (no media or master tag found) } _ => break, // Parser error encountered, can't read any more lines. } } is_master_opt } 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("#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 // ----------------------------------------------------------------------------------------------- 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. #[allow(clippy::large_enum_variant)] #[derive(Debug)] enum MasterPlaylistTag { Version(usize), VariantStream(VariantStream), AlternativeMedia(AlternativeMedia), SessionData(SessionData), SessionKey(SessionKey), Start(Start), IndependentSegments, Comment(String), Uri(String), Unknown(ExtTag), } fn master_playlist_tag(i: &[u8]) -> IResult<&[u8], MasterPlaylistTag> { // Don't accept empty inputs here peek(take(1usize))(i)?; alt(( 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) } fn master_playlist_from_tags(mut tags: Vec) -> MasterPlaylist { let mut master_playlist = MasterPlaylist::default(); while let Some(tag) = tags.pop() { match tag { MasterPlaylistTag::Version(v) => { master_playlist.version = Some(v); } MasterPlaylistTag::AlternativeMedia(v) => { master_playlist.alternatives.push(v); } MasterPlaylistTag::VariantStream(stream) => { master_playlist.variants.push(stream); } MasterPlaylistTag::Uri(uri) => { if let Some(stream) = master_playlist.get_newest_variant() { stream.uri = uri; } } MasterPlaylistTag::SessionData(data) => { master_playlist.session_data.push(data); } MasterPlaylistTag::SessionKey(key) => { master_playlist.session_key.push(key); } MasterPlaylistTag::Start(s) => { master_playlist.start = Some(s); } MasterPlaylistTag::IndependentSegments => { master_playlist.independent_segments = true; } MasterPlaylistTag::Unknown(unknown) => { master_playlist.unknown_tags.push(unknown); } _ => (), } } master_playlist } fn variant_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { map_res( pair(tag("#EXT-X-STREAM-INF:"), key_value_pairs), |(_, attributes)| VariantStream::from_hashmap(attributes, false), )(i) } fn variant_i_frame_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { map_res( pair(tag("#EXT-X-I-FRAME-STREAM-INF:"), key_value_pairs), |(_, attributes)| VariantStream::from_hashmap(attributes, true), )(i) } fn alternative_media_tag(i: &[u8]) -> IResult<&[u8], AlternativeMedia> { map(pair(tag("#EXT-X-MEDIA:"), key_value_pairs), |(_, media)| { AlternativeMedia::from_hashmap(media) })(i) } 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) } fn session_key_tag(i: &[u8]) -> IResult<&[u8], SessionKey> { map(pair(tag("#EXT-X-SESSION-KEY:"), key), |(_, key)| { SessionKey(key) })(i) } // ----------------------------------------------------------------------------------------------- // Media Playlist // ----------------------------------------------------------------------------------------------- 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. #[derive(Debug)] enum MediaPlaylistTag { Version(usize), Segment(SegmentTag), TargetDuration(f32), MediaSequence(u64), DiscontinuitySequence(u64), EndList, PlaylistType(MediaPlaylistType), IFramesOnly, Start(Start), IndependentSegments, } fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { // Don't accept empty inputs here peek(take(1usize))(i)?; alt(( 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) } 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; let mut map = None; while let Some(tag) = tags.pop() { match tag { MediaPlaylistTag::Version(v) => { media_playlist.version = Some(v); } MediaPlaylistTag::TargetDuration(d) => { media_playlist.target_duration = d; } MediaPlaylistTag::MediaSequence(n) => { media_playlist.media_sequence = n; } MediaPlaylistTag::DiscontinuitySequence(n) => { media_playlist.discontinuity_sequence = n; } MediaPlaylistTag::EndList => { media_playlist.end_list = true; } MediaPlaylistTag::PlaylistType(t) => { media_playlist.playlist_type = Some(t); } MediaPlaylistTag::IFramesOnly => { media_playlist.i_frames_only = true; } MediaPlaylistTag::Start(s) => { media_playlist.start = Some(s); } MediaPlaylistTag::IndependentSegments => { media_playlist.independent_segments = true; } MediaPlaylistTag::Segment(segment_tag) => match segment_tag { SegmentTag::Extinf(d, t) => { next_segment.duration = d; next_segment.title = t; } SegmentTag::ByteRange(b) => { next_segment.byte_range = Some(b); } SegmentTag::Discontinuity => { next_segment.discontinuity = true; } SegmentTag::Key(k) => { encryption_key = Some(k); } SegmentTag::Map(m) => { map = Some(m); } SegmentTag::ProgramDateTime(d) => { next_segment.program_date_time = Some(d); } SegmentTag::DateRange(d) => { next_segment.daterange = Some(d); } SegmentTag::Unknown(t) => { next_segment.unknown_tags.push(t); } SegmentTag::Uri(u) => { next_segment.key = encryption_key.clone(); next_segment.map = map.clone(); next_segment.uri = u; media_playlist.segments.push(next_segment); next_segment = MediaSegment::empty(); encryption_key = None; map = None; } _ => (), }, } } media_playlist } 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 // ----------------------------------------------------------------------------------------------- /// All possible media segment tags. #[derive(Debug)] enum SegmentTag { Extinf(f32, Option), ByteRange(ByteRange), Discontinuity, Key(Key), Map(Map), ProgramDateTime(String), DateRange(String), Unknown(ExtTag), Comment(String), Uri(String), } 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) } 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) } fn key(i: &[u8]) -> IResult<&[u8], Key> { map(key_value_pairs, Key::from_hashmap)(i) } 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.to_string().as_bytes()) { IResult::Ok((_, range)) => Ok(range), IResult::Err(_) => Err("invalid byte range"), }) .transpose()?; Ok(Map { uri: uri.to_string(), byte_range, }) })(i) } // ----------------------------------------------------------------------------------------------- // Basic tags // ----------------------------------------------------------------------------------------------- fn m3u_tag(i: &[u8]) -> IResult<&[u8], ()> { map(tag("#EXTM3U"), |_| ())(i) } 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) } fn start_tag(i: &[u8]) -> IResult<&[u8], Start> { map( pair(tag("#EXT-X-START:"), key_value_pairs), |(_, attributes)| Start::from_hashmap(attributes), )(i) } 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) } 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 // ----------------------------------------------------------------------------------------------- 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) } #[derive(Debug, PartialEq, Eq, Clone)] pub enum QuotedOrUnquoted { Unquoted(String), Quoted(String), } impl Default for QuotedOrUnquoted { fn default() -> Self { QuotedOrUnquoted::Quoted(String::new()) } } impl QuotedOrUnquoted { pub fn as_str(&self) -> &str { match self { QuotedOrUnquoted::Quoted(s) => s.as_str(), QuotedOrUnquoted::Unquoted(s) => s.as_str(), } } pub fn as_unquoted(&self) -> Option<&str> { match self { QuotedOrUnquoted::Unquoted(s) => Some(s.as_str()), _ => None, } } pub fn as_quoted(&self) -> Option<&str> { match self { QuotedOrUnquoted::Quoted(s) => Some(s.as_str()), _ => None, } } } impl From<&str> for QuotedOrUnquoted { fn from(s: &str) -> Self { if s.starts_with('"') && s.ends_with('"') { return QuotedOrUnquoted::Quoted(s.trim_matches('"').to_string()); } QuotedOrUnquoted::Unquoted(s.to_string()) } } impl fmt::Display for QuotedOrUnquoted { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", match self { QuotedOrUnquoted::Unquoted(s) => s, QuotedOrUnquoted::Quoted(u) => u, } ) } } fn key_value_pair(i: &[u8]) -> IResult<&[u8], (String, QuotedOrUnquoted)> { 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) } fn quoted(i: &[u8]) -> IResult<&[u8], QuotedOrUnquoted> { delimited( char('\"'), map_res(is_not("\""), quoted_from_utf8_slice), char('\"'), )(i) } fn unquoted(i: &[u8]) -> IResult<&[u8], QuotedOrUnquoted> { map_res(is_not(",\r\n"), unquoted_from_utf8_slice)(i) } fn consume_line(i: &[u8]) -> IResult<&[u8], String> { map( pair(map_res(not_line_ending, from_utf8_slice), opt(line_ending)), |(line, _)| line, )(i) } fn number(i: &[u8]) -> IResult<&[u8], u64> { 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) } fn byte_range_val(i: &[u8]) -> IResult<&[u8], ByteRange> { map(pair(number, opt(preceded(char('@'), number))), |(n, o)| { ByteRange { length: n, offset: o, } })(i) } 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 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) } fn from_utf8_slice(s: &[u8]) -> Result { String::from_utf8(s.to_vec()) } fn quoted_from_utf8_slice(s: &[u8]) -> Result { match String::from_utf8(s.to_vec()) { Ok(q) => Ok(QuotedOrUnquoted::Quoted(q)), Err(e) => Err(e), } } fn unquoted_from_utf8_slice(s: &[u8]) -> Result { match String::from_utf8(s.to_vec()) { Ok(q) => Ok(QuotedOrUnquoted::Unquoted(q)), Err(e) => Err(e), } } #[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, 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), v.into())) .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), v.into())) .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), v.into())) .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), v.into())) .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".into()))) ); } #[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\"".into())) ); } #[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())))) ); } }