1use crate::error::{Error, Result};
7use alloc::{
8 format,
9 string::{String, ToString},
10 vec::Vec,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16#[non_exhaustive]
17pub enum Scte35Cue {
18 Out,
20 In,
22 Cmd,
24}
25
26impl Scte35Cue {
27 pub fn name(&self) -> &'static str {
29 match self {
30 Scte35Cue::Out => "out",
31 Scte35Cue::In => "in",
32 Scte35Cue::Cmd => "cmd",
33 }
34 }
35 fn attr_key(&self) -> &'static str {
36 match self {
37 Scte35Cue::Out => "SCTE35-OUT",
38 Scte35Cue::In => "SCTE35-IN",
39 Scte35Cue::Cmd => "SCTE35-CMD",
40 }
41 }
42}
43dvb_common::impl_spec_display!(Scte35Cue);
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub struct Scte35Attr {
49 pub cue: Scte35Cue,
51 pub raw: Vec<u8>,
53}
54
55#[derive(Debug, Clone, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct DateRange {
59 pub id: String,
61 pub start_date: String,
63 pub class: Option<String>,
65 pub duration: Option<f64>,
67 pub planned_duration: Option<f64>,
69 pub scte35: Option<Scte35Attr>,
71}
72
73const TAG: &str = "#EXT-X-DATERANGE:";
77
78impl DateRange {
79 pub fn to_tag_line(&self) -> String {
83 let mut out = String::from(TAG);
84 out.push_str(&format!("ID=\"{}\"", self.id));
85 out.push_str(&format!(",START-DATE=\"{}\"", self.start_date));
86 if let Some(c) = &self.class {
87 out.push_str(&format!(",CLASS=\"{}\"", c));
88 }
89 if let Some(d) = self.duration {
90 out.push_str(&format!(",DURATION={}", fmt_f64(d)));
91 }
92 if let Some(d) = self.planned_duration {
93 out.push_str(&format!(",PLANNED-DURATION={}", fmt_f64(d)));
94 }
95 if let Some(s) = &self.scte35 {
96 out.push_str(&format!(",{}=0x{}", s.cue.attr_key(), to_hex_upper(&s.raw)));
97 }
98 out
99 }
100
101 pub fn parse_tag_line(s: &str) -> Result<DateRange> {
103 let body = s
104 .strip_prefix(TAG)
105 .ok_or_else(|| Error::AttrParse("missing #EXT-X-DATERANGE: prefix".to_string()))?;
106 let mut dr = DateRange {
107 id: String::new(),
108 start_date: String::new(),
109 class: None,
110 duration: None,
111 planned_duration: None,
112 scte35: None,
113 };
114 let mut seen_id = false;
115 for (k, v) in split_attrs(body) {
116 match k {
117 "ID" => {
118 dr.id = unquote(v);
119 seen_id = true;
120 }
121 "START-DATE" => dr.start_date = unquote(v),
122 "CLASS" => dr.class = Some(unquote(v)),
123 "DURATION" => dr.duration = Some(parse_f64(v)?),
124 "PLANNED-DURATION" => dr.planned_duration = Some(parse_f64(v)?),
125 "SCTE35-OUT" => {
126 dr.scte35 = Some(Scte35Attr {
127 cue: Scte35Cue::Out,
128 raw: parse_hex(v)?,
129 })
130 }
131 "SCTE35-IN" => {
132 dr.scte35 = Some(Scte35Attr {
133 cue: Scte35Cue::In,
134 raw: parse_hex(v)?,
135 })
136 }
137 "SCTE35-CMD" => {
138 dr.scte35 = Some(Scte35Attr {
139 cue: Scte35Cue::Cmd,
140 raw: parse_hex(v)?,
141 })
142 }
143 _ => {} }
145 }
146 if !seen_id {
147 return Err(Error::AttrParse("DATERANGE missing ID".to_string()));
148 }
149 Ok(dr)
150 }
151}
152
153fn fmt_f64(v: f64) -> String {
154 let trunc = v as i64;
157 if v == trunc as f64 {
158 format!("{}", trunc)
159 } else {
160 format!("{}", v)
161 }
162}
163
164fn to_hex_upper(b: &[u8]) -> String {
165 let mut s = String::with_capacity(b.len() * 2);
166 for byte in b {
167 s.push_str(&format!("{:02X}", byte));
168 }
169 s
170}
171
172fn unquote(v: &str) -> String {
173 v.trim_matches('"').to_string()
174}
175
176fn parse_f64(v: &str) -> Result<f64> {
177 v.parse::<f64>()
178 .map_err(|_| Error::AttrParse(format!("bad number: {v}")))
179}
180
181fn parse_hex(v: &str) -> Result<Vec<u8>> {
182 let h = v
183 .strip_prefix("0x")
184 .or_else(|| v.strip_prefix("0X"))
185 .unwrap_or(v);
186 if h.len() % 2 != 0 {
187 return Err(Error::AttrParse("odd-length hex".to_string()));
188 }
189 (0..h.len())
190 .step_by(2)
191 .map(|i| {
192 u8::from_str_radix(&h[i..i + 2], 16)
193 .map_err(|_| Error::AttrParse("bad hex".to_string()))
194 })
195 .collect()
196}
197
198fn split_attrs(body: &str) -> Vec<(&str, &str)> {
200 let mut pairs = Vec::new();
201 let bytes = body.as_bytes();
202 let (mut start, mut in_q) = (0usize, false);
203 let mut i = 0;
204 while i <= bytes.len() {
205 let at_end = i == bytes.len();
206 let c = if at_end { b',' } else { bytes[i] };
207 match c {
208 b'"' => in_q = !in_q,
209 b',' if !in_q => {
210 let field = &body[start..i];
211 if let Some(eq) = field.find('=') {
212 pairs.push((&field[..eq], &field[eq + 1..]));
213 }
214 start = i + 1;
215 }
216 _ => {}
217 }
218 i += 1;
219 }
220 pairs
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use alloc::{string::ToString, vec};
227
228 fn sample() -> DateRange {
229 DateRange {
230 id: "2002".to_string(),
231 start_date: "2018-10-29T10:38:00.000Z".to_string(),
232 class: None,
233 duration: None,
234 planned_duration: Some(24.0),
235 scte35: Some(Scte35Attr {
236 cue: Scte35Cue::Out,
237 raw: vec![0xFC, 0x30, 0x21],
238 }),
239 }
240 }
241
242 #[test]
243 fn tag_round_trips_byte_identical() {
244 let dr = sample();
245 let line = dr.to_tag_line();
246 assert!(line.starts_with("#EXT-X-DATERANGE:"));
247 assert!(line.contains("SCTE35-OUT=0xFC3021"));
248 let back = DateRange::parse_tag_line(&line).unwrap();
249 assert_eq!(back, dr);
250 }
251
252 #[test]
253 fn cue_labels() {
254 assert_eq!(Scte35Cue::Out.name(), "out");
255 assert_eq!(alloc::format!("{}", Scte35Cue::In), "in");
256 }
257}