postproject_core/
identifier_registry.rs1use crate::{Error, ErrorKind, ExternalIdentifier, IdentifierScheme, Result};
4
5pub const SMPTE_UMID_SCHEME: &str = "urn:smpte:umid";
7pub const ISAN_SCHEME: &str = "urn:isan";
9pub const EIDR_SCHEME: &str = "urn:eidr";
11pub const POSTPROJECT_APPLICATION_SCHEME: &str = "https://postproject.org/id/application";
13
14#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
16#[non_exhaustive]
17pub enum IdentifierValidationKind {
18 Opaque,
20 Syntax,
22}
23
24#[derive(Clone, Copy, Debug)]
26pub struct IdentifierSchemeDefinition {
27 scheme: &'static str,
28 label: &'static str,
29 reference: &'static str,
30 validation: IdentifierValidationKind,
31 validator: Option<fn(&str) -> bool>,
32}
33
34impl IdentifierSchemeDefinition {
35 #[must_use]
37 pub const fn scheme(self) -> &'static str {
38 self.scheme
39 }
40
41 #[must_use]
43 pub const fn label(self) -> &'static str {
44 self.label
45 }
46
47 #[must_use]
49 pub const fn reference(self) -> &'static str {
50 self.reference
51 }
52
53 #[must_use]
55 pub const fn validation(self) -> IdentifierValidationKind {
56 self.validation
57 }
58
59 pub fn validate_value(self, value: &str) -> Result<()> {
66 if self.validator.is_none_or(|validator| validator(value)) {
67 return Ok(());
68 }
69 Err(Error::new(
70 ErrorKind::InvalidArgument,
71 format!("identifier value does not match {} syntax", self.label),
72 ))
73 }
74}
75
76pub const IDENTIFIER_SCHEMES: &[IdentifierSchemeDefinition] = &[
78 definition(
79 SMPTE_UMID_SCHEME,
80 "SMPTE UMID",
81 "https://pub.smpte.org/doc/st2029/20090310-pub/st2029-2009.pdf",
82 Some(valid_umid),
83 ),
84 definition(
85 ISAN_SCHEME,
86 "ISAN",
87 "https://www.rfc-editor.org/rfc/rfc4246.html",
88 Some(valid_isan),
89 ),
90 definition(
91 EIDR_SCHEME,
92 "EIDR",
93 "https://www.rfc-editor.org/rfc/rfc7972.html",
94 Some(valid_eidr),
95 ),
96 definition(
97 POSTPROJECT_APPLICATION_SCHEME,
98 "PostProject application identifier",
99 POSTPROJECT_APPLICATION_SCHEME,
100 None,
101 ),
102];
103
104#[must_use]
106pub fn identifier_scheme_definition(
107 scheme: &IdentifierScheme,
108) -> Option<&'static IdentifierSchemeDefinition> {
109 IDENTIFIER_SCHEMES
110 .iter()
111 .find(|definition| definition.scheme == scheme.as_str())
112}
113
114pub fn validate_known_identifier(identifier: &ExternalIdentifier) -> Result<bool> {
122 let Some(definition) = identifier_scheme_definition(identifier.scheme()) else {
123 return Ok(false);
124 };
125 definition.validate_value(identifier.value())?;
126 Ok(true)
127}
128
129const fn definition(
130 scheme: &'static str,
131 label: &'static str,
132 reference: &'static str,
133 validator: Option<fn(&str) -> bool>,
134) -> IdentifierSchemeDefinition {
135 IdentifierSchemeDefinition {
136 scheme,
137 label,
138 reference,
139 validation: if validator.is_some() {
140 IdentifierValidationKind::Syntax
141 } else {
142 IdentifierValidationKind::Opaque
143 },
144 validator,
145 }
146}
147
148fn valid_umid(value: &str) -> bool {
149 if !value.contains('.') {
150 return matches!(value.len(), 64 | 128)
151 && value.bytes().all(|byte| byte.is_ascii_hexdigit());
152 }
153 let mut groups = value.split('.');
154 let count = groups.clone().count();
155 matches!(count, 8 | 16)
156 && groups
157 .all(|group| group.len() == 8 && group.bytes().all(|byte| byte.is_ascii_hexdigit()))
158}
159
160fn valid_isan(value: &str) -> bool {
161 let parts: Vec<_> = value.split('-').collect();
162 if !matches!(parts.len(), 5 | 8)
163 || !parts[..4].iter().all(|part| hex_group(part, 4))
164 || !check_character(parts[4])
165 {
166 return false;
167 }
168 parts.len() == 5
169 || (parts[5..7].iter().all(|part| hex_group(part, 4)) && check_character(parts[7]))
170}
171
172fn valid_eidr(value: &str) -> bool {
173 let Some((prefix, suffix)) = value.split_once(':') else {
174 return false;
175 };
176 if prefix.is_empty()
177 || suffix.is_empty()
178 || !prefix.bytes().all(eidr_character)
179 || !suffix.bytes().all(eidr_character)
180 {
181 return false;
182 }
183 if prefix != "10.5240" {
184 return true;
185 }
186 let parts: Vec<_> = suffix.split('-').collect();
187 parts.len() == 6
188 && parts[..5].iter().all(|part| hex_group(part, 4))
189 && check_character(parts[5])
190}
191
192fn hex_group(value: &str, length: usize) -> bool {
193 value.len() == length && value.bytes().all(|byte| byte.is_ascii_hexdigit())
194}
195
196fn check_character(value: &str) -> bool {
197 value.len() == 1 && value.bytes().all(|byte| byte.is_ascii_alphanumeric())
198}
199
200fn eidr_character(byte: u8) -> bool {
201 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_')
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn identifier(scheme: &str, value: &str) -> ExternalIdentifier {
209 ExternalIdentifier::new(
210 IdentifierScheme::new(scheme).expect("valid scheme"),
211 value,
212 None,
213 )
214 .expect("generically valid identifier")
215 }
216
217 #[test]
218 fn published_examples_pass_local_syntax_checks() {
219 let umid = identifier(
220 SMPTE_UMID_SCHEME,
221 "060a2b34.01010105.01010d20.13000000.d2c9036c.8f195343.ab7014d2.d718bfda",
222 );
223 let isan = identifier(ISAN_SCHEME, "1881-66C7-3420-6541-9-9F3A-0245-U");
224 let eidr = identifier(EIDR_SCHEME, "10.5240:7791-8534-2C23-9030-8610-5");
225 assert_eq!(validate_known_identifier(&umid), Ok(true));
226 assert_eq!(validate_known_identifier(&isan), Ok(true));
227 assert_eq!(validate_known_identifier(&eidr), Ok(true));
228 }
229
230 #[test]
231 fn known_invalid_values_fail_without_changing_unknown_schemes() {
232 assert!(validate_known_identifier(&identifier(EIDR_SCHEME, "not-an-eidr")).is_err());
233 assert_eq!(
234 validate_known_identifier(&identifier("com.example.camera", " exact value ")),
235 Ok(false)
236 );
237 }
238
239 #[test]
240 fn application_identifiers_are_known_and_remain_opaque() {
241 let identifier = identifier(POSTPROJECT_APPLICATION_SCHEME, "editor:scene/42");
242 assert_eq!(validate_known_identifier(&identifier), Ok(true));
243 assert_eq!(
244 identifier_scheme_definition(identifier.scheme())
245 .expect("known application scheme")
246 .reference(),
247 POSTPROJECT_APPLICATION_SCHEME
248 );
249 }
250}