1use crate::registry::{PsshData, drm_system_name, hex_string, uuid_string};
16
17pub const WIDEVINE_SYSTEM_ID: [u8; 16] = [
18 0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED,
19];
20
21pub const PLAYREADY_SYSTEM_ID: [u8; 16] = [
22 0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95,
23];
24
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27pub struct WidevinePsshData {
28 #[serde(skip_serializing_if = "Option::is_none", default)]
30 pub algorithm: Option<String>,
31 pub key_ids: Vec<String>,
33 #[serde(skip_serializing_if = "Option::is_none", default)]
34 pub provider: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none", default)]
37 pub content_id: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none", default)]
40 pub content_id_text: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none", default)]
42 pub policy: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none", default)]
44 pub crypto_period_index: Option<u32>,
45 #[serde(skip_serializing_if = "Option::is_none", default)]
47 pub protection_scheme: Option<String>,
48}
49
50#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
52pub struct PlayReadyPsshData {
53 pub record_count: u16,
54 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub wrm_header_version: Option<String>,
57 pub key_ids: Vec<String>,
60 #[serde(skip_serializing_if = "Option::is_none", default)]
61 pub la_url: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none", default)]
64 pub xml: Option<String>,
65}
66
67struct ProtoReader<'a> {
70 data: &'a [u8],
71 pos: usize,
72}
73
74impl<'a> ProtoReader<'a> {
75 fn varint(&mut self) -> Option<u64> {
76 let mut value: u64 = 0;
77 let mut shift = 0u32;
78 loop {
79 let byte = *self.data.get(self.pos)?;
80 self.pos += 1;
81 if shift >= 64 {
82 return None;
83 }
84 value |= u64::from(byte & 0x7F) << shift;
85 if byte & 0x80 == 0 {
86 return Some(value);
87 }
88 shift += 7;
89 }
90 }
91
92 fn bytes(&mut self) -> Option<&'a [u8]> {
93 let len = self.varint()? as usize;
94 let end = self.pos.checked_add(len)?;
95 if end > self.data.len() {
96 return None;
97 }
98 let out = &self.data[self.pos..end];
99 self.pos = end;
100 Some(out)
101 }
102
103 fn skip(&mut self, wire: u8) -> Option<()> {
104 match wire {
105 0 => {
106 self.varint()?;
107 }
108 1 => {
109 self.pos = self.pos.checked_add(8)?;
110 }
111 2 => {
112 self.bytes()?;
113 }
114 5 => {
115 self.pos = self.pos.checked_add(4)?;
116 }
117 _ => return None,
118 }
119 (self.pos <= self.data.len()).then_some(())
120 }
121}
122
123pub fn parse_widevine_pssh_data(data: &[u8]) -> Option<WidevinePsshData> {
127 if data.is_empty() {
128 return None;
129 }
130 let mut out = WidevinePsshData {
131 algorithm: None,
132 key_ids: Vec::new(),
133 provider: None,
134 content_id: None,
135 content_id_text: None,
136 policy: None,
137 crypto_period_index: None,
138 protection_scheme: None,
139 };
140 let mut recognized = 0usize;
141 let mut r = ProtoReader { data, pos: 0 };
142 while r.pos < data.len() {
143 let key = r.varint()?;
144 let field = key >> 3;
145 let wire = (key & 7) as u8;
146 match (field, wire) {
147 (1, 0) => {
148 out.algorithm = Some(match r.varint()? {
149 0 => "UNENCRYPTED".to_string(),
150 1 => "AESCTR".to_string(),
151 n => n.to_string(),
152 });
153 recognized += 1;
154 }
155 (2, 2) => {
156 out.key_ids.push(hex_string(r.bytes()?));
157 recognized += 1;
158 }
159 (3, 2) => {
160 out.provider = Some(String::from_utf8(r.bytes()?.to_vec()).ok()?);
161 recognized += 1;
162 }
163 (4, 2) => {
164 let id = r.bytes()?;
165 out.content_id = Some(hex_string(id));
166 if !id.is_empty() && id.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
167 out.content_id_text = Some(String::from_utf8(id.to_vec()).ok()?);
168 }
169 recognized += 1;
170 }
171 (6, 2) => {
172 out.policy = Some(String::from_utf8(r.bytes()?.to_vec()).ok()?);
173 recognized += 1;
174 }
175 (7, 0) => {
176 out.crypto_period_index = Some(u32::try_from(r.varint()?).ok()?);
177 recognized += 1;
178 }
179 (9, 0) => {
180 let fourcc = u32::try_from(r.varint()?).ok()?.to_be_bytes();
181 out.protection_scheme = if fourcc.iter().all(|b| b.is_ascii_graphic()) {
182 Some(String::from_utf8(fourcc.to_vec()).ok()?)
183 } else {
184 Some(hex_string(&fourcc))
185 };
186 recognized += 1;
187 }
188 _ => r.skip(wire)?,
189 }
190 }
191 (recognized > 0).then_some(out)
194}
195
196fn utf16le_to_string(bytes: &[u8]) -> Option<String> {
199 if !bytes.len().is_multiple_of(2) {
200 return None;
201 }
202 let units: Vec<u16> = bytes
203 .chunks_exact(2)
204 .map(|c| u16::from_le_bytes([c[0], c[1]]))
205 .collect();
206 String::from_utf16(&units)
207 .ok()
208 .map(|s| s.trim_start_matches('\u{feff}').to_string())
209}
210
211fn xml_unescape(s: &str) -> String {
212 s.replace("<", "<")
213 .replace(">", ">")
214 .replace(""", "\"")
215 .replace("'", "'")
216 .replace("&", "&")
217}
218
219fn xml_element_text<'a>(xml: &'a str, tag: &str) -> Option<&'a str> {
221 let open = format!("<{tag}>");
222 let close = format!("</{tag}>");
223 let start = xml.find(&open)? + open.len();
224 let end = start + xml[start..].find(&close)?;
225 Some(&xml[start..end])
226}
227
228fn xml_attr_value<'a>(tag_text: &'a str, attr: &str) -> Option<&'a str> {
230 let needle = format!("{attr}=\"");
231 let start = tag_text.find(&needle)? + needle.len();
232 let end = start + tag_text[start..].find('"')?;
233 Some(&tag_text[start..end])
234}
235
236fn guid_le_to_be(mut guid: [u8; 16]) -> [u8; 16] {
240 guid[0..4].reverse();
241 guid[4..6].reverse();
242 guid[6..8].reverse();
243 guid
244}
245
246fn playready_kid_to_hex(b64: &str) -> Option<String> {
247 let bytes = crate::util::base64_decode(b64.trim())?;
248 let guid: [u8; 16] = bytes.try_into().ok()?;
249 Some(hex_string(&guid_le_to_be(guid)))
250}
251
252fn extract_playready_kids(xml: &str) -> Vec<String> {
255 let mut out: Vec<String> = Vec::new();
256 let mut search = 0usize;
257 while let Some(found) = xml[search..].find("<KID") {
258 let after = search + found + "<KID".len();
259 search = after;
260 match xml.as_bytes().get(after) {
261 Some(b'>') => {
262 if let Some(end) = xml[after + 1..].find("</KID>")
263 && let Some(kid) = playready_kid_to_hex(&xml[after + 1..after + 1 + end])
264 {
265 out.push(kid);
266 }
267 }
268 Some(c) if c.is_ascii_whitespace() => {
269 if let Some(end) = xml[after..].find('>')
270 && let Some(value) = xml_attr_value(&xml[after..after + end], "VALUE")
271 && let Some(kid) = playready_kid_to_hex(value)
272 {
273 out.push(kid);
274 }
275 }
276 _ => {}
278 }
279 }
280 out.dedup();
281 out
282}
283
284pub fn parse_playready_pssh_data(data: &[u8]) -> Option<PlayReadyPsshData> {
287 if data.len() < 10 {
288 return None;
289 }
290 let total = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
291 if total < 10 || total > data.len() {
292 return None;
293 }
294 let record_count = u16::from_le_bytes(data[4..6].try_into().unwrap());
295 let mut pos = 6usize;
296 let mut xml: Option<String> = None;
297 for _ in 0..record_count {
298 if pos + 4 > total {
299 return None;
300 }
301 let record_type = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap());
302 let record_len = u16::from_le_bytes(data[pos + 2..pos + 4].try_into().unwrap()) as usize;
303 pos += 4;
304 if pos + record_len > total {
305 return None;
306 }
307 if record_type == 1 && xml.is_none() {
310 xml = utf16le_to_string(&data[pos..pos + record_len]);
311 }
312 pos += record_len;
313 }
314 let xml = xml?;
315 let wrm_header_version = xml
316 .find("<WRMHEADER")
317 .and_then(|i| {
318 let tag_end = i + xml[i..].find('>')?;
319 xml_attr_value(&xml[i..tag_end], "version")
320 })
321 .map(str::to_string);
322 Some(PlayReadyPsshData {
323 record_count,
324 wrm_header_version,
325 key_ids: extract_playready_kids(&xml),
326 la_url: xml_element_text(&xml, "LA_URL").map(xml_unescape),
327 xml: Some(xml),
328 })
329}
330
331pub(crate) fn parse_pssh_body(version: u8, flags: u32, body: &[u8]) -> anyhow::Result<PsshData> {
337 if body.len() < 16 {
338 anyhow::bail!("pssh body truncated ({} bytes)", body.len());
339 }
340 let system_id: [u8; 16] = body[0..16].try_into().unwrap();
341 let mut pos = 16usize;
342
343 let mut key_ids = Vec::new();
344 if version >= 1 {
345 if pos + 4 > body.len() {
346 anyhow::bail!("pssh KID count truncated");
347 }
348 let kid_count = u32::from_be_bytes(body[pos..pos + 4].try_into().unwrap()) as usize;
349 pos += 4;
350 if pos + kid_count.saturating_mul(16) > body.len() {
351 anyhow::bail!("pssh KID list truncated");
352 }
353 for _ in 0..kid_count {
354 key_ids.push(hex_string(&body[pos..pos + 16]));
355 pos += 16;
356 }
357 }
358
359 if pos + 4 > body.len() {
360 anyhow::bail!("pssh data size truncated");
361 }
362 let data_size = u32::from_be_bytes(body[pos..pos + 4].try_into().unwrap());
363 pos += 4;
364 let data = &body[pos..pos + (data_size as usize).min(body.len() - pos)];
366
367 let widevine = (system_id == WIDEVINE_SYSTEM_ID)
368 .then(|| parse_widevine_pssh_data(data))
369 .flatten()
370 .map(Box::new);
371 let playready = (system_id == PLAYREADY_SYSTEM_ID)
372 .then(|| parse_playready_pssh_data(data))
373 .flatten()
374 .map(Box::new);
375
376 Ok(PsshData {
377 version,
378 flags,
379 system_id: uuid_string(&system_id),
380 system_name: drm_system_name(&system_id).map(str::to_string),
381 key_ids,
382 data_size,
383 widevine,
384 playready,
385 })
386}
387
388pub fn parse_pssh_boxes(data: &[u8]) -> anyhow::Result<Vec<PsshData>> {
392 let mut out = Vec::new();
393 let mut pos = 0usize;
394 while data.len() - pos >= 8 {
395 if &data[pos + 4..pos + 8] != b"pssh" {
396 anyhow::bail!("not a pssh box at offset {pos}");
397 }
398 let declared = u32::from_be_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
399 let size = if declared == 0 {
402 data.len() - pos
403 } else {
404 declared
405 };
406 if size < 12 || pos + size > data.len() {
407 anyhow::bail!("invalid pssh box size {declared} at offset {pos}");
408 }
409 let payload = &data[pos + 8..pos + size];
410 let version = payload[0];
411 let flags = u32::from_be_bytes([0, payload[1], payload[2], payload[3]]);
412 out.push(parse_pssh_body(version, flags, &payload[4..])?);
413 pos += size;
414 }
415 if out.is_empty() {
416 anyhow::bail!("no pssh box found");
417 }
418 Ok(out)
419}
420
421pub fn pssh_from_raw_widevine(data: &[u8]) -> Option<PsshData> {
425 let widevine = parse_widevine_pssh_data(data)?;
426 Some(PsshData {
427 version: 0,
428 flags: 0,
429 system_id: uuid_string(&WIDEVINE_SYSTEM_ID),
430 system_name: Some("Widevine".to_string()),
431 key_ids: Vec::new(),
432 data_size: data.len() as u32,
433 widevine: Some(Box::new(widevine)),
434 playready: None,
435 })
436}