mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-22 15:21:01 +00:00
updated parser
This commit is contained in:
parent
91c6698f16
commit
1a35463185
15 changed files with 289 additions and 175 deletions
|
@ -17,6 +17,7 @@ codecov = {repository = "sile/hls_m3u8"}
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
trackable = "0.2"
|
trackable = "0.2"
|
||||||
|
getset = "0.0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
clap = "2"
|
clap = "2"
|
||||||
|
|
|
@ -141,7 +141,7 @@ impl MediaPlaylistBuilder {
|
||||||
|
|
||||||
// CHECK: `#EXT-X-BYTE-RANGE`
|
// CHECK: `#EXT-X-BYTE-RANGE`
|
||||||
if let Some(tag) = s.byte_range_tag() {
|
if let Some(tag) = s.byte_range_tag() {
|
||||||
if tag.range().start.is_none() {
|
if tag.to_range().start().is_none() {
|
||||||
let last_uri = track_assert_some!(last_range_uri, ErrorKind::InvalidInput);
|
let last_uri = track_assert_some!(last_range_uri, ErrorKind::InvalidInput);
|
||||||
track_assert_eq!(last_uri, s.uri(), ErrorKind::InvalidInput);
|
track_assert_eq!(last_uri, s.uri(), ErrorKind::InvalidInput);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.1.1. EXTM3U]
|
/// [4.3.1.1. EXTM3U]
|
||||||
///
|
///
|
||||||
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
|
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
|
||||||
|
@ -27,8 +29,8 @@ impl fmt::Display for ExtM3u {
|
||||||
impl FromStr for ExtM3u {
|
impl FromStr for ExtM3u {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtM3u)
|
Ok(ExtM3u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.1.2. EXT-X-VERSION]
|
/// [4.3.1.2. EXT-X-VERSION]
|
||||||
///
|
///
|
||||||
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
||||||
|
@ -37,10 +39,8 @@ impl fmt::Display for ExtXVersion {
|
||||||
impl FromStr for ExtXVersion {
|
impl FromStr for ExtXVersion {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
let version = tag(input, Self::PREFIX)?.parse()?;
|
||||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
|
||||||
let version = track!(suffix.parse())?;
|
|
||||||
Ok(ExtXVersion::new(version))
|
Ok(ExtXVersion::new(version))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,35 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use getset::{Getters, MutGetters, Setters};
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion};
|
use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion};
|
||||||
use crate::utils::parse_u64;
|
use crate::utils::parse_u64;
|
||||||
use crate::utils::{quote, unquote};
|
use crate::utils::{quote, tag, unquote};
|
||||||
use crate::{Error, ErrorKind, Result};
|
use crate::{Error, ErrorKind};
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
|
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
|
||||||
///
|
///
|
||||||
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
|
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Getters, Setters, MutGetters, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
pub struct ExtXIFrameStreamInf {
|
pub struct ExtXIFrameStreamInf {
|
||||||
|
/// The URI, that identifies the associated media playlist.
|
||||||
uri: String,
|
uri: String,
|
||||||
|
/// The peak segment bit rate of the variant stream.
|
||||||
bandwidth: u64,
|
bandwidth: u64,
|
||||||
|
/// The average segment bit rate of the variant stream.
|
||||||
average_bandwidth: Option<u64>,
|
average_bandwidth: Option<u64>,
|
||||||
|
/// A string that represents the list of codec types contained the variant stream.
|
||||||
codecs: Option<String>,
|
codecs: Option<String>,
|
||||||
|
/// The optimal pixel resolution at which to display all the video in the variant stream.
|
||||||
resolution: Option<DecimalResolution>,
|
resolution: Option<DecimalResolution>,
|
||||||
|
/// The HDCP level of the variant stream.
|
||||||
hdcp_level: Option<HdcpLevel>,
|
hdcp_level: Option<HdcpLevel>,
|
||||||
|
/// The group identifier for the video in the variant stream.
|
||||||
video: Option<String>,
|
video: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,41 +49,6 @@ impl ExtXIFrameStreamInf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the URI that identifies the associated media playlist.
|
|
||||||
pub const fn uri(&self) -> &String {
|
|
||||||
&self.uri
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the peak segment bit rate of the variant stream.
|
|
||||||
pub const fn bandwidth(&self) -> u64 {
|
|
||||||
self.bandwidth
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the average segment bit rate of the variant stream.
|
|
||||||
pub const fn average_bandwidth(&self) -> Option<u64> {
|
|
||||||
self.average_bandwidth
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a string that represents the list of codec types contained the variant stream.
|
|
||||||
pub fn codecs(&self) -> Option<&String> {
|
|
||||||
self.codecs.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the optimal pixel resolution at which to display all the video in the variant stream.
|
|
||||||
pub const fn resolution(&self) -> Option<DecimalResolution> {
|
|
||||||
self.resolution
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the HDCP level of the variant stream.
|
|
||||||
pub const fn hdcp_level(&self) -> Option<HdcpLevel> {
|
|
||||||
self.hdcp_level
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the group identifier for the video in the variant stream.
|
|
||||||
pub fn video(&self) -> Option<&String> {
|
|
||||||
self.video.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
|
@ -105,8 +83,8 @@ impl fmt::Display for ExtXIFrameStreamInf {
|
||||||
impl FromStr for ExtXIFrameStreamInf {
|
impl FromStr for ExtXIFrameStreamInf {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut bandwidth = None;
|
let mut bandwidth = None;
|
||||||
|
@ -115,7 +93,8 @@ impl FromStr for ExtXIFrameStreamInf {
|
||||||
let mut resolution = None;
|
let mut resolution = None;
|
||||||
let mut hdcp_level = None;
|
let mut hdcp_level = None;
|
||||||
let mut video = None;
|
let mut video = None;
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
|
let attrs = AttributePairs::parse(input);
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
let (key, value) = track!(attr)?;
|
let (key, value) = track!(attr)?;
|
||||||
match key {
|
match key {
|
||||||
|
@ -167,7 +146,7 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(i_frame_stream_inf.uri(), "foo");
|
assert_eq!(i_frame_stream_inf.uri(), "foo");
|
||||||
assert_eq!(i_frame_stream_inf.bandwidth(), 1000);
|
assert_eq!(*i_frame_stream_inf.bandwidth(), 1000);
|
||||||
// TODO: test all the optional fields
|
// TODO: test all the optional fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::types::{InStreamId, MediaType, ProtocolVersion};
|
use crate::types::{InStreamId, MediaType, ProtocolVersion};
|
||||||
use crate::utils::{parse_yes_or_no, quote, unquote};
|
use crate::utils::{parse_yes_or_no, quote, tag, unquote};
|
||||||
use crate::{Error, ErrorKind, Result};
|
use crate::{Error, ErrorKind};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ impl ExtXMediaBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a `ExtXMedia` instance.
|
/// Builds a `ExtXMedia` instance.
|
||||||
pub fn finish(self) -> Result<ExtXMedia> {
|
pub fn finish(self) -> crate::Result<ExtXMedia> {
|
||||||
let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput);
|
let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput);
|
||||||
let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput);
|
let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput);
|
||||||
let name = track_assert_some!(self.name, ErrorKind::InvalidInput);
|
let name = track_assert_some!(self.name, ErrorKind::InvalidInput);
|
||||||
|
@ -309,11 +309,11 @@ impl fmt::Display for ExtXMedia {
|
||||||
impl FromStr for ExtXMedia {
|
impl FromStr for ExtXMedia {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut builder = ExtXMediaBuilder::new();
|
let mut builder = ExtXMediaBuilder::new();
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
let attrs = AttributePairs::parse(input);
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
let (key, value) = track!(attr)?;
|
let (key, value) = track!(attr)?;
|
||||||
match key {
|
match key {
|
||||||
|
|
|
@ -1,17 +1,26 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{ProtocolVersion, SessionData};
|
|
||||||
use crate::utils::{quote, unquote};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use getset::{Getters, MutGetters, Setters};
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{ProtocolVersion, SessionData};
|
||||||
|
use crate::utils::{quote, tag, unquote};
|
||||||
|
use crate::{Error, ErrorKind};
|
||||||
|
|
||||||
/// [4.3.4.4. EXT-X-SESSION-DATA]
|
/// [4.3.4.4. EXT-X-SESSION-DATA]
|
||||||
///
|
///
|
||||||
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Getters, MutGetters, Setters, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
pub struct ExtXSessionData {
|
pub struct ExtXSessionData {
|
||||||
|
/// The identifier of the data.
|
||||||
data_id: String,
|
data_id: String,
|
||||||
|
/// The session data.
|
||||||
data: SessionData,
|
data: SessionData,
|
||||||
|
/// The language of the data.
|
||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,21 +45,6 @@ impl ExtXSessionData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the identifier of the data.
|
|
||||||
pub const fn data_id(&self) -> &String {
|
|
||||||
&self.data_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the session data.
|
|
||||||
pub const fn data(&self) -> &SessionData {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the language of the data.
|
|
||||||
pub fn language(&self) -> Option<&String> {
|
|
||||||
self.language.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
|
@ -75,14 +69,15 @@ impl fmt::Display for ExtXSessionData {
|
||||||
impl FromStr for ExtXSessionData {
|
impl FromStr for ExtXSessionData {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut data_id = None;
|
let mut data_id = None;
|
||||||
let mut session_value = None;
|
let mut session_value = None;
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut language = None;
|
let mut language = None;
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
|
let attrs = AttributePairs::parse(input);
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
let (key, value) = track!(attr)?;
|
let (key, value) = track!(attr)?;
|
||||||
match key {
|
match key {
|
||||||
|
@ -119,23 +114,38 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_session_data() {
|
fn test_display() {
|
||||||
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into()));
|
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into()));
|
||||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz");
|
let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz");
|
||||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
||||||
|
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||||
|
|
||||||
|
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into()));
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
||||||
|
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||||
|
|
||||||
|
let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz");
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
||||||
|
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::types::{DecryptionKey, ProtocolVersion};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
|
use crate::utils::tag;
|
||||||
|
|
||||||
/// [4.3.4.5. EXT-X-SESSION-KEY]
|
/// [4.3.4.5. EXT-X-SESSION-KEY]
|
||||||
///
|
///
|
||||||
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
||||||
|
@ -37,13 +38,11 @@ impl fmt::Display for ExtXSessionKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXSessionKey {
|
impl FromStr for ExtXSessionKey {
|
||||||
type Err = Error;
|
type Err = crate::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
let key = tag(input, Self::PREFIX)?.parse()?;
|
||||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
Ok(Self::new(key))
|
||||||
let key = track!(suffix.parse())?;
|
|
||||||
Ok(ExtXSessionKey { key })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion,
|
ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion,
|
||||||
SingleLineString,
|
SingleLineString,
|
||||||
};
|
};
|
||||||
use crate::utils::{parse_u64, quote, unquote};
|
use crate::utils::{parse_u64, quote, tag, unquote};
|
||||||
use crate::{Error, ErrorKind, Result};
|
use crate::{Error, ErrorKind};
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// [4.3.4.2. EXT-X-STREAM-INF]
|
/// [4.3.4.2. EXT-X-STREAM-INF]
|
||||||
///
|
///
|
||||||
|
@ -146,16 +147,15 @@ impl fmt::Display for ExtXStreamInf {
|
||||||
impl FromStr for ExtXStreamInf {
|
impl FromStr for ExtXStreamInf {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let mut lines = s.splitn(2, '\n');
|
let mut lines = input.lines();
|
||||||
let first_line = lines.next().expect("Never fails").trim_end_matches('\r');
|
let first_line = lines.next().ok_or(ErrorKind::InvalidInput)?; // TODO!
|
||||||
let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput);
|
let second_line = lines.next().ok_or(ErrorKind::InvalidInput)?; // TODO!
|
||||||
|
|
||||||
|
tag(first_line, Self::PREFIX)?;
|
||||||
|
|
||||||
track_assert!(
|
|
||||||
first_line.starts_with(Self::PREFIX),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
let uri = track!(SingleLineString::new(second_line))?;
|
let uri = track!(SingleLineString::new(second_line))?;
|
||||||
|
|
||||||
let mut bandwidth = None;
|
let mut bandwidth = None;
|
||||||
let mut average_bandwidth = None;
|
let mut average_bandwidth = None;
|
||||||
let mut codecs = None;
|
let mut codecs = None;
|
||||||
|
@ -186,6 +186,7 @@ impl FromStr for ExtXStreamInf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
||||||
Ok(ExtXStreamInf {
|
Ok(ExtXStreamInf {
|
||||||
uri,
|
uri,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
|
||||||
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
||||||
///
|
///
|
||||||
|
@ -39,12 +39,11 @@ impl fmt::Display for ExtXDiscontinuitySequence {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXDiscontinuitySequence {
|
impl FromStr for ExtXDiscontinuitySequence {
|
||||||
type Err = Error;
|
type Err = crate::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
let seq_num = tag(input, Self::PREFIX)?.parse().unwrap(); // TODO!
|
||||||
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
Ok(Self::new(seq_num))
|
||||||
Ok(ExtXDiscontinuitySequence { seq_num })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,39 +1,72 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
use crate::types::{ByteRange, ProtocolVersion};
|
use crate::types::{ByteRange, ProtocolVersion};
|
||||||
use crate::{Error, ErrorKind, Result};
|
use crate::{Error, ErrorKind, Result};
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]
|
/// [4.3.2.2. EXT-X-BYTERANGE]
|
||||||
///
|
///
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXByteRange {
|
pub struct ExtXByteRange(ByteRange);
|
||||||
range: ByteRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXByteRange {
|
impl ExtXByteRange {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
||||||
|
|
||||||
/// Makes a new `ExtXByteRange` tag.
|
/// Makes a new `ExtXByteRange` tag.
|
||||||
pub const fn new(range: ByteRange) -> Self {
|
/// # Example
|
||||||
ExtXByteRange { range }
|
/// ```
|
||||||
|
/// use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
///
|
||||||
|
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||||
|
/// ```
|
||||||
|
pub const fn new(length: usize, start: Option<usize>) -> Self {
|
||||||
|
Self(ByteRange::new(length, start))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the range of the associated media segment.
|
/// Converts the [ExtXByteRange] to a [ByteRange].
|
||||||
pub const fn range(&self) -> ByteRange {
|
/// # Example
|
||||||
self.range
|
/// ```
|
||||||
|
/// use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
/// use hls_m3u8::types::ByteRange;
|
||||||
|
///
|
||||||
|
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||||
|
/// let range: ByteRange = byte_range.to_range();
|
||||||
|
/// ```
|
||||||
|
pub const fn to_range(&self) -> ByteRange {
|
||||||
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
/// use hls_m3u8::types::ProtocolVersion;
|
||||||
|
///
|
||||||
|
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||||
|
/// assert_eq!(byte_range.requires_version(), ProtocolVersion::V4);
|
||||||
|
/// ```
|
||||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V4
|
ProtocolVersion::V4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for ExtXByteRange {
|
||||||
|
type Target = ByteRange;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXByteRange {
|
impl fmt::Display for ExtXByteRange {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.range)
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
write!(f, "{}", self.0)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,9 +74,30 @@ impl FromStr for ExtXByteRange {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
// check if the string starts with the PREFIX
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
let byte_range = s.split_at(Self::PREFIX.len()).1;
|
||||||
Ok(ExtXByteRange { range })
|
let tokens = byte_range.splitn(2, '@').collect::<Vec<_>>();
|
||||||
|
if tokens.is_empty() {
|
||||||
|
Err(ErrorKind::InvalidInput)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = tokens[0]
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| ErrorKind::InvalidInput.cause(e))?;
|
||||||
|
let start = {
|
||||||
|
let mut result = None;
|
||||||
|
if tokens.len() == 2 {
|
||||||
|
result = Some(
|
||||||
|
tokens[1]
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| ErrorKind::InvalidInput.cause(e))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExtXByteRange::new(length, start))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,21 +106,40 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_byterange() {
|
fn test_display() {
|
||||||
let tag = ExtXByteRange::new(ByteRange {
|
let byte_range = ExtXByteRange::new(0, Some(5));
|
||||||
length: 3,
|
assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:0@5".to_string());
|
||||||
start: None,
|
|
||||||
});
|
|
||||||
assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
|
||||||
|
|
||||||
let tag = ExtXByteRange::new(ByteRange {
|
let byte_range = ExtXByteRange::new(99999, Some(2));
|
||||||
length: 3,
|
assert_eq!(
|
||||||
start: Some(5),
|
byte_range.to_string(),
|
||||||
});
|
"#EXT-X-BYTERANGE:99999@2".to_string()
|
||||||
assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag));
|
);
|
||||||
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
let byte_range = ExtXByteRange::new(99999, None);
|
||||||
|
assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:99999".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let byte_range = ExtXByteRange::new(99999, Some(2));
|
||||||
|
assert_eq!(
|
||||||
|
byte_range,
|
||||||
|
"#EXT-X-BYTERANGE:99999@2".parse::<ExtXByteRange>().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let byte_range = ExtXByteRange::new(99999, None);
|
||||||
|
assert_eq!(
|
||||||
|
byte_range,
|
||||||
|
"#EXT-X-BYTERANGE:99999".parse::<ExtXByteRange>().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deref() {
|
||||||
|
let byte_range = ExtXByteRange::new(0, Some(22));
|
||||||
|
|
||||||
|
assert_eq!(*byte_range.length(), 0);
|
||||||
|
assert_eq!(*byte_range.start(), Some(22));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::{Error, Result};
|
||||||
|
|
||||||
/// [4.3.2.3. EXT-X-DISCONTINUITY]
|
/// [4.3.2.3. EXT-X-DISCONTINUITY]
|
||||||
///
|
///
|
||||||
/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3
|
/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXDiscontinuity;
|
pub struct ExtXDiscontinuity;
|
||||||
|
|
||||||
impl ExtXDiscontinuity {
|
impl ExtXDiscontinuity {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
||||||
|
|
||||||
|
@ -25,8 +28,9 @@ impl fmt::Display for ExtXDiscontinuity {
|
||||||
|
|
||||||
impl FromStr for ExtXDiscontinuity {
|
impl FromStr for ExtXDiscontinuity {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtXDiscontinuity)
|
Ok(ExtXDiscontinuity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,13 +100,7 @@ mod test {
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||||
|
|
||||||
let tag = ExtXMap::with_range(
|
let tag = ExtXMap::with_range("foo", ByteRange::new(9, Some(2)));
|
||||||
"foo",
|
|
||||||
ByteRange {
|
|
||||||
length: 9,
|
|
||||||
start: Some(2),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#;
|
let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#;
|
||||||
track_try_unwrap!(ExtXMap::from_str(text));
|
track_try_unwrap!(ExtXMap::from_str(text));
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
use getset::{Getters, MutGetters, Setters};
|
||||||
use trackable::error::ErrorKindExt;
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
|
||||||
/// Byte range.
|
/// Byte range.
|
||||||
///
|
///
|
||||||
/// See: [4.3.2.2. EXT-X-BYTERANGE]
|
/// See: [4.3.2.2. EXT-X-BYTERANGE]
|
||||||
///
|
///
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
||||||
#[allow(missing_docs)]
|
#[derive(Getters, Setters, MutGetters, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
pub struct ByteRange {
|
pub struct ByteRange {
|
||||||
pub length: usize,
|
/// The length of the range.
|
||||||
pub start: Option<usize>,
|
length: usize,
|
||||||
|
/// The start of the range.
|
||||||
|
start: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ByteRange {
|
||||||
|
/// Creates a new [ByteRange].
|
||||||
|
pub const fn new(length: usize, start: Option<usize>) -> Self {
|
||||||
|
Self { length, start }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ByteRange {
|
impl fmt::Display for ByteRange {
|
||||||
|
@ -27,20 +41,28 @@ impl fmt::Display for ByteRange {
|
||||||
|
|
||||||
impl FromStr for ByteRange {
|
impl FromStr for ByteRange {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
let mut tokens = s.splitn(2, '@');
|
let tokens = s.splitn(2, '@').collect::<Vec<_>>();
|
||||||
let length = tokens.next().expect("Never fails");
|
if tokens.is_empty() {
|
||||||
let start = if let Some(start) = tokens.next() {
|
Err(ErrorKind::InvalidInput)?;
|
||||||
Some(track!(start
|
}
|
||||||
.parse()
|
|
||||||
.map_err(|e| ErrorKind::InvalidInput.cause(e)))?)
|
let length = tokens[0]
|
||||||
} else {
|
.parse()
|
||||||
None
|
.map_err(|e| ErrorKind::InvalidInput.cause(e))?;
|
||||||
|
let start = {
|
||||||
|
let mut result = None;
|
||||||
|
if tokens.len() == 2 {
|
||||||
|
result = Some(
|
||||||
|
tokens[1]
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| ErrorKind::InvalidInput.cause(e))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result
|
||||||
};
|
};
|
||||||
Ok(ByteRange {
|
Ok(ByteRange::new(length, start))
|
||||||
length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
|
||||||
start,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
src/utils.rs
30
src/utils.rs
|
@ -44,6 +44,19 @@ pub(crate) fn quote<T: ToString>(value: T) -> String {
|
||||||
format!("\"{}\"", value.to_string().replace("\"", ""))
|
format!("\"{}\"", value.to_string().replace("\"", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks, if the given tag is at the start of the input. If this is the case, it will remove it
|
||||||
|
/// return the rest of the input, otherwise it will return an error.
|
||||||
|
pub(crate) fn tag<T>(input: &str, tag: T) -> crate::Result<&str>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
if !input.starts_with(tag.as_ref()) {
|
||||||
|
Err(ErrorKind::InvalidInput)?; // TODO!
|
||||||
|
}
|
||||||
|
let result = input.split_at(tag.as_ref().len()).1;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -74,4 +87,21 @@ mod tests {
|
||||||
assert_eq!(quote("value"), "\"value\"".to_string());
|
assert_eq!(quote("value"), "\"value\"".to_string());
|
||||||
assert_eq!(quote("\"value\""), "\"value\"".to_string());
|
assert_eq!(quote("\"value\""), "\"value\"".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tag() {
|
||||||
|
let input = "HelloMyFriendThisIsASampleString";
|
||||||
|
|
||||||
|
let input = tag(input, "Hello").unwrap();
|
||||||
|
assert_eq!(input, "MyFriendThisIsASampleString");
|
||||||
|
|
||||||
|
let input = tag(input, "My").unwrap();
|
||||||
|
assert_eq!(input, "FriendThisIsASampleString");
|
||||||
|
|
||||||
|
let input = tag(input, "FriendThisIs").unwrap();
|
||||||
|
assert_eq!(input, "ASampleString");
|
||||||
|
|
||||||
|
let input = tag(input, "A").unwrap();
|
||||||
|
assert_eq!(input, "SampleString");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue