Skip to main content

st377_1/
types.rs

1//! MXF simple/compound data types — SMPTE ST 377-1:2019 §4.2/§4.3
2//! (`docs/st377-1.md`).
3
4extern crate alloc;
5
6use alloc::string::String;
7use alloc::vec::Vec;
8
9use crate::error::{Error, Result};
10
11/// A 16-byte SMPTE Universal Label or UUID.
12pub type UlBytes = [u8; 16];
13
14/// A Strong Reference (§5.4.4): a 16-byte UUID referencing another Set in the
15/// same file.  Identical on the wire to [`UlBytes`]; the alias documents the
16/// semantic role in property declarations.
17pub type StrongRef = UlBytes;
18
19/// A Rational number (§4.3) — two big-endian `Int32` values, 8 bytes total.
20/// Used for Edit Rate, Sample Rate, and similar time-base properties.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct Rational {
24    /// Numerator (signed 32-bit, big-endian on wire).
25    pub numerator: i32,
26    /// Denominator (signed 32-bit, big-endian on wire).
27    pub denominator: i32,
28}
29
30/// Wire size of [`Rational`] — always 8 bytes.
31pub const RATIONAL_LEN: usize = 8;
32
33impl Rational {
34    /// Parse 8 bytes as a Rational (two big-endian `Int32`).
35    pub fn parse(bytes: &[u8]) -> Result<Self> {
36        if bytes.len() != RATIONAL_LEN {
37            return Err(Error::InvalidPropertyLength {
38                tag: 0,
39                name: "Rational",
40                found: bytes.len(),
41                expected: RATIONAL_LEN,
42            });
43        }
44        Ok(Rational {
45            numerator: i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
46            denominator: i32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
47        })
48    }
49
50    /// Serialize into an 8-byte buffer.
51    pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
52        if buf.len() < RATIONAL_LEN {
53            return Err(Error::BufferTooShort {
54                need: RATIONAL_LEN,
55                have: buf.len(),
56                what: "Rational",
57            });
58        }
59        buf[0..4].copy_from_slice(&self.numerator.to_be_bytes());
60        buf[4..8].copy_from_slice(&self.denominator.to_be_bytes());
61        Ok(RATIONAL_LEN)
62    }
63}
64
65/// Copy the first 16 bytes of `bytes` into an owned [`UlBytes`].
66///
67/// Callers must have already verified `bytes.len() >= 16` (every call site
68/// does, immediately before calling this) — rather than a length-checked
69/// `try_into().expect(...)` (a panic-capable API call, even though
70/// unreachable here), this reads byte-by-byte via plain indexing, the
71/// established idiom throughout this workspace for a bounds-proven-safe
72/// fixed-size extraction.
73pub(crate) fn ul_bytes_from_prefix(bytes: &[u8]) -> UlBytes {
74    let mut out = [0u8; 16];
75    let mut i = 0;
76    while i < 16 {
77        out[i] = bytes[i];
78        i += 1;
79    }
80    out
81}
82
83/// A "Package ID" (§4.2): a 32-byte Basic UMID (SMPTE ST 330) or 32 zero
84/// bytes ("terminate a reference chain"). This crate treats the UMID's own
85/// internal bit layout as out of scope (ST 330 is a separate normative
86/// reference) — see `docs/st377-1.md`'s Scope section — and exposes it only
87/// as an opaque, fixed-size value.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub struct PackageId(#[cfg_attr(feature = "serde", serde(with = "serde_bytes32"))] pub [u8; 32]);
91
92impl PackageId {
93    /// The all-zero "reference chain terminator" value (§4.2).
94    pub const NULL: PackageId = PackageId([0u8; 32]);
95
96    /// True if this is the all-zero terminator value.
97    #[must_use]
98    pub fn is_null(&self) -> bool {
99        self.0 == [0u8; 32]
100    }
101}
102
103#[cfg(feature = "serde")]
104mod serde_bytes32 {
105    use serde::{Deserializer, Serializer};
106
107    pub fn serialize<S: Serializer>(v: &[u8; 32], s: S) -> core::result::Result<S::Ok, S::Error> {
108        s.serialize_bytes(v)
109    }
110
111    pub fn deserialize<'de, D: Deserializer<'de>>(
112        d: D,
113    ) -> core::result::Result<[u8; 32], D::Error> {
114        let bytes = serde_bytes_vec::deserialize(d)?;
115        <[u8; 32]>::try_from(bytes.as_slice())
116            .map_err(|_| serde::de::Error::custom("PackageId must be 32 bytes"))
117    }
118
119    mod serde_bytes_vec {
120        use alloc::vec::Vec;
121        use serde::Deserialize;
122        pub fn deserialize<'de, D: serde::Deserializer<'de>>(
123            d: D,
124        ) -> core::result::Result<Vec<u8>, D::Error> {
125            Vec::<u8>::deserialize(d)
126        }
127    }
128}
129
130/// An AUID (§4.2.1): a 16-byte field holding either a UL or a UUID,
131/// distinguished by the top bit of byte 0 (`0` = UL, stored value-order;
132/// `1` = UUID, stored with its top/bottom 8 bytes swapped from natural UUID
133/// order).
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136pub struct Auid(pub UlBytes);
137
138impl Auid {
139    /// True if this AUID holds a UL (byte 0's top bit clear).
140    #[must_use]
141    pub fn is_ul(&self) -> bool {
142        self.0[0] & 0x80 == 0
143    }
144
145    /// This AUID's bytes, interpreted as a UL (no transform — only
146    /// meaningful when [`Self::is_ul`]).
147    #[must_use]
148    pub fn as_ul_bytes(&self) -> UlBytes {
149        self.0
150    }
151
152    /// This AUID's bytes, interpreted as a UUID: swaps the top/bottom 8
153    /// bytes back to natural UUID storage order (§4.2.1, Table 2) — only
154    /// meaningful when [`Self::is_ul`] is false.
155    #[must_use]
156    pub fn as_uuid_bytes(&self) -> UlBytes {
157        let mut out = [0u8; 16];
158        out[..8].copy_from_slice(&self.0[8..]);
159        out[8..].copy_from_slice(&self.0[..8]);
160        out
161    }
162
163    /// Build an AUID from a UL (stored as-is).
164    #[must_use]
165    pub fn from_ul(ul: UlBytes) -> Self {
166        Auid(ul)
167    }
168
169    /// Build an AUID from a natural-order UUID (top/bottom 8 bytes swapped
170    /// on storage per §4.2.1).
171    #[must_use]
172    pub fn from_uuid(uuid: UlBytes) -> Self {
173        let mut out = [0u8; 16];
174        out[..8].copy_from_slice(&uuid[8..]);
175        out[8..].copy_from_slice(&uuid[..8]);
176        Auid(out)
177    }
178}
179
180/// A Gregorian timestamp (§4.3): `year: Int16, month/day/hour/minute/second/
181/// msec_div4: UInt8`, big-endian, 8 bytes total. All-zero means "unknown".
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184pub struct MxfTimestamp {
185    /// Year (may be negative per the `Int16` wire type, though the
186    /// Gregorian calendar this represents does not use negative years).
187    pub year: i16,
188    /// Month, 1-12 (0 in the all-zero "unknown" sentinel).
189    pub month: u8,
190    /// Day of month, 1-31 (0 in the all-zero "unknown" sentinel).
191    pub day: u8,
192    /// Hour, 0-23.
193    pub hour: u8,
194    /// Minute, 0-59.
195    pub minute: u8,
196    /// Second, 0-59.
197    pub second: u8,
198    /// Quarter-milliseconds (`msec / 4`), 0-249.
199    pub msec_div4: u8,
200}
201
202/// Wire size of [`MxfTimestamp`] — always 8 bytes.
203pub const TIMESTAMP_LEN: usize = 8;
204
205impl MxfTimestamp {
206    /// The all-zero "unknown" sentinel (§4.3 — "should not be used unless
207    /// unavoidable").
208    pub const UNKNOWN: MxfTimestamp = MxfTimestamp {
209        year: 0,
210        month: 0,
211        day: 0,
212        hour: 0,
213        minute: 0,
214        second: 0,
215        msec_div4: 0,
216    };
217
218    /// Parse 8 bytes as a Timestamp.
219    pub fn parse(bytes: &[u8]) -> Result<Self> {
220        if bytes.len() != TIMESTAMP_LEN {
221            return Err(Error::InvalidPropertyLength {
222                tag: 0,
223                name: "Timestamp",
224                found: bytes.len(),
225                expected: TIMESTAMP_LEN,
226            });
227        }
228        Ok(MxfTimestamp {
229            year: i16::from_be_bytes([bytes[0], bytes[1]]),
230            month: bytes[2],
231            day: bytes[3],
232            hour: bytes[4],
233            minute: bytes[5],
234            second: bytes[6],
235            msec_div4: bytes[7],
236        })
237    }
238
239    /// Serialize into an 8-byte buffer.
240    pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
241        if buf.len() < TIMESTAMP_LEN {
242            return Err(Error::BufferTooShort {
243                need: TIMESTAMP_LEN,
244                have: buf.len(),
245                what: "Timestamp",
246            });
247        }
248        let yb = self.year.to_be_bytes();
249        buf[0] = yb[0];
250        buf[1] = yb[1];
251        buf[2] = self.month;
252        buf[3] = self.day;
253        buf[4] = self.hour;
254        buf[5] = self.minute;
255        buf[6] = self.second;
256        buf[7] = self.msec_div4;
257        Ok(TIMESTAMP_LEN)
258    }
259}
260
261/// ProductVersion's `release` field enumeration (§4.3).
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
263#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
264#[non_exhaustive]
265pub enum ReleaseType {
266    /// `0` — Unknown version.
267    Unknown,
268    /// `1` — Released version.
269    Released,
270    /// `2` — Development version.
271    Development,
272    /// `3` — Released version with patches.
273    ReleasedWithPatches,
274    /// `4` — Pre-release beta version.
275    PreReleaseBeta,
276    /// `5` — Private version, not intended for general release.
277    Private,
278    /// Any other value — not defined by §4.3.
279    Reserved(u16),
280}
281
282impl ReleaseType {
283    /// The spec's own label (§4.3's enumeration list).
284    #[must_use]
285    pub fn name(&self) -> &'static str {
286        match self {
287            Self::Unknown => "unknown version",
288            Self::Released => "released version",
289            Self::Development => "development version",
290            Self::ReleasedWithPatches => "released version with patches",
291            Self::PreReleaseBeta => "pre-release beta version",
292            Self::Private => "private version",
293            Self::Reserved(_) => "reserved",
294        }
295    }
296
297    /// Decode from the wire `UInt16` value.
298    #[must_use]
299    pub fn from_u16(v: u16) -> Self {
300        match v {
301            0 => Self::Unknown,
302            1 => Self::Released,
303            2 => Self::Development,
304            3 => Self::ReleasedWithPatches,
305            4 => Self::PreReleaseBeta,
306            5 => Self::Private,
307            other => Self::Reserved(other),
308        }
309    }
310
311    /// Encode to the wire `UInt16` value.
312    #[must_use]
313    pub fn to_u16(self) -> u16 {
314        match self {
315            Self::Unknown => 0,
316            Self::Released => 1,
317            Self::Development => 2,
318            Self::ReleasedWithPatches => 3,
319            Self::PreReleaseBeta => 4,
320            Self::Private => 5,
321            Self::Reserved(v) => v,
322        }
323    }
324}
325
326broadcast_common::impl_spec_display!(ReleaseType, Reserved);
327
328/// Wire size of [`ProductVersion`] — always 10 bytes.
329pub const PRODUCT_VERSION_LEN: usize = 10;
330
331/// A tool/product version number (§4.3): 5 big-endian `UInt16` fields.
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
333#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
334pub struct ProductVersion {
335    /// Major version.
336    pub major: u16,
337    /// Minor version.
338    pub minor: u16,
339    /// Tertiary version.
340    pub tertiary: u16,
341    /// Patch version.
342    pub patch: u16,
343    /// Release kind (§4.3's 0-5 enumeration).
344    pub release: ReleaseType,
345}
346
347impl ProductVersion {
348    /// Parse 10 bytes as a ProductVersion.
349    pub fn parse(bytes: &[u8]) -> Result<Self> {
350        if bytes.len() != PRODUCT_VERSION_LEN {
351            return Err(Error::InvalidPropertyLength {
352                tag: 0,
353                name: "ProductVersion",
354                found: bytes.len(),
355                expected: PRODUCT_VERSION_LEN,
356            });
357        }
358        let u16_at = |i: usize| u16::from_be_bytes([bytes[i], bytes[i + 1]]);
359        Ok(ProductVersion {
360            major: u16_at(0),
361            minor: u16_at(2),
362            tertiary: u16_at(4),
363            patch: u16_at(6),
364            release: ReleaseType::from_u16(u16_at(8)),
365        })
366    }
367
368    /// Serialize into a 10-byte buffer.
369    pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
370        if buf.len() < PRODUCT_VERSION_LEN {
371            return Err(Error::BufferTooShort {
372                need: PRODUCT_VERSION_LEN,
373                have: buf.len(),
374                what: "ProductVersion",
375            });
376        }
377        let put =
378            |buf: &mut [u8], i: usize, v: u16| buf[i..i + 2].copy_from_slice(&v.to_be_bytes());
379        put(buf, 0, self.major);
380        put(buf, 2, self.minor);
381        put(buf, 4, self.tertiary);
382        put(buf, 6, self.patch);
383        put(buf, 8, self.release.to_u16());
384        Ok(PRODUCT_VERSION_LEN)
385    }
386}
387
388/// Decode a big-endian UTF-16 string (§4.3 "String") into an owned `String`.
389pub fn decode_utf16_be(bytes: &[u8]) -> Result<String> {
390    if !bytes.len().is_multiple_of(2) {
391        return Err(Error::InvalidUtf16 {
392            tag: 0,
393            name: "UTF-16 string",
394        });
395    }
396    let units: Vec<u16> = bytes
397        .chunks_exact(2)
398        .map(|c| u16::from_be_bytes([c[0], c[1]]))
399        .collect();
400    char::decode_utf16(units)
401        .collect::<core::result::Result<String, _>>()
402        .map_err(|_| Error::InvalidUtf16 {
403            tag: 0,
404            name: "UTF-16 string",
405        })
406}
407
408/// Encode a string as big-endian UTF-16 (§4.3 "String").
409#[must_use]
410pub fn encode_utf16_be(s: &str) -> Vec<u8> {
411    let mut out = Vec::with_capacity(s.len() * 2);
412    for unit in s.encode_utf16() {
413        out.extend_from_slice(&unit.to_be_bytes());
414    }
415    out
416}
417
418/// Parse a Batch/Array of 16-byte elements (§4.3): 8-byte header
419/// (`count: u32`, `item_len: u32`, both big-endian) followed by `count`
420/// 16-byte elements. Used for every UL/StrongRef Batch or Array in the
421/// Root Metadata Sets (`EssenceContainers`, `DMSchemes`, `Identifications`,
422/// `Packages`, `EssenceContainerData`).
423pub fn parse_uid_batch(bytes: &[u8]) -> Result<Vec<UlBytes>> {
424    if bytes.is_empty() {
425        // A zero-length property (no header at all) is treated as an empty
426        // batch — some encoders omit the property entirely rather than
427        // emit an empty 8-byte header; parse of an explicit empty header
428        // is handled by the `count == 0` branch below.
429        return Ok(Vec::new());
430    }
431    if bytes.len() < 8 {
432        return Err(Error::InvalidBatchHeader {
433            count: 0,
434            item_len: 0,
435            buffer_len: bytes.len(),
436        });
437    }
438    let count = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
439    let item_len = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
440    let body = &bytes[8..];
441    // `item_len` only describes the size of an actual element, so it is only
442    // meaningful (and validated against 16) when `count > 0`. A real
443    // encoder-written empty Batch (`count == 0`) has been observed with
444    // `item_len == 0` rather than 16 (ffmpeg's OP1a Preface `DMSchemes`,
445    // `tests/fixtures/op1a_mpeg2_pcm.mxf`) — this crate previously rejected
446    // that as `InvalidBatchHeader`, even though `count == 0` unambiguously
447    // means "no elements" regardless of the stated element size.
448    if (count > 0 && item_len != 16) || body.len() != count as usize * 16 {
449        return Err(Error::InvalidBatchHeader {
450            count,
451            item_len,
452            buffer_len: body.len(),
453        });
454    }
455    let mut out = Vec::with_capacity(count as usize);
456    for chunk in body.chunks_exact(16) {
457        out.push(ul_bytes_from_prefix(chunk));
458    }
459    Ok(out)
460}
461
462/// Serialize a Batch/Array of 16-byte elements (§4.3) — see
463/// [`parse_uid_batch`]. Always writes `item_len = 16`: it names the size of
464/// each *element type* in the batch, not the instance count, so this crate
465/// keeps it constant even when `count == 0`. (Some real encoders write
466/// `item_len = 0` for an empty batch instead — e.g. `ffmpeg`'s OP1a Preface
467/// `DMSchemes` in `tests/fixtures/op1a_mpeg2_pcm.mxf` — which
468/// [`parse_uid_batch`] tolerates on input; this crate's own output stays
469/// canonical.)
470#[must_use]
471pub fn serialize_uid_batch(items: &[UlBytes]) -> Vec<u8> {
472    let mut out = Vec::with_capacity(8 + items.len() * 16);
473    out.extend_from_slice(&(items.len() as u32).to_be_bytes());
474    out.extend_from_slice(&16u32.to_be_bytes());
475    for item in items {
476        out.extend_from_slice(item);
477    }
478    out
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use alloc::string::ToString;
485
486    #[test]
487    fn auid_ul_round_trip() {
488        let ul: UlBytes = [
489            0x06, 0x0E, 0x2B, 0x34, 0x01, 0x01, 0x01, 0x0E, 0x04, 0x04, 0x05, 0x03, 0, 0, 0, 0,
490        ];
491        let auid = Auid::from_ul(ul);
492        assert!(auid.is_ul());
493        assert_eq!(auid.as_ul_bytes(), ul);
494    }
495
496    #[test]
497    fn auid_uuid_round_trip() {
498        let uuid: UlBytes = [
499            0x07, 0x72, 0x26, 0x2E, 0x76, 0x55, 0x43, 0x6F, 0x8F, 0xF3, 0x8A, 0xC5, 0x1B, 0x77,
500            0x1E, 0x02,
501        ];
502        let auid = Auid::from_uuid(uuid);
503        assert!(!auid.is_ul());
504        // Spec Table 2 worked example: AUID storage order is
505        // 8F.F3.8A.C5.1B.77.1E.02.07.72.26.2E.76.55.43.6F
506        assert_eq!(
507            auid.0,
508            [
509                0x8F, 0xF3, 0x8A, 0xC5, 0x1B, 0x77, 0x1E, 0x02, 0x07, 0x72, 0x26, 0x2E, 0x76, 0x55,
510                0x43, 0x6F
511            ]
512        );
513        assert_eq!(auid.as_uuid_bytes(), uuid);
514    }
515
516    #[test]
517    fn timestamp_round_trip() {
518        let ts = MxfTimestamp {
519            year: 2019,
520            month: 11,
521            day: 28,
522            hour: 12,
523            minute: 34,
524            second: 56,
525            msec_div4: 10,
526        };
527        let mut buf = [0u8; TIMESTAMP_LEN];
528        ts.serialize_into(&mut buf).unwrap();
529        assert_eq!(MxfTimestamp::parse(&buf).unwrap(), ts);
530    }
531
532    #[test]
533    fn product_version_round_trip() {
534        let pv = ProductVersion {
535            major: 1,
536            minor: 2,
537            tertiary: 3,
538            patch: 4,
539            release: ReleaseType::Released,
540        };
541        let mut buf = [0u8; PRODUCT_VERSION_LEN];
542        pv.serialize_into(&mut buf).unwrap();
543        assert_eq!(ProductVersion::parse(&buf).unwrap(), pv);
544    }
545
546    #[test]
547    fn reserved_release_type_round_trips_value() {
548        let pv = ProductVersion {
549            major: 0,
550            minor: 0,
551            tertiary: 0,
552            patch: 0,
553            release: ReleaseType::from_u16(42),
554        };
555        assert_eq!(pv.release, ReleaseType::Reserved(42));
556        assert_eq!(pv.release.to_u16(), 42);
557        assert_eq!(pv.release.to_string(), "reserved(0x2A)");
558    }
559
560    #[test]
561    fn utf16_round_trip() {
562        let s = "MXF \u{1F3AC}"; // includes a surrogate-pair codepoint
563        let bytes = encode_utf16_be(s);
564        assert_eq!(decode_utf16_be(&bytes).unwrap(), s);
565    }
566
567    #[test]
568    fn rational_round_trip() {
569        let r = Rational {
570            numerator: 25,
571            denominator: 1,
572        };
573        let mut buf = [0u8; RATIONAL_LEN];
574        r.serialize_into(&mut buf).unwrap();
575        assert_eq!(Rational::parse(&buf).unwrap(), r);
576    }
577
578    #[test]
579    fn rational_negative_values_round_trip() {
580        let r = Rational {
581            numerator: -30000,
582            denominator: 1001,
583        };
584        let mut buf = [0u8; RATIONAL_LEN];
585        r.serialize_into(&mut buf).unwrap();
586        assert_eq!(Rational::parse(&buf).unwrap(), r);
587    }
588
589    #[test]
590    fn uid_batch_round_trip() {
591        let items = alloc::vec![[1u8; 16], [2u8; 16], [3u8; 16]];
592        let bytes = serialize_uid_batch(&items);
593        assert_eq!(parse_uid_batch(&bytes).unwrap(), items);
594    }
595
596    #[test]
597    fn empty_uid_batch_round_trip() {
598        let items: Vec<UlBytes> = Vec::new();
599        let bytes = serialize_uid_batch(&items);
600        assert_eq!(bytes.len(), 8);
601        assert_eq!(parse_uid_batch(&bytes).unwrap(), items);
602    }
603}