pssh_box/
lib.rs

1//! Parsing and serialization support for pssh boxes, as used in DRM systems.
2//!
3//! This crate defines Rust data structures allowing you to store, parse and serialize Protection System
4//! Specific Header (**PSSH**) boxes, which provide data for the initialization of a Digital Rights
5//! Management (DRM) system. PSSH boxes are used:
6//!
7//! - in an MP4 box of type `pssh` in an MP4 fragment (CMAF/MP4/ISOBMFF containers)
8//!
9//! - in a `<cenc:pssh>` element in a DASH MPD manifest
10//!
11//! - in DRM initialization data passed to the Encrypted Media Extension of a web browser
12//!
13//! - in an EXT-X-SESSION-KEY field of an m3u8 playlist.
14//!
15//! A PSSH box includes information for a single DRM system. This library supports the PSSH data formats
16//! for the following DRM systems:
17//!
18//! - Widevine, owned by Google, widely used for DASH streaming
19//! - PlayReady, owned by Microsoft, widely used for DASH streaming
20//! - WisePlay, owned by Huawei
21//! - Irdeto
22//! - Marlin
23//! - Nagra
24//! - FairPlay (the unofficial version used by Netflix)
25//! - Common Encryption
26//!
27//! PSSH boxes contain (depending on the DRM system) information on the key_ID for which to obtain a
28//! content key, the encryption scheme used (e.g. cenc, cbc1, cens or cbcs), the URL of the licence
29//! server, and checksum data.
30
31
32pub mod playready;
33pub mod widevine;
34pub mod irdeto;
35pub mod nagra;
36pub mod wiseplay;
37
38use std::fmt;
39use std::io::{Cursor, Read, Write};
40use hex_literal::hex;
41use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
42use zerocopy::FromBytes;
43use serde::{Serialize, Deserialize};
44use prost::Message;
45use base64::prelude::{Engine as _, BASE64_STANDARD};
46use base64::engine;
47use anyhow::{Result, Context, anyhow};
48use tracing::trace;
49use crate::widevine::WidevinePsshData;
50use crate::playready::PlayReadyPsshData;
51use crate::irdeto::IrdetoPsshData;
52use crate::nagra::NagraPsshData;
53use crate::wiseplay::WisePlayPsshData;
54
55
56/// The version of this crate.
57pub fn version() -> &'static str {
58    env!("CARGO_PKG_VERSION")
59}
60
61pub trait ToBytes {
62    fn to_bytes(&self) -> Vec<u8>;
63}
64
65/// Data in a PSSH box whose format is dependent on the DRM system used.
66#[non_exhaustive]
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub enum PsshData {
69    Widevine(WidevinePsshData),
70    PlayReady(PlayReadyPsshData),
71    Irdeto(IrdetoPsshData),
72    WisePlay(WisePlayPsshData),
73    Nagra(NagraPsshData),
74    Marlin(Vec<u8>),
75    CommonEnc(Vec<u8>),
76    FairPlay(Vec<u8>),
77}
78
79impl ToBytes for PsshData {
80    fn to_bytes(&self) -> Vec<u8> {
81        match self {
82            PsshData::Widevine(wv) => wv.to_bytes(),
83            PsshData::PlayReady(pr) => pr.to_bytes(),
84            PsshData::Irdeto(ir) => ir.to_bytes(),
85            PsshData::WisePlay(c) => c.to_bytes(),
86            PsshData::Nagra(n) => n.to_bytes(),
87            PsshData::Marlin(m) => m.to_vec(),
88            PsshData::CommonEnc(c) => c.to_vec(),
89            PsshData::FairPlay(c) => c.to_vec(),
90        }
91    }
92}
93
94/// The identifier for a DRM system.
95#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, FromBytes)]
96pub struct DRMSystemId {
97    id: [u8; 16],
98}
99
100impl TryFrom<&[u8]> for DRMSystemId {
101    type Error = ();
102
103    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
104        if let Ok(id) = value.try_into() {
105            Ok(DRMSystemId { id })
106        } else {
107            Err(())
108        }
109    }
110}
111
112impl TryFrom<Vec<u8>> for DRMSystemId {
113    type Error = ();
114
115    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
116        if value.len() == 16 {
117            DRMSystemId::try_from(&value[0..16])
118        } else {
119            Err(())
120        }
121    }
122}
123
124impl TryFrom<&str> for DRMSystemId {
125    type Error = ();
126
127    fn try_from(value: &str) -> Result<Self, Self::Error> {
128        if value.len() == 32 {
129            if let Ok(id) = hex::decode(value) {
130                return DRMSystemId::try_from(id);
131            }
132        }
133        Err(())
134    }
135}
136
137impl ToBytes for DRMSystemId {
138    fn to_bytes(&self) -> Vec<u8> {
139        self.id.into()
140    }
141}
142
143impl fmt::Display for DRMSystemId {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        // See list at https://dashif.org/identifiers/content_protection/
146        let family = if self.id == hex!("1077efecc0b24d02ace33c1e52e2fb4b") {
147            "Common"
148        } else if self.id == hex!("69f908af481646ea910ccd5dcccb0a3a") {
149            "CENC"
150        } else if self.id == hex!("edef8ba979d64acea3c827dcd51d21ed") {
151            "Widevine"
152        } else if self.id == hex!("9a04f07998404286ab92e65be0885f95") {
153            "PlayReady"
154        } else if self.id == hex!("6dd8b3c345f44a68bf3a64168d01a4a6") {
155            "ABV"
156        } else if self.id == hex!("f239e769efa348509c16a903c6932efb") {
157            "Adobe Primetime"
158        } else if self.id == hex!("616c7469636173742d50726f74656374") {
159            "Alticast"
160        } else if self.id == hex!("94ce86fb07ff4f43adb893d2fa968ca2") {
161            "Apple FairPlay"
162        } else if self.id == hex!("29701fe43cc74a348c5bae90c7439a47") {
163            // Unofficial FairPlay systemID used by Netflix for DASH streaming,
164            // see https://forums.developer.apple.com/thread/6185
165            "Apple FairPlay-Netflix variant"
166        } else if self.id == hex!("3ea8778f77424bf9b18be834b2acbd47") {
167            "ClearKey AES-128"
168        } else if self.id == hex!("be58615b19c4468488b3c8c57e99e957") {
169            "ClearKey SAMPLE-AES"
170        } else if self.id == hex!("e2719d58a985b3c9781ab030af78d30e") {
171            "ClearKey DASH-IF"
172        } else if self.id == hex!("45d481cb8fe049c0ada9ab2d2455b2f2") {
173            "CoreTrust"
174        } else if self.id == hex!("80a6be7e14484c379e70d5aebe04c8d2") {
175            "Irdeto"
176        } else if self.id == hex!("5e629af538da4063897797ffbd9902d4") {
177            "Marlin"
178        } else if self.id == hex!("adb41c242dbf4a6d958b4457c0d27b95") {
179            "Nagra"
180        } else if self.id == hex!("1f83e1e86ee94f0dba2f5ec4e3ed1a66") {
181            "SecureMedia"
182        } else if self.id == hex!("3d5e6d359b9a41e8b843dd3c6e72c42c") {
183            // WisePlay (from Huawei) and ChinaDRM are apparently different DRM systems that are
184            // identified by the same system id.
185            "WisePlay-ChinaDRM"
186        } else if self.id == hex!("793b79569f944946a94223e7ef7e44b4") {
187            "VisionCrypt"
188        } else {
189            "Unknown"
190        };
191        let hex = hex::encode(self.id);
192        write!(f, "{}/DRMSystemId<{}-{}-{}-{}-{}>",
193               family,
194               &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
195    }
196}
197
198impl fmt::Debug for DRMSystemId {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "DRMSystemId<{}>", hex::encode(self.id))
201    }
202}
203
204pub const COMMON_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("1077efecc0b24d02ace33c1e52e2fb4b") };
205pub const CENC_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("69f908af481646ea910ccd5dcccb0a3a") };
206pub const WIDEVINE_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("edef8ba979d64acea3c827dcd51d21ed") };
207pub const PLAYREADY_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("9a04f07998404286ab92e65be0885f95") };
208pub const FAIRPLAYNFLX_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("29701fe43cc74a348c5bae90c7439a47") };
209pub const IRDETO_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("80a6be7e14484c379e70d5aebe04c8d2") };
210pub const MARLIN_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("5e629af538da4063897797ffbd9902d4") };
211pub const NAGRA_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("adb41c242dbf4a6d958b4457c0d27b95") };
212pub const WISEPLAY_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("3d5e6d359b9a41e8b843dd3c6e72c42c") };
213
214/// The Content Key or default_KID.
215#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, FromBytes)]
216pub struct DRMKeyId {
217    id: [u8; 16],
218}
219
220impl TryFrom<&[u8]> for DRMKeyId {
221    type Error = ();
222
223    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
224        if let Ok(id) = value.try_into() {
225            Ok(DRMKeyId { id })
226        } else {
227            Err(())
228        }
229    }
230}
231
232impl TryFrom<Vec<u8>> for DRMKeyId {
233    type Error = ();
234
235    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
236        if value.len() == 16 {
237            DRMKeyId::try_from(&value[0..16])
238        } else {
239            Err(())
240        }
241    }
242}
243
244impl TryFrom<&str> for DRMKeyId {
245    type Error = ();
246
247    fn try_from(value: &str) -> Result<Self, Self::Error> {
248        if value.len() == 32 {
249            if let Ok(id) = hex::decode(value) {
250                return DRMKeyId::try_from(id);
251            }
252        }
253        // UUID-style format, like 5ade6a1e-c0d4-43c6-92f2-2d36862ba8dd
254        if value.len() == 36 {
255            let v36 = value.as_bytes();
256            if v36[8] == b'-' &&
257                v36[13] == b'-' &&
258                v36[18] == b'-' &&
259                v36[23] == b'-'
260            {
261                let maybe_hex = value.replace('-', "");
262                if let Ok(id) = hex::decode(maybe_hex) {
263                    return DRMKeyId::try_from(id);
264                }
265            }
266        }
267        Err(())
268    }
269}
270
271impl ToBytes for DRMKeyId {
272    fn to_bytes(&self) -> Vec<u8> {
273        self.id.into()
274    }
275}
276
277impl fmt::Display for DRMKeyId {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        // example: 72c3ed2c-7a5f-4aad-902f-cbef1efe89a9
280        let hex = hex::encode(self.id);
281        write!(f, "DRMKeyId<{}-{}-{}-{}-{}>",
282               &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
283    }
284}
285
286impl fmt::Debug for DRMKeyId {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        write!(f, "DRMKeyId<{}>", hex::encode(self.id))
289    }
290}
291
292
293/// A PSSH box, also called a ProtectionSystemSpecificHeaderBox in ISO 23001-7:2012.
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295pub struct PsshBox {
296    pub version: u8,
297    pub flags: u32,
298    pub system_id: DRMSystemId,
299    pub key_ids: Vec<DRMKeyId>,
300    pub pssh_data: PsshData,
301}
302
303impl PsshBox {
304    /// Return an empty v1 Widevine PSSH box.
305    pub fn new_widevine() -> PsshBox {
306        let empty = WidevinePsshData {
307            provider: None,
308            ..Default::default()
309        };
310        PsshBox {
311            version: 1,
312            flags: 0,
313            system_id: WIDEVINE_SYSTEM_ID,
314            key_ids: vec![],
315            pssh_data: PsshData::Widevine(empty),
316        }
317    }
318
319    /// Return an empty v1 PlayReady PSSH box.
320    pub fn new_playready() -> PsshBox {
321        let empty = PlayReadyPsshData::new();
322        PsshBox {
323            version: 1,
324            flags: 0,
325            system_id: PLAYREADY_SYSTEM_ID,
326            key_ids: vec![],
327            pssh_data: PsshData::PlayReady(empty),
328        }
329    }
330
331    pub fn add_key_id(&mut self, kid: DRMKeyId) {
332        self.key_ids.push(kid);
333    }
334
335    pub fn to_base64(self) -> String {
336        BASE64_STANDARD.encode(self.to_bytes())
337    }
338
339    pub fn to_hex(self) -> String {
340        hex::encode(self.to_bytes())
341    }
342}
343
344/// This to_string() method provides the most compact representation possible on a single line; see
345/// the pprint() function for a more verbose layout.
346impl fmt::Display for PsshBox {
347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        let mut keys = Vec::new();
349        if self.version == 1 {
350            for key in &self.key_ids {
351                keys.push(hex::encode(key.id));
352            }
353        }
354        let key_str = match keys.len() {
355            0 => String::from(""),
356            1 => format!("key_id: {}, ", keys.first().unwrap()),
357            _ => format!("key_ids: {}, ", keys.join(", ")),
358        };
359        match &self.pssh_data {
360            PsshData::Widevine(wv) => {
361                let mut items = Vec::new();
362                let json = wv.to_json();
363                if let Some(alg) = json.get("algorithm") {
364                    if let Some(a) = alg.as_str() {
365                        items.push(String::from(a));
366                    }
367                }
368                // We are merging keys potentially present in the v1 PSSH box data with those
369                // present in the Widevine PSSH data.
370                if let Some(kav) = json.get("key_id") {
371                    if let Some(ka) = kav.as_array() {
372                        for kv in ka {
373                            if let Some(k) = kv.as_str() {
374                                keys.push(String::from(k));
375                            }
376                        }
377                    }
378                }
379                if keys.len() == 1 {
380                    items.push(format!("key_id: {}", keys.first().unwrap()));
381                }
382                if keys.len() > 1 {
383                    items.push(format!("key_ids: {}", keys.join(", ")));
384                }
385                if let Some(jo) = json.as_object() {
386                    for (k, v) in jo.iter() {
387                        if k.ne("algorithm") && k.ne("key_id") {
388                            items.push(format!("{k}: {v}"));
389                        }
390                    }
391                }
392                write!(f, "WidevinePSSH<{}>", items.join(", "))
393            },
394            PsshData::PlayReady(pr) => write!(f, "PlayReadyPSSH<{key_str}{pr:?}>"),
395            PsshData::Irdeto(pd) => write!(f, "IrdetoPSSH<{key_str}{}>", pd.xml),
396            PsshData::Marlin(pd) => write!(f, "  MarlinPSSH<{key_str}pssh data len {} octets>", pd.len()),
397            PsshData::Nagra(pd) => write!(f, "NagraPSSH<{key_str}{pd:?}>"),
398            PsshData::WisePlay(pd) => write!(f, "WisePlayPSSH<{key_str}{}>", pd.json),
399            PsshData::CommonEnc(pd) => write!(f, "CommonPSSH<{key_str}pssh data len {} octets>", pd.len()),
400            PsshData::FairPlay(pd) => write!(f, "FairPlayPSSH<{key_str}pssh data len {} octets>", pd.len()),
401        }
402    }
403}
404
405
406impl ToBytes for PsshBox {
407    #[allow(unused_must_use)]
408    fn to_bytes(self: &PsshBox) -> Vec<u8> {
409        let mut out = Vec::new();
410        let pssh_data_bytes = self.pssh_data.to_bytes();
411        let mut total_length: u32 = 4 // box size
412            + 4     // BMFF box header 'pssh'
413            + 4     // version+flags
414            + 16    // system_id
415            + 4     // pssh_data length
416            + pssh_data_bytes.len() as u32;
417        if self.version == 1 {
418            total_length += 4 // key_id count
419                + self.key_ids.len() as u32 * 16;
420        }
421        out.write_u32::<BigEndian>(total_length);
422        out.write_all(b"pssh");
423        let version_and_flags: u32 = self.flags ^ ((self.version as u32) << 24);
424        out.write_u32::<BigEndian>(version_and_flags);
425        out.write_all(&self.system_id.id);
426        if self.version == 1 {
427            out.write_u32::<BigEndian>(self.key_ids.len() as u32);
428            for k in &self.key_ids {
429                out.write_all(&k.id);
430            }
431        }
432        out.write_u32::<BigEndian>(pssh_data_bytes.len() as u32);
433        out.write_all(&pssh_data_bytes);
434        out
435    }
436}
437
438
439#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
440pub struct PsshBoxVec(Vec<PsshBox>);
441
442impl PsshBoxVec {
443    pub fn new() -> PsshBoxVec {
444        PsshBoxVec(Vec::new())
445    }
446
447    pub fn contains(&self, bx: &PsshBox) -> bool {
448        self.0.contains(bx)
449    }
450
451    pub fn add(&mut self, bx: PsshBox) {
452        self.0.push(bx);
453    }
454
455    pub fn len(&self) -> usize {
456        self.0.len()
457    }
458
459    pub fn is_empty(&self) -> bool {
460        self.0.is_empty()
461    }
462
463    pub fn iter(&self) -> impl Iterator<Item=&PsshBox>{
464        self.0.iter()
465    }
466
467    pub fn to_base64(self) -> String {
468        let mut buf = Vec::new();
469        for bx in self.0 {
470            buf.append(&mut bx.to_bytes());
471        }
472        BASE64_STANDARD.encode(buf)
473    }
474
475    pub fn to_hex(self) -> String {
476        let mut buf = Vec::new();
477        for bx in self.0 {
478            buf.append(&mut bx.to_bytes());
479        }
480        hex::encode(buf)
481    }
482}
483
484impl Default for PsshBoxVec {
485    fn default() -> Self {
486        Self::new()
487    }
488}
489
490impl IntoIterator for PsshBoxVec {
491    type Item = PsshBox;
492    type IntoIter = std::vec::IntoIter<Self::Item>;
493
494    fn into_iter(self) -> Self::IntoIter {
495        self.0.into_iter()
496    }
497}
498
499impl std::ops::Index<usize> for PsshBoxVec {
500    type Output = PsshBox;
501
502    fn index(&self, index: usize) -> &PsshBox {
503        &self.0[index]
504    }
505}
506
507impl fmt::Display for PsshBoxVec {
508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509        let mut items = Vec::new();
510        for pssh in self.iter() {
511            items.push(pssh.to_string());
512        }
513        // Print one PsshBox per line, without a trailing newline.
514        write!(f, "{}", items.join("\n"))
515    }
516}
517
518// Initialization Data is always composed of one or more concatenated 'pssh' boxes. The CDM must be
519// able to examine multiple 'pssh' boxes in the Initialization Data to find a 'pssh' box that it
520// supports.
521
522/// Parse one or more PSSH boxes from some initialization data encoded in base64 format.
523pub fn from_base64(init_data: &str) -> Result<PsshBoxVec> {
524    let b64_tolerant_config = engine::GeneralPurposeConfig::new()
525        .with_decode_allow_trailing_bits(true)
526        .with_decode_padding_mode(engine::DecodePaddingMode::Indifferent);
527    let b64_tolerant_engine = engine::GeneralPurpose::new(&base64::alphabet::STANDARD, b64_tolerant_config);
528    if init_data.len() < 8 {
529        return Err(anyhow!("insufficient length for init data"));
530    }
531    // We start by attempting to base64 decode the full string and parse that.
532    if let Ok(buf) = b64_tolerant_engine.decode(init_data) {
533        return from_bytes(&buf);
534    }
535    // If that doesn't work, attempt to decode PSSH boxes from subsequences of the init data. We
536    // look at a sliding window that starts at start and ends at start + the length we see from the
537    // PSSH box header.
538    let total_len = init_data.len();
539    let mut start = 0;
540    let mut boxes = Vec::new();
541    while start < total_len - 1 {
542        let buf = b64_tolerant_engine.decode(&init_data[start..start+7])
543            .context("base64 decoding first 32-bit length word")?;
544        let mut rdr = Cursor::new(buf);
545        let box_size: u32 = rdr.read_u32::<BigEndian>()
546            .context("reading PSSH box size")?;
547        trace!("box size from header = {box_size}");
548        // The number of octets that we obtain from decoding box_size chars worth of base64
549        let wanted_octets = (box_size.div_ceil(3) * 4) as usize;
550        let end = start + wanted_octets;
551        trace!("attempting to decode {wanted_octets} octets out of {}", init_data.len());
552        if end > init_data.len() {
553            // FIXME actually we shouldn't fail here, but rather break and return any boxes that we
554            // did manage to parse
555            return Err(anyhow!("insufficient length for init data (wanted {end}, have {})", init_data.len()));
556        }
557        let buf = b64_tolerant_engine.decode(&init_data[start..end])
558            .context("decoding base64")?;
559        let bx = from_bytes(&buf)
560            .context("parsing the PSSH initialization data")?;
561        assert!(bx.len() == 1);
562        trace!("Got one box {}", bx[0].clone());
563        boxes.push(bx[0].clone());
564        start = end;
565    }
566    Ok(PsshBoxVec(boxes))
567}
568
569/// Parse one or more PSSH boxes from some initialization data encoded in hex format.
570pub fn from_hex(init_data: &str) -> Result<PsshBoxVec> {
571    let buf = hex::decode(init_data)
572        .context("decoding hex")?;
573    from_bytes(&buf)
574        .context("parsing the PSSH initialization_data")
575}
576
577/// Parse a single PSSH box.
578fn read_pssh_box(rdr: &mut Cursor<&[u8]>) -> Result<PsshBox> {
579    let size: u32 = rdr.read_u32::<BigEndian>()
580        .context("reading PSSH box size")?;
581    trace!("PSSH box of size {size} octets");
582    let mut box_header = [0u8; 4];
583    rdr.read_exact(&mut box_header)
584        .context("reading box header")?;
585    // the ISO BMFF box header
586    if !box_header.eq(b"pssh") {
587        return Err(anyhow!("expecting BMFF header"));
588    }
589    let version_and_flags: u32 = rdr.read_u32::<BigEndian>()
590        .context("reading PSSH version/flags")?;
591    let version: u8 = (version_and_flags >> 24).try_into().unwrap();
592    trace!("PSSH box version {version}");
593    if version > 1 {
594        return Err(anyhow!("unknown PSSH version {version}"));
595    }
596    let mut system_id_buf = [0u8; 16];
597    rdr.read_exact(&mut system_id_buf)
598        .context("reading system_id")?;
599    let system_id = DRMSystemId { id: system_id_buf };
600    let mut key_ids = Vec::new();
601    if version == 1 {
602        let mut kid_count = rdr.read_u32::<BigEndian>()
603            .context("reading KID count")?;
604        trace!("PSSH box has {kid_count} KIDs in box header");
605        while kid_count > 0 {
606            let mut key = [0u8; 16];
607            rdr.read_exact(&mut key)
608                .context("reading key_id")?;
609            key_ids.push(DRMKeyId { id: key });
610            kid_count -= 1;
611        }
612    }
613    let pssh_data_len = rdr.read_u32::<BigEndian>()
614        .context("reading PSSH data length")?;
615    trace!("PSSH box data length {pssh_data_len} octets");
616    let mut pssh_data = Vec::new();
617    rdr.take(pssh_data_len.into()).read_to_end(&mut pssh_data)
618        .context("extracting PSSH data")?;
619    match system_id {
620        WIDEVINE_SYSTEM_ID => {
621            let wv_pssh_data = WidevinePsshData::decode(Cursor::new(pssh_data))
622                .context("parsing Widevine PSSH data")?;
623            Ok(PsshBox {
624                version,
625                flags: version_and_flags & 0xF,
626                system_id,
627                key_ids,
628                pssh_data: PsshData::Widevine(wv_pssh_data),
629            })
630        },
631        PLAYREADY_SYSTEM_ID => {
632            let pr_pssh_data = playready::parse_pssh_data(&pssh_data)
633                .context("parsing PlayReady PSSH data")?;
634            Ok(PsshBox {
635                version,
636                flags: version_and_flags & 0xF,
637                system_id,
638                key_ids,
639                pssh_data: PsshData::PlayReady(pr_pssh_data),
640            })
641        },
642        IRDETO_SYSTEM_ID => {
643            let ir_pssh_data = irdeto::parse_pssh_data(&pssh_data)
644                .context("parsing Irdeto PSSH data")?;
645            Ok(PsshBox {
646                version,
647                flags: version_and_flags & 0xF,
648                system_id,
649                key_ids,
650                pssh_data: PsshData::Irdeto(ir_pssh_data),
651            })
652        },
653        MARLIN_SYSTEM_ID => {
654            Ok(PsshBox {
655                version,
656                flags: version_and_flags & 0xF,
657                system_id,
658                key_ids,
659                pssh_data: PsshData::Marlin(pssh_data),
660            })
661        },
662        NAGRA_SYSTEM_ID => {
663            let pd = nagra::parse_pssh_data(&pssh_data)
664                .context("parsing Nagra PSSH data")?;
665            Ok(PsshBox {
666                version,
667                flags: version_and_flags & 0xF,
668                system_id,
669                key_ids,
670                pssh_data: PsshData::Nagra(pd),
671            })
672        },
673        WISEPLAY_SYSTEM_ID => {
674            let cdrm_pssh_data = wiseplay::parse_pssh_data(&pssh_data)
675                .context("parsing WisePlay PSSH data")?;
676            Ok(PsshBox {
677                version,
678                flags: version_and_flags & 0xF,
679                system_id,
680                key_ids,
681                pssh_data: PsshData::WisePlay(cdrm_pssh_data),
682            })
683        },
684        COMMON_SYSTEM_ID => {
685            Ok(PsshBox {
686                version,
687                flags: version_and_flags & 0xF,
688                system_id,
689                key_ids,
690                pssh_data: PsshData::CommonEnc(pssh_data),
691            })
692        },
693        FAIRPLAYNFLX_SYSTEM_ID => {
694            Ok(PsshBox {
695                version,
696                flags: version_and_flags & 0xF,
697                system_id,
698                key_ids,
699                pssh_data: PsshData::FairPlay(pssh_data),
700            })
701        },
702        _ => Err(anyhow!("can't parse this system_id type: {:?}", system_id)),
703    }
704}
705
706/// Read one or more PSSH boxes from some initialization data provided as a slice of octets,
707/// returning an error if any non-PSSH data is found in the slice or if the parsing fails.
708pub fn from_bytes(init_data: &[u8]) -> Result<PsshBoxVec> {
709    let total_len = init_data.len();
710    let mut rdr = Cursor::new(init_data);
711    let mut boxes = PsshBoxVec::new();
712    while (rdr.position() as usize) < total_len - 1  {
713        let bx = read_pssh_box(&mut rdr)?;
714        boxes.add(bx.clone());
715        trace!("Read one box {bx} from bytes, remaining {} octets", total_len as u64 - rdr.position());
716        let pos = rdr.position() as usize;
717        if let Some(remaining) = &rdr.get_ref().get(pos..total_len) {
718            // skip over any octets that are NULL
719            if remaining.iter().all(|b| *b == 0) {
720                break;
721            }
722        }
723    }
724    Ok(boxes)
725}
726
727/// Read one or more PSSH boxes from a slice of octets, stopping (but not returning an error) when
728/// non-PSSH data is found in the slice. An error is returned if the parsing fails.
729pub fn from_buffer(init_data: &[u8]) -> Result<PsshBoxVec> {
730    let total_len = init_data.len();
731    let mut rdr = Cursor::new(init_data);
732    let mut boxes = PsshBoxVec::new();
733    while (rdr.position() as usize) < total_len - 1  {
734        if let Ok(bx) = read_pssh_box(&mut rdr) {
735            boxes.add(bx);
736        } else {
737            break;
738        }
739    }
740    Ok(boxes)
741}
742
743/// Locate the positions of a PsshBox in a buffer, if present. Returns an iterator over start
744/// positions for PSSH boxes in the buffer.
745pub fn find_iter(buffer: &[u8]) -> impl Iterator<Item = usize> + '_ {
746    use bstr::ByteSlice;
747
748    buffer.find_iter(b"pssh")
749        .filter(|offset| {
750            if offset+24 > buffer.len() {
751                return false;
752            }
753            let start = offset - 4;
754            let mut rdr = Cursor::new(&buffer[start..]);
755            let size: u32 = rdr.read_u32::<BigEndian>().unwrap();
756            let end = start + size as usize;
757            from_bytes(&buffer[start..end]).is_ok()
758        })
759        .map(|offset| offset - 4)
760}
761
762/// Multiline pretty printing of a PsshBox (verbose alternative to `to_string()` method).
763pub fn pprint(pssh: &PsshBox) {
764    println!("PSSH Box v{}", pssh.version);
765    println!("  SystemID: {}", pssh.system_id);
766    if pssh.version == 1 {
767        for key in &pssh.key_ids {
768            println!("  Key ID: {}", key);
769        }
770    }
771    match &pssh.pssh_data {
772        PsshData::Widevine(wv) => println!("  {wv:?}"),
773        PsshData::PlayReady(pr) => println!("  {pr:?}"),
774        PsshData::Irdeto(pd) => {
775            println!("Irdeto XML: {}", pd.xml);
776        },
777        PsshData::Marlin(pd) => {
778            println!("  Marlin PSSH data ({} octets)", pd.len());
779            if !pd.is_empty() {
780                println!("== Hexdump of pssh data ==");
781                let mut hxbuf = Vec::new();
782                hxdmp::hexdump(pd, &mut hxbuf).unwrap();
783                println!("{}", String::from_utf8_lossy(&hxbuf));
784            }
785        },
786        PsshData::Nagra(pd) => println!("  {pd:?}"),
787        PsshData::WisePlay(pd) => {
788            println!("  WisePlay JSON: {}", pd.json);
789        },
790        PsshData::CommonEnc(pd) => {
791            println!("  Common PSSH data ({} octets)", pd.len());
792            if !pd.is_empty() {
793                println!("== Hexdump of pssh data ==");
794                let mut hxbuf = Vec::new();
795                hxdmp::hexdump(pd, &mut hxbuf).unwrap();
796                println!("{}", String::from_utf8_lossy(&hxbuf));
797            }
798        },
799        PsshData::FairPlay(pd) => {
800            println!("  FairPlay PSSH data ({} octets)", pd.len());
801            if !pd.is_empty() {
802                println!("== Hexdump of pssh data ==");
803                let mut hxbuf = Vec::new();
804                hxdmp::hexdump(pd, &mut hxbuf).unwrap();
805                println!("{}", String::from_utf8_lossy(&hxbuf));
806            }
807        },
808    }
809}