Skip to main content

imferno_core/assetmap/
codes.rs

1//! Typed validation-code catalogue for SMPTE ST 2067-2.
2//!
3//! Two groups of codes live here:
4//!
5//! 1. **Package-level codes** (`St2067_2_2020`) — AssetMap / PKL / checksum checks
6//!    emitted by the package validator.
7//!
8//! 2. **Core Constraints CPL codes** (`St2067_2_2013_Core` / `St2067_2_2016_Core` /
9//!    `St2067_2_2020_Core`) — CPL structure rules (XSD, §6.1, §6.9, §6.10, §8, §10,
10//!    ST 377-4) shared across all three spec editions, emitted by the CoreConstraints
11//!    validators.  A `macro_rules!` generates the three enums from a
12//!    single source-of-truth variant list.  Each enum also exposes `for_code` so that
13//!    shared helper functions can produce the right `&'static str` without any runtime
14//!    string building.
15//!
16//! Also re-exports [`St429_9_2014`] from the volindex module.
17
18// Re-export St429_9_2014 from the volindex module for convenience.
19pub use crate::assetmap::volindex_codes::St429_9_2014;
20
21use crate::diagnostics::codes::ValidationCode;
22use crate::diagnostics::{Category, Severity};
23
24// ─────────────────────────────────────────────────────────────────────────────
25// ST 2067-2:2020 — Package-level codes
26// ─────────────────────────────────────────────────────────────────────────────
27
28/// Validation codes defined by SMPTE ST 2067-2:2020 for package-level checks
29/// (AssetMap, PKL, file integrity).
30#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
31pub enum St2067_2_2020 {
32    /// AssetMap document is invalid or cannot be parsed.
33    AssetMap,
34    /// The ASSETMAP.xml document is not well-formed XML (ST 2067-2:2020 §7).
35    AssetMapMalformedXml,
36    /// A Packing List document is not well-formed XML (ST 2067-2:2020 §9).
37    PklMalformedXml,
38    /// No CPL assets found in the AssetMap.
39    NoCpls,
40    /// Declared file size does not match the on-disk size.
41    SizeMismatch,
42    /// A referenced asset file is not present at the declared path.
43    FileNotFound,
44    /// File hash does not match the declared SHA-1/SHA-256 checksum.
45    ChecksumMismatch,
46    /// UUID referenced in the CPL does not resolve to a known asset.
47    UnresolvedUuid,
48    /// Two or more assets within the package share the same UUID.
49    DuplicateUuid,
50    /// An I/O error prevented the asset from being read.
51    IoError,
52    /// EssenceDescriptorList element is required per ST 2067-2:2020 §6.4.2.
53    EssenceDescriptorList,
54    /// PKL document namespace is not one of the published SMPTE PKL
55    /// namespace URIs — breaks interop with conformant validators.
56    PklUnknownNamespace,
57    /// AssetMap declares no asset as a PackingList — ST 429-9 §6.3
58    /// requires every package to have at least one PKL identified
59    /// by `<PackingList>true</PackingList>`.
60    AssetMapHasNoPackingList,
61    /// A PKL document was parsed but its Id is not declared in the
62    /// AssetMap as a `PackingList`-flagged asset (ST 429-9 §6.3).
63    PklIdNotInAssetMap,
64}
65
66impl ValidationCode for St2067_2_2020 {
67    fn code(&self) -> &'static str {
68        match self {
69            Self::AssetMap => "ST2067-2:2020:7/AssetMap",
70            Self::AssetMapMalformedXml => "ST2067-2:2020:7/MalformedXml",
71            Self::PklMalformedXml => "ST2067-2:2020:9/MalformedXml",
72            Self::NoCpls => "ST2067-2:2020:7/NoCpls",
73            Self::SizeMismatch => "ST2067-2:2020:8.3/SizeMismatch",
74            Self::FileNotFound => "ST2067-2:2020:8.3/FileNotFound",
75            Self::ChecksumMismatch => "ST2067-2:2020:8.3/ChecksumMismatch",
76            Self::UnresolvedUuid => "ST2067-2:2020:7/UnresolvedUuid",
77            Self::DuplicateUuid => "ST2067-2:2020:7/DuplicateUuid",
78            Self::IoError => "IMF:General/IoError",
79            Self::EssenceDescriptorList => "ST2067-2:2020:6.4.2/EssenceDescriptorList",
80            Self::PklUnknownNamespace => "ST2067-2:2020:9/PklUnknownNamespace",
81            Self::AssetMapHasNoPackingList => "ST429-9:2014:6.3/AssetMapHasNoPackingList",
82            Self::PklIdNotInAssetMap => "ST429-9:2014:6.3/PklIdNotInAssetMap",
83        }
84    }
85    fn description(&self) -> &'static str {
86        match self {
87            Self::AssetMap => "AssetMap document is invalid or cannot be parsed.",
88            Self::AssetMapMalformedXml => "The ASSETMAP.xml document is not well-formed XML.",
89            Self::PklMalformedXml => "A Packing List document is not well-formed XML.",
90            Self::NoCpls => "No CPL assets found in the AssetMap.",
91            Self::SizeMismatch => "Declared file size does not match the on-disk size.",
92            Self::FileNotFound => "A referenced asset file is not present at the declared path.",
93            Self::ChecksumMismatch => {
94                "File hash does not match the declared SHA-1/SHA-256 checksum."
95            }
96            Self::UnresolvedUuid => "UUID referenced in the CPL does not resolve to a known asset.",
97            Self::DuplicateUuid => "Two or more assets within the package share the same UUID.",
98            Self::IoError => "An I/O error prevented the asset from being read.",
99            Self::EssenceDescriptorList => {
100                "EssenceDescriptorList element is required per ST 2067-2:2020 §6.4.2."
101            }
102            Self::PklUnknownNamespace => {
103                "PKL document namespace is not one of the published SMPTE PKL namespaces."
104            }
105            Self::AssetMapHasNoPackingList => {
106                "AssetMap declares no asset as a PackingList (ST 429-9 §6.3)."
107            }
108            Self::PklIdNotInAssetMap => {
109                "PKL document Id is not declared as a PackingList asset in the AssetMap."
110            }
111        }
112    }
113    fn default_severity(&self) -> Severity {
114        match self {
115            Self::AssetMap | Self::NoCpls | Self::AssetMapHasNoPackingList => Severity::Critical,
116            _ => Severity::Error,
117        }
118    }
119    fn category(&self) -> Category {
120        match self {
121            Self::AssetMap
122            | Self::AssetMapMalformedXml
123            | Self::PklMalformedXml
124            | Self::NoCpls
125            | Self::EssenceDescriptorList
126            | Self::PklUnknownNamespace
127            | Self::AssetMapHasNoPackingList => Category::Structure,
128            Self::SizeMismatch | Self::FileNotFound | Self::IoError | Self::ChecksumMismatch => {
129                Category::Asset
130            }
131            Self::UnresolvedUuid | Self::DuplicateUuid | Self::PklIdNotInAssetMap => {
132                Category::Reference
133            }
134        }
135    }
136    fn example(&self) -> Option<&'static str> {
137        Some(match self {
138            Self::AssetMap => "ASSETMAP.xml exists but parse_assetmap returns Err (e.g. wrong root element)",
139            Self::AssetMapMalformedXml => "ASSETMAP.xml truncated mid-element: `<AssetMap><Id>urn:uuid:…</Id>`",
140            Self::PklMalformedXml => "PKL.xml with mismatched tag: `<PackingList>…</packinglist>`",
141            Self::NoCpls => "ASSETMAP.xml with all assets flagged `<PackingList>true</PackingList>` and zero CPLs",
142            Self::SizeMismatch => "PKL `<Size>1024</Size>` but the actual file is 2048 bytes on disk",
143            Self::FileNotFound => "AssetMap `<Path>missing-track.mxf</Path>` but no such file exists in the package",
144            Self::ChecksumMismatch => "PKL `<Hash>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</Hash>` (SHA-1 of empty string) on a non-empty file",
145            Self::UnresolvedUuid => "CPL TrackFileId references urn:uuid:aaa… but no PKL/AssetMap asset has that Id",
146            Self::DuplicateUuid => "Two AssetMap `<Asset><Id>urn:uuid:00…01</Id></Asset>` entries with the same Id",
147            Self::IoError => "Underlying filesystem refused the read (permissions, device error, etc.)",
148            Self::EssenceDescriptorList => "CPL with `<SourceEncoding>` references but no top-level `<EssenceDescriptorList>` element",
149            Self::PklUnknownNamespace => "PKL with `xmlns=\"http://example.org/not-a-pkl-namespace\"`",
150            Self::AssetMapHasNoPackingList => "AssetMap whose `<Asset>` entries all omit `<PackingList>true</PackingList>`",
151            Self::PklIdNotInAssetMap => "PKL `<Id>urn:uuid:abc…</Id>` not present in the AssetMap as a packing-list asset",
152        })
153    }
154}
155
156impl St2067_2_2020 {
157    pub const ALL: &'static [Self] = &[
158        Self::AssetMap,
159        Self::AssetMapMalformedXml,
160        Self::PklMalformedXml,
161        Self::NoCpls,
162        Self::SizeMismatch,
163        Self::FileNotFound,
164        Self::ChecksumMismatch,
165        Self::UnresolvedUuid,
166        Self::DuplicateUuid,
167        Self::IoError,
168        Self::EssenceDescriptorList,
169        Self::PklUnknownNamespace,
170        Self::AssetMapHasNoPackingList,
171        Self::PklIdNotInAssetMap,
172    ];
173}
174
175impl From<St2067_2_2020> for String {
176    fn from(c: St2067_2_2020) -> String {
177        c.code().to_string()
178    }
179}
180
181// ─────────────────────────────────────────────────────────────────────────────
182// Core Constraints CPL codes  (emitted by CoreConstraints validators in st2067-21)
183// ─────────────────────────────────────────────────────────────────────────────
184
185/// Spec-agnostic reason codes for Core Constraints CPL validation.
186///
187/// Passed to each edition's `for_code` dispatch function to get the full
188/// `&'static str` code without any runtime string building.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum CoreConstraintsCode {
191    ResourceListEmpty,
192    ContentTitle,
193    TotalRunningTimeFormat,
194    SegmentList,
195    Segment,
196    EditRate,
197    IssueDate,
198    IssueDateFormat,
199    CompositionTimecodeDropFrame,
200    CompositionTimecodeRate,
201    CompositionTimecodeStartAddress,
202    CompositionTimecodeRateZero,
203    CompositionTimecodeStartAddressFormat,
204    CompositionTimecodeRateMismatch,
205    LocaleListNonEmpty,
206    UniqueSegmentId,
207    UniqueEssenceDescriptorId,
208    UniqueResourceId,
209    IntrinsicDuration,
210    EntryPoint,
211    SourceDuration,
212    ResourceDuration,
213    RepeatCount,
214    TrackFileId,
215    VirtualTrackContinuity,
216    VirtualTrackEditRate,
217    TimedTextSampleRate,
218    TimedTextEmptyLanguageTag,
219    TimedTextMalformedLanguageTag,
220    AudioSampleRate,
221    ChannelCount,
222    MCASubDescriptors,
223    SoundfieldGroup,
224    MCATagSymbol,
225    SoundfieldChannelCount,
226    DigitalSignature,
227    DanglingEssenceDescriptor,
228    EssenceDescriptorList,
229}
230
231macro_rules! define_core_constraints_enum {
232    // No identical predecessor — trait default `None`.
233    ($name:ident, $prefix:literal) => {
234        define_core_constraints_enum!(@inner $name, $prefix, None);
235    };
236    // Identical to a prior edition (verified via snapshot diff,
237    // docs/catalogue-todos.md Item 2).
238    ($name:ident, $prefix:literal, $previous:literal) => {
239        define_core_constraints_enum!(@inner $name, $prefix, Some($previous));
240    };
241    (@inner $name:ident, $prefix:literal, $previous:expr) => {
242        /// Validation codes for Core Constraints CPL checks, edition
243        #[doc = $prefix]
244        #[allow(non_camel_case_types)]
245        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
246        pub enum $name {
247            /// A Sequence has an empty ResourceList.
248            ResourceListEmpty,
249            /// ContentTitle shall not be empty.
250            ContentTitle,
251            /// TotalRunningTime does not match format HH:MM:SS.
252            TotalRunningTimeFormat,
253            /// SegmentList shall contain at least one Segment.
254            SegmentList,
255            /// A Segment contains no sequences.
256            Segment,
257            /// CPL EditRate is required (XSD schema §88).
258            EditRate,
259            /// IssueDate shall not be empty.
260            IssueDate,
261            /// IssueDate is not a valid xs:dateTime format.
262            IssueDateFormat,
263            /// CompositionTimecode.TimecodeDropFrame is required when CompositionTimecode is present.
264            CompositionTimecodeDropFrame,
265            /// CompositionTimecode.TimecodeRate is required when CompositionTimecode is present.
266            CompositionTimecodeRate,
267            /// CompositionTimecode.TimecodeStartAddress is required when CompositionTimecode is present.
268            CompositionTimecodeStartAddress,
269            /// CompositionTimecode.TimecodeRate shall be a positive integer.
270            CompositionTimecodeRateZero,
271            /// TimecodeStartAddress does not match SMPTE timecode format HH:MM:SS:FF.
272            CompositionTimecodeStartAddressFormat,
273            /// CompositionTimecode.TimecodeRate does not match the CPL EditRate.
274            CompositionTimecodeRateMismatch,
275            /// LocaleList shall contain at least one Locale.
276            LocaleListNonEmpty,
277            /// Duplicate Segment Id within the CPL.
278            UniqueSegmentId,
279            /// Duplicate EssenceDescriptor Id within the CPL.
280            UniqueEssenceDescriptorId,
281            /// Duplicate Resource Id within the CPL.
282            UniqueResourceId,
283            /// IntrinsicDuration shall be greater than 0.
284            IntrinsicDuration,
285            /// EntryPoint shall be less than IntrinsicDuration.
286            EntryPoint,
287            /// EntryPoint + SourceDuration exceeds IntrinsicDuration.
288            SourceDuration,
289            /// SourceDuration shall be a positive integer.
290            ResourceDuration,
291            /// RepeatCount shall be a positive integer.
292            RepeatCount,
293            /// A non-marker resource is missing a TrackFileId.
294            TrackFileId,
295            /// A virtual track is missing from one or more segments.
296            VirtualTrackContinuity,
297            /// All resources in a virtual track shall have the same edit rate.
298            VirtualTrackEditRate,
299            /// DCTimedTextDescriptor SampleRate is missing.
300            TimedTextSampleRate,
301            /// Empty language tag in RFC5646LanguageTagList.
302            TimedTextEmptyLanguageTag,
303            /// Language tag does not start with an ASCII letter (RFC 5646 primary subtag).
304            TimedTextMalformedLanguageTag,
305            /// WAVEPCMDescriptor has no AudioSampleRate or SampleRate.
306            AudioSampleRate,
307            /// WAVEPCMDescriptor ChannelCount is zero or missing.
308            ChannelCount,
309            /// WAVEPCMDescriptor has no MCA SubDescriptors.
310            MCASubDescriptors,
311            /// WAVEPCMDescriptor SubDescriptors missing SoundfieldGroupLabelSubDescriptor.
312            SoundfieldGroup,
313            /// SoundfieldGroupLabelSubDescriptor is missing MCATagSymbol.
314            MCATagSymbol,
315            /// Soundfield group channel count is inconsistent with WAVEPCMDescriptor.ChannelCount.
316            SoundfieldChannelCount,
317            /// Digital signature validation (ST 2067-2 §8) is not currently performed.
318            DigitalSignature,
319            /// EssenceDescriptor present in EssenceDescriptorList but not referenced by any Resource.
320            DanglingEssenceDescriptor,
321            /// EssenceDescriptorList is required per ST 2067-2 §6.4.2.
322            EssenceDescriptorList,
323        }
324
325        impl ValidationCode for $name {
326            fn code(&self) -> &'static str {
327                match self {
328                    Self::ResourceListEmpty                    => concat!($prefix, ":XSD/ResourceList-Empty"),
329                    Self::ContentTitle                         => concat!($prefix, ":XSD/ContentTitle"),
330                    Self::TotalRunningTimeFormat               => concat!($prefix, ":XSD/TotalRunningTime-Format"),
331                    Self::SegmentList                          => concat!($prefix, ":XSD/SegmentList"),
332                    Self::Segment                              => concat!($prefix, ":XSD/Segment"),
333                    Self::EditRate                             => concat!($prefix, ":XSD/CompositionPlaylist/EditRate"),
334                    Self::IssueDate                            => concat!($prefix, ":XSD/CompositionPlaylist/IssueDate"),
335                    Self::IssueDateFormat                      => concat!($prefix, ":XSD/CompositionPlaylist/IssueDate-Format"),
336                    Self::CompositionTimecodeDropFrame         => concat!($prefix, ":XSD/CompositionTimecode/DropFrame"),
337                    Self::CompositionTimecodeRate              => concat!($prefix, ":XSD/CompositionTimecode/Rate"),
338                    Self::CompositionTimecodeStartAddress      => concat!($prefix, ":XSD/CompositionTimecode/StartAddress"),
339                    Self::CompositionTimecodeRateZero          => concat!($prefix, ":XSD/CompositionTimecode/Rate-Zero"),
340                    Self::CompositionTimecodeStartAddressFormat => concat!($prefix, ":XSD/CompositionTimecode/StartAddress-Format"),
341                    Self::CompositionTimecodeRateMismatch      => concat!($prefix, ":XSD/CompositionTimecode/Rate-Mismatch"),
342                    Self::LocaleListNonEmpty                   => concat!($prefix, ":XSD/LocaleList-NonEmpty"),
343                    Self::UniqueSegmentId                      => concat!($prefix, ":6.1/UniqueSegmentId"),
344                    Self::UniqueEssenceDescriptorId            => concat!($prefix, ":6.1/UniqueEssenceDescriptorId"),
345                    Self::UniqueResourceId                     => concat!($prefix, ":6.1/UniqueResourceId"),
346                    Self::IntrinsicDuration                    => concat!($prefix, ":6.10/IntrinsicDuration"),
347                    Self::EntryPoint                           => concat!($prefix, ":6.10/EntryPoint"),
348                    Self::SourceDuration                       => concat!($prefix, ":6.10/SourceDuration"),
349                    Self::ResourceDuration                     => concat!($prefix, ":6.10/ResourceDuration"),
350                    Self::RepeatCount                          => concat!($prefix, ":6.10/RepeatCount"),
351                    Self::TrackFileId                          => concat!($prefix, ":6.10/TrackFileId"),
352                    Self::VirtualTrackContinuity               => concat!($prefix, ":6.9/VirtualTrackContinuity"),
353                    Self::VirtualTrackEditRate                 => concat!($prefix, ":6.9.3/VirtualTrackEditRate"),
354                    Self::TimedTextSampleRate                  => concat!($prefix, ":10/TimedText-SampleRate"),
355                    Self::TimedTextEmptyLanguageTag            => concat!($prefix, ":10/TimedText-EmptyLanguageTag"),
356                    Self::TimedTextMalformedLanguageTag        => concat!($prefix, ":10/TimedText-MalformedLanguageTag"),
357                    Self::AudioSampleRate                      => concat!($prefix, ":ST377-4/AudioSampleRate"),
358                    Self::ChannelCount                         => concat!($prefix, ":ST377-4/ChannelCount"),
359                    Self::MCASubDescriptors                    => concat!($prefix, ":ST377-4/MCASubDescriptors"),
360                    Self::SoundfieldGroup                      => concat!($prefix, ":ST377-4/SoundfieldGroup"),
361                    Self::MCATagSymbol                         => concat!($prefix, ":ST377-4/MCATagSymbol"),
362                    Self::SoundfieldChannelCount               => concat!($prefix, ":ST377-4/SoundfieldChannelCount"),
363                    Self::DigitalSignature                     => concat!($prefix, ":8/DigitalSignature"),
364                    Self::DanglingEssenceDescriptor            => concat!($prefix, ":6.4.2/DanglingEssenceDescriptor"),
365                    Self::EssenceDescriptorList                => concat!($prefix, ":6.4.2/EssenceDescriptorList"),
366                }
367            }
368            fn description(&self) -> &'static str {
369                match self {
370                    Self::ResourceListEmpty                    => "A Sequence has an empty ResourceList.",
371                    Self::ContentTitle                         => "ContentTitle shall not be empty.",
372                    Self::TotalRunningTimeFormat               => "TotalRunningTime does not match required format HH:MM:SS.",
373                    Self::SegmentList                          => "SegmentList shall contain at least one Segment.",
374                    Self::Segment                              => "A Segment contains no sequences.",
375                    Self::EditRate                             => "CPL EditRate is required (XSD schema §88).",
376                    Self::IssueDate                            => "IssueDate shall not be empty.",
377                    Self::IssueDateFormat                      => "IssueDate is not a valid xs:dateTime format.",
378                    Self::CompositionTimecodeDropFrame         => "CompositionTimecode.TimecodeDropFrame is required when CompositionTimecode is present.",
379                    Self::CompositionTimecodeRate              => "CompositionTimecode.TimecodeRate is required when CompositionTimecode is present.",
380                    Self::CompositionTimecodeStartAddress      => "CompositionTimecode.TimecodeStartAddress is required when CompositionTimecode is present.",
381                    Self::CompositionTimecodeRateZero          => "CompositionTimecode.TimecodeRate shall be a positive integer.",
382                    Self::CompositionTimecodeStartAddressFormat => "TimecodeStartAddress does not match SMPTE timecode format HH:MM:SS:FF.",
383                    Self::CompositionTimecodeRateMismatch      => "CompositionTimecode.TimecodeRate does not match the CPL EditRate.",
384                    Self::LocaleListNonEmpty                   => "LocaleList shall contain at least one Locale.",
385                    Self::UniqueSegmentId                      => "Duplicate Segment Id within the CPL.",
386                    Self::UniqueEssenceDescriptorId            => "Duplicate EssenceDescriptor Id within the CPL.",
387                    Self::UniqueResourceId                     => "Duplicate Resource Id within the CPL.",
388                    Self::IntrinsicDuration                    => "IntrinsicDuration shall be greater than 0.",
389                    Self::EntryPoint                           => "EntryPoint shall be less than IntrinsicDuration.",
390                    Self::SourceDuration                       => "EntryPoint + SourceDuration exceeds IntrinsicDuration.",
391                    Self::ResourceDuration                     => "SourceDuration shall be a positive integer.",
392                    Self::RepeatCount                          => "RepeatCount shall be a positive integer.",
393                    Self::TrackFileId                          => "A non-marker resource is missing a TrackFileId.",
394                    Self::VirtualTrackContinuity               => "A virtual track is missing from one or more segments.",
395                    Self::VirtualTrackEditRate                 => "All resources in a virtual track shall have the same edit rate.",
396                    Self::TimedTextSampleRate                  => "DCTimedTextDescriptor SampleRate is missing.",
397                    Self::TimedTextEmptyLanguageTag            => "Empty language tag in RFC5646LanguageTagList.",
398                    Self::TimedTextMalformedLanguageTag        => "Language tag does not start with an ASCII letter (RFC 5646 primary subtag).",
399                    Self::AudioSampleRate                      => "WAVEPCMDescriptor has no AudioSampleRate or SampleRate.",
400                    Self::ChannelCount                         => "WAVEPCMDescriptor ChannelCount is zero or missing.",
401                    Self::MCASubDescriptors                    => "WAVEPCMDescriptor has no MCA SubDescriptors.",
402                    Self::SoundfieldGroup                      => "WAVEPCMDescriptor SubDescriptors missing SoundfieldGroupLabelSubDescriptor.",
403                    Self::MCATagSymbol                         => "SoundfieldGroupLabelSubDescriptor is missing MCATagSymbol.",
404                    Self::SoundfieldChannelCount               => "Soundfield group channel count is inconsistent with WAVEPCMDescriptor.ChannelCount.",
405                    Self::DigitalSignature                     => "Digital signature validation (ST 2067-2 §8) is not currently performed.",
406                    Self::DanglingEssenceDescriptor            => "EssenceDescriptor present in EssenceDescriptorList but not referenced by any Resource.",
407                    Self::EssenceDescriptorList                => "EssenceDescriptorList is required per ST 2067-2 §6.4.2.",
408                }
409            }
410            fn default_severity(&self) -> Severity {
411                match self {
412                    Self::SegmentList => Severity::Critical,
413                    Self::IssueDateFormat
414                    | Self::CompositionTimecodeRateMismatch
415                    | Self::TimedTextSampleRate
416                    | Self::TimedTextEmptyLanguageTag
417                    | Self::TimedTextMalformedLanguageTag
418                    | Self::AudioSampleRate
419                    | Self::ChannelCount
420                    | Self::MCASubDescriptors
421                    | Self::SoundfieldGroup
422                    | Self::MCATagSymbol => Severity::Warning,
423                    Self::DigitalSignature => Severity::Info,
424                    _ => Severity::Error,
425                }
426            }
427            fn category(&self) -> Category {
428                match self {
429                    Self::ContentTitle
430                    | Self::IssueDate
431                    | Self::IssueDateFormat
432                    | Self::CompositionTimecodeRateMismatch => Category::Metadata,
433
434                    Self::CompositionTimecodeDropFrame
435                    | Self::CompositionTimecodeRate
436                    | Self::CompositionTimecodeStartAddress
437                    | Self::CompositionTimecodeRateZero
438                    | Self::CompositionTimecodeStartAddressFormat
439                    | Self::IntrinsicDuration
440                    | Self::EntryPoint
441                    | Self::SourceDuration
442                    | Self::ResourceDuration
443                    | Self::RepeatCount
444                    | Self::VirtualTrackEditRate => Category::Timing,
445
446                    Self::TimedTextSampleRate
447                    | Self::TimedTextEmptyLanguageTag
448                    | Self::TimedTextMalformedLanguageTag => Category::Subtitle,
449
450                    Self::AudioSampleRate
451                    | Self::ChannelCount
452                    | Self::MCASubDescriptors
453                    | Self::SoundfieldGroup
454                    | Self::MCATagSymbol
455                    | Self::SoundfieldChannelCount => Category::Audio,
456
457                    Self::DanglingEssenceDescriptor | Self::TrackFileId => Category::Reference,
458
459                    Self::DigitalSignature => Category::Security,
460
461                    _ => Category::Structure,
462                }
463            }
464            fn previous_identical_edition(&self) -> Option<&'static str> {
465                $previous
466            }
467            fn example(&self) -> Option<&'static str> {
468                Some(match self {
469                    Self::ResourceListEmpty                    => "<Sequence><ResourceList></ResourceList></Sequence>",
470                    Self::ContentTitle                         => "<ContentTitle></ContentTitle>",
471                    Self::TotalRunningTimeFormat               => "<TotalRunningTime>1h23m</TotalRunningTime>",
472                    Self::SegmentList                          => "<SegmentList></SegmentList>",
473                    Self::Segment                              => "<Segment><SequenceList></SequenceList></Segment>",
474                    Self::EditRate                             => "<CompositionPlaylist>…</CompositionPlaylist>  <!-- no <EditRate> element -->",
475                    Self::IssueDate                            => "<IssueDate></IssueDate>",
476                    Self::IssueDateFormat                      => "<IssueDate>2024/01/01</IssueDate>  <!-- not xs:dateTime -->",
477                    Self::CompositionTimecodeDropFrame         => "<CompositionTimecode><TimecodeRate>24</TimecodeRate>…</CompositionTimecode>  <!-- no <TimecodeDropFrame> -->",
478                    Self::CompositionTimecodeRate              => "<CompositionTimecode><TimecodeDropFrame>false</TimecodeDropFrame>…</CompositionTimecode>  <!-- no <TimecodeRate> -->",
479                    Self::CompositionTimecodeStartAddress      => "<CompositionTimecode><TimecodeRate>24</TimecodeRate><TimecodeDropFrame>false</TimecodeDropFrame></CompositionTimecode>  <!-- no <TimecodeStartAddress> -->",
480                    Self::CompositionTimecodeRateZero          => "<TimecodeRate>0</TimecodeRate>",
481                    Self::CompositionTimecodeStartAddressFormat => "<TimecodeStartAddress>1:2:3</TimecodeStartAddress>  <!-- expected HH:MM:SS:FF -->",
482                    Self::CompositionTimecodeRateMismatch      => "CPL <EditRate>24 1</EditRate> but <TimecodeRate>25</TimecodeRate>",
483                    Self::LocaleListNonEmpty                   => "<LocaleList></LocaleList>",
484                    Self::UniqueSegmentId                      => "Two <Segment><Id>urn:uuid:abc…</Id></Segment> with the same Id",
485                    Self::UniqueEssenceDescriptorId            => "Two <EssenceDescriptor><Id>urn:uuid:abc…</Id></EssenceDescriptor> with the same Id",
486                    Self::UniqueResourceId                     => "Two <Resource><Id>urn:uuid:abc…</Id></Resource> with the same Id",
487                    Self::IntrinsicDuration                    => "<IntrinsicDuration>0</IntrinsicDuration>",
488                    Self::EntryPoint                           => "<IntrinsicDuration>100</IntrinsicDuration><EntryPoint>150</EntryPoint>",
489                    Self::SourceDuration                       => "<IntrinsicDuration>100</IntrinsicDuration><EntryPoint>50</EntryPoint><SourceDuration>80</SourceDuration>  <!-- 50+80 > 100 -->",
490                    Self::ResourceDuration                     => "<SourceDuration>-5</SourceDuration>",
491                    Self::RepeatCount                          => "<RepeatCount>0</RepeatCount>",
492                    Self::TrackFileId                          => "<Resource xsi:type=\"TrackFileResourceType\">…</Resource>  <!-- no <TrackFileId> child -->",
493                    Self::VirtualTrackContinuity               => "Two <Segment> entries, MainAudio track present in segment 0 but absent from segment 1",
494                    Self::VirtualTrackEditRate                 => "Track resources mix <EditRate>24 1</EditRate> and <EditRate>25 1</EditRate>",
495                    Self::TimedTextSampleRate                  => "<DCTimedTextDescriptor>…</DCTimedTextDescriptor>  <!-- no SampleRate -->",
496                    Self::TimedTextEmptyLanguageTag            => "<RFC5646LanguageTagList><RFC5646LanguageTag></RFC5646LanguageTag></RFC5646LanguageTagList>",
497                    Self::TimedTextMalformedLanguageTag        => "<RFC5646LanguageTag>123-en</RFC5646LanguageTag>  <!-- primary subtag must start with a letter -->",
498                    Self::AudioSampleRate                      => "<WAVEPCMDescriptor>…</WAVEPCMDescriptor>  <!-- no AudioSampleRate -->",
499                    Self::ChannelCount                         => "<WAVEPCMDescriptor><ChannelCount>0</ChannelCount></WAVEPCMDescriptor>",
500                    Self::MCASubDescriptors                    => "<WAVEPCMDescriptor>…</WAVEPCMDescriptor>  <!-- no SubDescriptors strong-ref list -->",
501                    Self::SoundfieldGroup                      => "<WAVEPCMDescriptor><SubDescriptors>…</SubDescriptors></WAVEPCMDescriptor>  <!-- but no SoundfieldGroupLabelSubDescriptor inside -->",
502                    Self::MCATagSymbol                         => "<SoundfieldGroupLabelSubDescriptor>…</SoundfieldGroupLabelSubDescriptor>  <!-- no <MCATagSymbol> -->",
503                    Self::SoundfieldChannelCount               => "<WAVEPCMDescriptor><ChannelCount>2</ChannelCount>…</WAVEPCMDescriptor> but soundfield group declares 6 channels",
504                    Self::DigitalSignature                     => "CPL declares Signer/Signature but the engine does not yet verify them (info notice)",
505                    Self::DanglingEssenceDescriptor            => "<EssenceDescriptor><Id>urn:uuid:abc…</Id></EssenceDescriptor>  <!-- no Resource references this Id -->",
506                    Self::EssenceDescriptorList                => "CPL with `<SourceEncoding>` references but no top-level `<EssenceDescriptorList>` element",
507                })
508            }
509        }
510
511        impl $name {
512            pub const ALL: &'static [Self] = &[
513                Self::ResourceListEmpty,
514                Self::ContentTitle,
515                Self::TotalRunningTimeFormat,
516                Self::SegmentList,
517                Self::Segment,
518                Self::EditRate,
519                Self::IssueDate,
520                Self::IssueDateFormat,
521                Self::CompositionTimecodeDropFrame,
522                Self::CompositionTimecodeRate,
523                Self::CompositionTimecodeStartAddress,
524                Self::CompositionTimecodeRateZero,
525                Self::CompositionTimecodeStartAddressFormat,
526                Self::CompositionTimecodeRateMismatch,
527                Self::LocaleListNonEmpty,
528                Self::UniqueSegmentId,
529                Self::UniqueEssenceDescriptorId,
530                Self::UniqueResourceId,
531                Self::IntrinsicDuration,
532                Self::EntryPoint,
533                Self::SourceDuration,
534                Self::ResourceDuration,
535                Self::RepeatCount,
536                Self::TrackFileId,
537                Self::VirtualTrackContinuity,
538                Self::VirtualTrackEditRate,
539                Self::TimedTextSampleRate,
540                Self::TimedTextEmptyLanguageTag,
541                Self::TimedTextMalformedLanguageTag,
542                Self::AudioSampleRate,
543                Self::ChannelCount,
544                Self::MCASubDescriptors,
545                Self::SoundfieldGroup,
546                Self::MCATagSymbol,
547                Self::SoundfieldChannelCount,
548                Self::DigitalSignature,
549                Self::DanglingEssenceDescriptor,
550                Self::EssenceDescriptorList,
551            ];
552
553            /// Dispatch from the spec-agnostic [`CoreConstraintsCode`] to this
554            /// edition's static code string.  Used by the shared validator helpers.
555            pub fn for_code(r: CoreConstraintsCode) -> &'static str {
556                match r {
557                    CoreConstraintsCode::ResourceListEmpty                    => concat!($prefix, ":XSD/ResourceList-Empty"),
558                    CoreConstraintsCode::ContentTitle                         => concat!($prefix, ":XSD/ContentTitle"),
559                    CoreConstraintsCode::TotalRunningTimeFormat               => concat!($prefix, ":XSD/TotalRunningTime-Format"),
560                    CoreConstraintsCode::SegmentList                          => concat!($prefix, ":XSD/SegmentList"),
561                    CoreConstraintsCode::Segment                              => concat!($prefix, ":XSD/Segment"),
562                    CoreConstraintsCode::EditRate                             => concat!($prefix, ":XSD/CompositionPlaylist/EditRate"),
563                    CoreConstraintsCode::IssueDate                            => concat!($prefix, ":XSD/CompositionPlaylist/IssueDate"),
564                    CoreConstraintsCode::IssueDateFormat                      => concat!($prefix, ":XSD/CompositionPlaylist/IssueDate-Format"),
565                    CoreConstraintsCode::CompositionTimecodeDropFrame         => concat!($prefix, ":XSD/CompositionTimecode/DropFrame"),
566                    CoreConstraintsCode::CompositionTimecodeRate              => concat!($prefix, ":XSD/CompositionTimecode/Rate"),
567                    CoreConstraintsCode::CompositionTimecodeStartAddress      => concat!($prefix, ":XSD/CompositionTimecode/StartAddress"),
568                    CoreConstraintsCode::CompositionTimecodeRateZero          => concat!($prefix, ":XSD/CompositionTimecode/Rate-Zero"),
569                    CoreConstraintsCode::CompositionTimecodeStartAddressFormat => concat!($prefix, ":XSD/CompositionTimecode/StartAddress-Format"),
570                    CoreConstraintsCode::CompositionTimecodeRateMismatch      => concat!($prefix, ":XSD/CompositionTimecode/Rate-Mismatch"),
571                    CoreConstraintsCode::LocaleListNonEmpty                   => concat!($prefix, ":XSD/LocaleList-NonEmpty"),
572                    CoreConstraintsCode::UniqueSegmentId                      => concat!($prefix, ":6.1/UniqueSegmentId"),
573                    CoreConstraintsCode::UniqueEssenceDescriptorId            => concat!($prefix, ":6.1/UniqueEssenceDescriptorId"),
574                    CoreConstraintsCode::UniqueResourceId                     => concat!($prefix, ":6.1/UniqueResourceId"),
575                    CoreConstraintsCode::IntrinsicDuration                    => concat!($prefix, ":6.10/IntrinsicDuration"),
576                    CoreConstraintsCode::EntryPoint                           => concat!($prefix, ":6.10/EntryPoint"),
577                    CoreConstraintsCode::SourceDuration                       => concat!($prefix, ":6.10/SourceDuration"),
578                    CoreConstraintsCode::ResourceDuration                     => concat!($prefix, ":6.10/ResourceDuration"),
579                    CoreConstraintsCode::RepeatCount                          => concat!($prefix, ":6.10/RepeatCount"),
580                    CoreConstraintsCode::TrackFileId                          => concat!($prefix, ":6.10/TrackFileId"),
581                    CoreConstraintsCode::VirtualTrackContinuity               => concat!($prefix, ":6.9/VirtualTrackContinuity"),
582                    CoreConstraintsCode::VirtualTrackEditRate                 => concat!($prefix, ":6.9.3/VirtualTrackEditRate"),
583                    CoreConstraintsCode::TimedTextSampleRate                  => concat!($prefix, ":10/TimedText-SampleRate"),
584                    CoreConstraintsCode::TimedTextEmptyLanguageTag            => concat!($prefix, ":10/TimedText-EmptyLanguageTag"),
585                    CoreConstraintsCode::TimedTextMalformedLanguageTag        => concat!($prefix, ":10/TimedText-MalformedLanguageTag"),
586                    CoreConstraintsCode::AudioSampleRate                      => concat!($prefix, ":ST377-4/AudioSampleRate"),
587                    CoreConstraintsCode::ChannelCount                         => concat!($prefix, ":ST377-4/ChannelCount"),
588                    CoreConstraintsCode::MCASubDescriptors                    => concat!($prefix, ":ST377-4/MCASubDescriptors"),
589                    CoreConstraintsCode::SoundfieldGroup                      => concat!($prefix, ":ST377-4/SoundfieldGroup"),
590                    CoreConstraintsCode::MCATagSymbol                         => concat!($prefix, ":ST377-4/MCATagSymbol"),
591                    CoreConstraintsCode::SoundfieldChannelCount               => concat!($prefix, ":ST377-4/SoundfieldChannelCount"),
592                    CoreConstraintsCode::DigitalSignature                     => concat!($prefix, ":8/DigitalSignature"),
593                    CoreConstraintsCode::DanglingEssenceDescriptor            => concat!($prefix, ":6.4.2/DanglingEssenceDescriptor"),
594                    CoreConstraintsCode::EssenceDescriptorList                => concat!($prefix, ":6.4.2/EssenceDescriptorList"),
595                }
596            }
597        }
598
599        impl From<$name> for String {
600            fn from(c: $name) -> String {
601                c.code().to_string()
602            }
603        }
604    };
605}
606
607define_core_constraints_enum!(St2067_2_2013_Core, "ST2067-2:2013");
608// 2016 catalogue is bit-for-bit identical to 2013 (audit, 2026-06-04).
609define_core_constraints_enum!(St2067_2_2016_Core, "ST2067-2:2016", "ST2067-2:2013");
610define_core_constraints_enum!(St2067_2_2020_Core, "ST2067-2:2020");