From 870ca830d38a62b2b008c24fafe8044b9e58eb05 Mon Sep 17 00:00:00 2001 From: Jan Schmidt Date: Tue, 20 Apr 2021 02:35:45 +1000 Subject: [PATCH 1/2] SessionData: Must have either VALUE or URI, but not both. If SessionData has a value, don't write the URI and vice-versa. As per https://tools.ietf.org/html/rfc8216#section-4.3.4.4 EXT-X-SESSION-DATA must have one or the other, not both. --- src/lib.rs | 5 +++-- src/playlist.rs | 53 ++++++++++++++++++++++++++++++++++++------------- tests/lib.rs | 13 ++++++------ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bb7588c..cdb2765 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -319,8 +319,9 @@ named!(pub alternative_media_tag, ); named!(pub session_data_tag, - do_parse!( tag!("#EXT-X-SESSION-DATA:") >> attributes: key_value_pairs >> - ( SessionData::from_hashmap(attributes))) + do_parse!( tag!("#EXT-X-SESSION-DATA:") >> + session_data: map_res!(key_value_pairs, |attrs| SessionData::from_hashmap(attrs)) >> + ( session_data)) ); named!(pub session_key_tag, diff --git a/src/playlist.rs b/src/playlist.rs index a085e0f..cdecb13 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -343,6 +343,9 @@ 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) +/// 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. #[derive(Debug, Default, PartialEq, Clone)] pub struct SessionKey(pub Key); @@ -354,34 +357,56 @@ impl SessionKey { } } +#[derive(Debug, PartialEq, Clone)] +pub enum SessionDataField { + Value(String), + Uri(String) +} + /// [`#EXT-X-SESSION-DATA:`] /// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.4) -/// 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. -#[derive(Debug, Default, PartialEq, Clone)] +/// The EXT-X-SESSION-DATA tag allows arbitrary session data to be carried +/// in a Master Playlist. +#[derive(Debug, PartialEq, Clone)] pub struct SessionData { pub data_id: String, - pub value: String, - pub uri: String, + pub field: SessionDataField, pub language: Option, } impl SessionData { - pub fn from_hashmap(mut attrs: HashMap) -> SessionData { - SessionData { - data_id: attrs.remove("DATA-ID").unwrap_or_else(String::new), - value: attrs.remove("VALUE").unwrap_or_else(String::new), - uri: attrs.remove("URI").unwrap_or_else(String::new), + pub fn from_hashmap(mut attrs: HashMap) -> Result { + let data_id = match attrs.remove("DATA-ID") { + Some(data_id) => data_id, + None => return Err("EXT-X-SESSION-DATA field without DATA-ID".to_string()) + }; + + let value = attrs.remove("VALUE"); + let uri = attrs.remove("URI"); + + // SessionData must contain either a VALUE or a URI, + // but not both https://tools.ietf.org/html/rfc8216#section-4.3.4.4 + let field = match (value, uri) { + (Some(value), None) => SessionDataField::Value(value), + (None, Some(uri)) => SessionDataField::Uri(uri), + (Some(_), Some(_)) => return Err(format!["EXT-X-SESSION-DATA tag {} contains both a value and a uri", data_id]), + (None, None) => return Err(format!["EXT-X-SESSION-DATA tag {} must contain either a value or a uri", data_id]), + }; + + Ok(SessionData { + data_id, + field, language: attrs.remove("LANGUAGE"), - } + }) } pub fn write_to(&self, w: &mut T) -> std::io::Result<()> { write!(w, "#EXT-X-SESSION-DATA:")?; write!(w, "DATA-ID=\"{}\"", self.data_id)?; - write!(w, ",VALUE=\"{}\"", self.value)?; - write!(w, ",URI=\"{}\"", self.uri)?; + match &self.field { + SessionDataField::Value(value) => write!(w, ",VALUE=\"{}\"", value)?, + SessionDataField::Uri(uri) => write!(w, ",URI=\"{}\"", uri)?, + }; write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?; write!(w, "\n") } diff --git a/tests/lib.rs b/tests/lib.rs index 41cb456..1b04afe 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -333,12 +333,13 @@ fn create_and_parse_master_playlist_full() { closed_captions: Some("closed_captions".into()), } ], - session_data: Some(SessionData { - data_id: "****".into(), - value: "%%%%".into(), - uri: "++++".into(), - language: Some("SessionDataLanguage".into()), - }), + session_data: Some( + SessionData { + data_id: "****".into(), + field: SessionDataField::Value("%%%%".to_string()), + language: Some("SessionDataLanguage".into()), + } + ), session_key: Some(SessionKey(Key { method: "AES-128".into(), uri: Some("https://secure.domain.com".into()), From 05669cab6830330ff564e66a066885cedd53134d Mon Sep 17 00:00:00 2001 From: Jan Schmidt Date: Tue, 20 Apr 2021 01:55:02 +1000 Subject: [PATCH 2/2] Support multiple session data and key tags. Collect Vecs of session_data and session_key tags to allow for multiples of each. Doesn't do any validation as to disallowed duplicated values. Fixes #20 --- src/playlist.rs | 13 ++++++------- tests/lib.rs | 20 +++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/playlist.rs b/src/playlist.rs index cdecb13..af937c1 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -65,8 +65,8 @@ impl Playlist { pub struct MasterPlaylist { pub version: usize, pub variants: Vec, - pub session_data: Option, - pub session_key: Option, + pub session_data: Vec, + pub session_key: Vec, pub start: Option, pub independent_segments: bool, pub alternatives: Vec, // EXT-X-MEDIA tags @@ -74,7 +74,6 @@ pub struct MasterPlaylist { } impl MasterPlaylist { - pub fn from_tags(mut tags: Vec) -> MasterPlaylist { let mut master_playlist = MasterPlaylist::default(); @@ -95,10 +94,10 @@ impl MasterPlaylist { } } MasterPlaylistTag::SessionData(data) => { - master_playlist.session_data = Some(data); + master_playlist.session_data.push(data); } MasterPlaylistTag::SessionKey(key) => { - master_playlist.session_key = Some(key); + master_playlist.session_key.push(key); } MasterPlaylistTag::Start(s) => { master_playlist.start = Some(s); @@ -131,10 +130,10 @@ impl MasterPlaylist { for variant in &self.variants { variant.write_to(w)?; } - if let Some(ref session_data) = self.session_data { + for session_data in &self.session_data { session_data.write_to(w)?; } - if let Some(ref session_key) = self.session_key { + for session_key in &self.session_key { session_key.write_to(w)?; } if let Some(ref start) = self.start { diff --git a/tests/lib.rs b/tests/lib.rs index 1b04afe..601ff43 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -333,20 +333,22 @@ fn create_and_parse_master_playlist_full() { closed_captions: Some("closed_captions".into()), } ], - session_data: Some( + session_data: vec![ SessionData { data_id: "****".into(), field: SessionDataField::Value("%%%%".to_string()), language: Some("SessionDataLanguage".into()), } - ), - session_key: Some(SessionKey(Key { - method: "AES-128".into(), - uri: Some("https://secure.domain.com".into()), - iv: Some("0xb059217aa2649ce170b734".into()), - keyformat: Some("xXkeyformatXx".into()), - keyformatversions: Some("xXFormatVers".into()), - })), + ], + session_key: vec![ + SessionKey(Key { + method: "AES-128".into(), + uri: Some("https://secure.domain.com".into()), + iv: Some("0xb059217aa2649ce170b734".into()), + keyformat: Some("xXkeyformatXx".into()), + keyformatversions: Some("xXFormatVers".into()), + }) + ], start: Some(Start { time_offset: "123123123".into(), precise: Some("YES".into()),