Skip to main content

st377_1/
package.rs

1//! Material Package and Source Package — SMPTE ST 377-1:2019 Annex B §B.1/
2//! E.1-E.4 (`docs/st377-1.md`): the two concrete Package kinds every MXF
3//! file uses.
4//!
5//! Both inherit the Generic Package properties (B.1): Package UID, Name,
6//! Creation Date, Modified Date, and a Tracks batch.  `SourcePackage` adds a
7//! `Descriptor` strong reference (§E.2).
8//!
9//! `MaterialPackage` — byte 14/15 = `0x01`/`0x36`.
10//! `SourcePackage`   — byte 14/15 = `0x01`/`0x37`.
11
12extern crate alloc;
13
14use alloc::string::String;
15use alloc::vec::Vec;
16
17use broadcast_common::{Parse, Serialize};
18
19use crate::error::{Error, Result};
20use crate::local_set::{LocalSet, StructuralSetKind};
21use crate::sets::{
22    InterchangeObjectFields, LocalSetOwnedItem, collect_dark, finish_owned_set, get_optional_raw,
23    get_required_fixed, get_required_raw, owned_set_serialized_len, serialize_owned_set,
24};
25use crate::types::{
26    MxfTimestamp, PackageId, StrongRef, TIMESTAMP_LEN, decode_utf16_be, encode_utf16_be,
27    parse_uid_batch, serialize_uid_batch,
28};
29
30// ── Generic Package local tags (B.1) ────────────────────────────────────
31
32/// Local tag: Package UID (B.1) — 32-byte UMID.
33pub const TAG_PACKAGE_UID: u16 = 0x4401;
34/// Local tag: Name (B.1) — UTF-16 string, optional.
35pub const TAG_NAME: u16 = 0x4402;
36/// Local tag: Tracks (B.1) — Array of StrongRef.
37pub const TAG_TRACKS: u16 = 0x4403;
38/// Local tag: Package Modified Date (B.1).
39pub const TAG_MODIFIED_DATE: u16 = 0x4404;
40/// Local tag: Package Creation Date (B.1).
41pub const TAG_CREATION_DATE: u16 = 0x4405;
42
43// ── Source Package additional tag (E.2) ─────────────────────────────────
44
45/// Local tag: Descriptor (E.2) — StrongRef to the top-level Descriptor.
46pub const TAG_DESCRIPTOR: u16 = 0x4701;
47
48// ── Known-tag tables ────────────────────────────────────────────────────
49
50const MATERIAL_KNOWN_TAGS: [u16; 8] = [
51    crate::sets::TAG_INSTANCE_UID,
52    crate::sets::TAG_GENERATION_UID,
53    crate::sets::TAG_OBJECT_CLASS,
54    TAG_PACKAGE_UID,
55    TAG_NAME,
56    TAG_TRACKS,
57    TAG_MODIFIED_DATE,
58    TAG_CREATION_DATE,
59];
60
61const SOURCE_KNOWN_TAGS: [u16; 9] = [
62    crate::sets::TAG_INSTANCE_UID,
63    crate::sets::TAG_GENERATION_UID,
64    crate::sets::TAG_OBJECT_CLASS,
65    TAG_PACKAGE_UID,
66    TAG_NAME,
67    TAG_TRACKS,
68    TAG_MODIFIED_DATE,
69    TAG_CREATION_DATE,
70    TAG_DESCRIPTOR,
71];
72
73// ═══════════════════════════════════════════════════════════════════════
74// MaterialPackage
75// ═══════════════════════════════════════════════════════════════════════
76
77/// The Material Package Set — SMPTE ST 377-1:2019 Annex E §E.1 (byte
78/// 14/15 = `0x01`/`0x36`): the top-level composition that describes the
79/// final timeline of the file.  Carries only the Generic Package
80/// properties (B.1) — no additional fields.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct MaterialPackage {
83    /// Interchange Object (A.1) base properties.
84    pub interchange: InterchangeObjectFields,
85    /// Package UID (`0x4401`, Req) — 32-byte UMID.
86    pub package_uid: PackageId,
87    /// Name (`0x4402`, Opt) — human-readable package name.
88    pub name: Option<String>,
89    /// Package Creation Date (`0x4405`, Req).
90    pub creation_date: MxfTimestamp,
91    /// Package Modified Date (`0x4404`, Req).
92    pub modified_date: MxfTimestamp,
93    /// Tracks (`0x4403`, Req) — strong references to this package's Tracks.
94    pub tracks: Vec<StrongRef>,
95    /// Unrecognized properties preserved for round-trip fidelity.
96    pub dark: Vec<(u16, Vec<u8>)>,
97}
98
99impl<'a> Parse<'a> for MaterialPackage {
100    type Error = Error;
101
102    fn parse(bytes: &'a [u8]) -> Result<Self> {
103        let set = LocalSet::parse(bytes)?;
104        if set.kind() != StructuralSetKind::MaterialPackage {
105            return Err(Error::KeyPrefixMismatch {
106                what: "Material Package (Table 17)",
107            });
108        }
109        let items = &set.items;
110        let interchange = InterchangeObjectFields::decode(items, "Material Package")?;
111        let package_uid = PackageId(get_required_fixed::<32>(
112            items,
113            TAG_PACKAGE_UID,
114            "Package UID",
115            "Material Package",
116        )?);
117        let name = get_optional_raw(items, TAG_NAME)
118            .map(decode_utf16_be)
119            .transpose()
120            .map_err(|_| Error::InvalidUtf16 {
121                tag: TAG_NAME,
122                name: "Name",
123            })?;
124        let creation_date = MxfTimestamp::parse(get_required_raw(
125            items,
126            TAG_CREATION_DATE,
127            "Package Creation Date",
128            "Material Package",
129        )?)?;
130        let modified_date = MxfTimestamp::parse(get_required_raw(
131            items,
132            TAG_MODIFIED_DATE,
133            "Package Modified Date",
134            "Material Package",
135        )?)?;
136        let tracks = parse_uid_batch(get_required_raw(
137            items,
138            TAG_TRACKS,
139            "Tracks",
140            "Material Package",
141        )?)?;
142        let dark = collect_dark(items, &MATERIAL_KNOWN_TAGS);
143
144        Ok(MaterialPackage {
145            interchange,
146            package_uid,
147            name,
148            creation_date,
149            modified_date,
150            tracks,
151            dark,
152        })
153    }
154}
155
156impl MaterialPackage {
157    fn owned_items(&self) -> Vec<LocalSetOwnedItem> {
158        let mut out = Vec::new();
159        self.interchange.encode_into(&mut out);
160        out.push(LocalSetOwnedItem::fixed(
161            TAG_PACKAGE_UID,
162            self.package_uid.0,
163        ));
164        if let Some(n) = &self.name {
165            out.push(LocalSetOwnedItem::owned(TAG_NAME, encode_utf16_be(n)));
166        }
167        encode_timestamp_item(&mut out, TAG_CREATION_DATE, &self.creation_date);
168        encode_timestamp_item(&mut out, TAG_MODIFIED_DATE, &self.modified_date);
169        out.push(LocalSetOwnedItem::owned(
170            TAG_TRACKS,
171            serialize_uid_batch(&self.tracks),
172        ));
173        out
174    }
175}
176
177impl Serialize for MaterialPackage {
178    type Error = Error;
179
180    fn serialized_len(&self) -> usize {
181        let (key, items) = finish_owned_set(
182            StructuralSetKind::MaterialPackage,
183            self.owned_items(),
184            &self.dark,
185        );
186        owned_set_serialized_len(key, &items)
187    }
188
189    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
190        let (key, items) = finish_owned_set(
191            StructuralSetKind::MaterialPackage,
192            self.owned_items(),
193            &self.dark,
194        );
195        serialize_owned_set(key, &items, buf)
196    }
197}
198
199// ═══════════════════════════════════════════════════════════════════════
200// SourcePackage
201// ═══════════════════════════════════════════════════════════════════════
202
203/// The Source Package Set — SMPTE ST 377-1:2019 Annex E §E.2 (byte
204/// 14/15 = `0x01`/`0x37`): references the file's actual essence via a
205/// `Descriptor` strong reference, plus the Generic Package properties
206/// (B.1).
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct SourcePackage {
209    /// Interchange Object (A.1) base properties.
210    pub interchange: InterchangeObjectFields,
211    /// Package UID (`0x4401`, Req) — 32-byte UMID.
212    pub package_uid: PackageId,
213    /// Name (`0x4402`, Opt) — human-readable package name.
214    pub name: Option<String>,
215    /// Package Creation Date (`0x4405`, Req).
216    pub creation_date: MxfTimestamp,
217    /// Package Modified Date (`0x4404`, Req).
218    pub modified_date: MxfTimestamp,
219    /// Tracks (`0x4403`, Req) — strong references to this package's Tracks.
220    pub tracks: Vec<StrongRef>,
221    /// Descriptor (`0x4701`, Req) — strong reference to the top-level
222    /// Descriptor (or Multiple Descriptor) for this package's essence.
223    ///
224    /// This crate has no typed `EssenceDescriptor` (see the crate root
225    /// docs' "OP1a support is structural-metadata-only" section): this is
226    /// the raw 16-byte Instance UID, opaque here, not a value this crate
227    /// can dereference to the target Descriptor Set.
228    pub descriptor: StrongRef,
229    /// Unrecognized properties preserved for round-trip fidelity.
230    pub dark: Vec<(u16, Vec<u8>)>,
231}
232
233impl<'a> Parse<'a> for SourcePackage {
234    type Error = Error;
235
236    fn parse(bytes: &'a [u8]) -> Result<Self> {
237        let set = LocalSet::parse(bytes)?;
238        if set.kind() != StructuralSetKind::SourcePackage {
239            return Err(Error::KeyPrefixMismatch {
240                what: "Source Package (Table 17)",
241            });
242        }
243        let items = &set.items;
244        let interchange = InterchangeObjectFields::decode(items, "Source Package")?;
245        let package_uid = PackageId(get_required_fixed::<32>(
246            items,
247            TAG_PACKAGE_UID,
248            "Package UID",
249            "Source Package",
250        )?);
251        let name = get_optional_raw(items, TAG_NAME)
252            .map(decode_utf16_be)
253            .transpose()
254            .map_err(|_| Error::InvalidUtf16 {
255                tag: TAG_NAME,
256                name: "Name",
257            })?;
258        let creation_date = MxfTimestamp::parse(get_required_raw(
259            items,
260            TAG_CREATION_DATE,
261            "Package Creation Date",
262            "Source Package",
263        )?)?;
264        let modified_date = MxfTimestamp::parse(get_required_raw(
265            items,
266            TAG_MODIFIED_DATE,
267            "Package Modified Date",
268            "Source Package",
269        )?)?;
270        let tracks = parse_uid_batch(get_required_raw(
271            items,
272            TAG_TRACKS,
273            "Tracks",
274            "Source Package",
275        )?)?;
276        let descriptor =
277            get_required_fixed::<16>(items, TAG_DESCRIPTOR, "Descriptor", "Source Package")?;
278        let dark = collect_dark(items, &SOURCE_KNOWN_TAGS);
279
280        Ok(SourcePackage {
281            interchange,
282            package_uid,
283            name,
284            creation_date,
285            modified_date,
286            tracks,
287            descriptor,
288            dark,
289        })
290    }
291}
292
293impl SourcePackage {
294    fn owned_items(&self) -> Vec<LocalSetOwnedItem> {
295        let mut out = Vec::new();
296        self.interchange.encode_into(&mut out);
297        out.push(LocalSetOwnedItem::fixed(
298            TAG_PACKAGE_UID,
299            self.package_uid.0,
300        ));
301        if let Some(n) = &self.name {
302            out.push(LocalSetOwnedItem::owned(TAG_NAME, encode_utf16_be(n)));
303        }
304        encode_timestamp_item(&mut out, TAG_CREATION_DATE, &self.creation_date);
305        encode_timestamp_item(&mut out, TAG_MODIFIED_DATE, &self.modified_date);
306        out.push(LocalSetOwnedItem::owned(
307            TAG_TRACKS,
308            serialize_uid_batch(&self.tracks),
309        ));
310        out.push(LocalSetOwnedItem::fixed(TAG_DESCRIPTOR, self.descriptor));
311        out
312    }
313}
314
315impl Serialize for SourcePackage {
316    type Error = Error;
317
318    fn serialized_len(&self) -> usize {
319        let (key, items) = finish_owned_set(
320            StructuralSetKind::SourcePackage,
321            self.owned_items(),
322            &self.dark,
323        );
324        owned_set_serialized_len(key, &items)
325    }
326
327    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
328        let (key, items) = finish_owned_set(
329            StructuralSetKind::SourcePackage,
330            self.owned_items(),
331            &self.dark,
332        );
333        serialize_owned_set(key, &items, buf)
334    }
335}
336
337// ── Shared helpers ──────────────────────────────────────────────────────
338
339/// Encode a [`MxfTimestamp`] into an owned local-set item.
340fn encode_timestamp_item(out: &mut Vec<LocalSetOwnedItem>, tag: u16, ts: &MxfTimestamp) {
341    let mut buf = [0u8; TIMESTAMP_LEN];
342    ts.serialize_into(&mut buf).expect("fixed-size buffer");
343    out.push(LocalSetOwnedItem::owned(tag, buf.to_vec()));
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    fn sample_timestamp() -> MxfTimestamp {
351        MxfTimestamp {
352            year: 2026,
353            month: 7,
354            day: 12,
355            hour: 10,
356            minute: 0,
357            second: 0,
358            msec_div4: 0,
359        }
360    }
361
362    fn sample_material_package() -> MaterialPackage {
363        MaterialPackage {
364            interchange: InterchangeObjectFields {
365                instance_uid: [0x11; 16],
366                generation_uid: Some([0x22; 16]),
367                object_class: None,
368            },
369            package_uid: PackageId([0x33; 32]),
370            name: Some(String::from("Main Timeline")),
371            creation_date: sample_timestamp(),
372            modified_date: sample_timestamp(),
373            tracks: alloc::vec![[0x44; 16], [0x55; 16]],
374            dark: Vec::new(),
375        }
376    }
377
378    fn sample_source_package() -> SourcePackage {
379        SourcePackage {
380            interchange: InterchangeObjectFields {
381                instance_uid: [0xAA; 16],
382                generation_uid: None,
383                object_class: None,
384            },
385            package_uid: PackageId([0xBB; 32]),
386            name: None,
387            creation_date: sample_timestamp(),
388            modified_date: sample_timestamp(),
389            tracks: alloc::vec![[0xCC; 16]],
390            descriptor: [0xDD; 16],
391            dark: Vec::new(),
392        }
393    }
394
395    #[test]
396    fn material_package_round_trip() {
397        let mp = sample_material_package();
398        let bytes = mp.to_bytes();
399        let parsed = MaterialPackage::parse(&bytes).unwrap();
400        assert_eq!(parsed, mp);
401        assert_eq!(parsed.to_bytes(), bytes);
402    }
403
404    #[test]
405    fn material_package_no_name_round_trip() {
406        let mut mp = sample_material_package();
407        mp.name = None;
408        let bytes = mp.to_bytes();
409        let parsed = MaterialPackage::parse(&bytes).unwrap();
410        assert_eq!(parsed.name, None);
411        assert_eq!(parsed.to_bytes(), bytes);
412    }
413
414    #[test]
415    fn material_package_dark_preserved() {
416        let mut mp = sample_material_package();
417        mp.dark = alloc::vec![(0x9001, alloc::vec![1, 2, 3])];
418        let bytes = mp.to_bytes();
419        let parsed = MaterialPackage::parse(&bytes).unwrap();
420        assert_eq!(parsed.dark, mp.dark);
421    }
422
423    #[test]
424    fn material_package_wrong_kind_rejected() {
425        let sp = sample_source_package();
426        let bytes = sp.to_bytes();
427        assert!(matches!(
428            MaterialPackage::parse(&bytes),
429            Err(Error::KeyPrefixMismatch { .. })
430        ));
431    }
432
433    #[test]
434    fn source_package_round_trip() {
435        let sp = sample_source_package();
436        let bytes = sp.to_bytes();
437        let parsed = SourcePackage::parse(&bytes).unwrap();
438        assert_eq!(parsed, sp);
439        assert_eq!(parsed.to_bytes(), bytes);
440    }
441
442    #[test]
443    fn source_package_with_name_round_trip() {
444        let mut sp = sample_source_package();
445        sp.name = Some(String::from("File Source"));
446        let bytes = sp.to_bytes();
447        let parsed = SourcePackage::parse(&bytes).unwrap();
448        assert_eq!(parsed.name.as_deref(), Some("File Source"));
449        assert_eq!(parsed.to_bytes(), bytes);
450    }
451
452    #[test]
453    fn source_package_wrong_kind_rejected() {
454        let mp = sample_material_package();
455        let bytes = mp.to_bytes();
456        assert!(matches!(
457            SourcePackage::parse(&bytes),
458            Err(Error::KeyPrefixMismatch { .. })
459        ));
460    }
461
462    #[test]
463    fn mutation_changes_serialized_bytes() {
464        let mut mp = sample_material_package();
465        let before = mp.to_bytes();
466        mp.tracks.push([0x66; 16]);
467        let after = mp.to_bytes();
468        assert_ne!(before, after);
469        assert_eq!(MaterialPackage::parse(&after).unwrap().tracks.len(), 3);
470    }
471}