postproject_core/
identifier.rs1use crate::{Error, ErrorKind, Result};
4
5pub const MAX_IDENTIFIER_SCHEME_BYTES: usize = 255;
7
8pub const MAX_IDENTIFIER_VALUE_BYTES: usize = 4096;
10
11pub const MAX_IDENTIFIER_QUALIFIER_BYTES: usize = 1024;
13
14#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
20pub struct IdentifierScheme(String);
21
22impl IdentifierScheme {
23 pub fn new(value: impl Into<String>) -> Result<Self> {
30 let value = value.into();
31 if value.is_empty() || value.len() > MAX_IDENTIFIER_SCHEME_BYTES {
32 return Err(Error::new(
33 ErrorKind::InvalidArgument,
34 format!(
35 "identifier scheme must contain 1-{MAX_IDENTIFIER_SCHEME_BYTES} UTF-8 bytes"
36 ),
37 ));
38 }
39 if value.chars().any(char::is_whitespace) || value.chars().any(char::is_control) {
40 return Err(Error::new(
41 ErrorKind::InvalidArgument,
42 "identifier scheme must not contain whitespace or control characters",
43 ));
44 }
45 Ok(Self(value))
46 }
47
48 #[must_use]
50 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53
54 #[must_use]
56 pub fn into_string(self) -> String {
57 self.0
58 }
59}
60
61#[derive(Clone, Debug, Eq, Hash, PartialEq)]
66pub struct ExternalIdentifier {
67 scheme: IdentifierScheme,
68 value: String,
69 qualifier: Option<String>,
70}
71
72impl ExternalIdentifier {
73 pub fn new(
80 scheme: IdentifierScheme,
81 value: impl Into<String>,
82 qualifier: Option<String>,
83 ) -> Result<Self> {
84 let value = value.into();
85 validate_bounded_text("identifier value", &value, MAX_IDENTIFIER_VALUE_BYTES)?;
86 if let Some(qualifier) = qualifier.as_deref() {
87 validate_bounded_text(
88 "identifier qualifier",
89 qualifier,
90 MAX_IDENTIFIER_QUALIFIER_BYTES,
91 )?;
92 }
93 Ok(Self {
94 scheme,
95 value,
96 qualifier,
97 })
98 }
99
100 #[must_use]
102 pub const fn scheme(&self) -> &IdentifierScheme {
103 &self.scheme
104 }
105
106 #[must_use]
108 pub fn value(&self) -> &str {
109 &self.value
110 }
111
112 #[must_use]
114 pub fn qualifier(&self) -> Option<&str> {
115 self.qualifier.as_deref()
116 }
117}
118
119fn validate_bounded_text(label: &str, value: &str, maximum: usize) -> Result<()> {
120 if value.is_empty() || value.len() > maximum || value.contains('\0') {
121 return Err(Error::new(
122 ErrorKind::InvalidArgument,
123 format!("{label} must contain 1-{maximum} UTF-8 bytes without NUL"),
124 ));
125 }
126 Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn unknown_scheme_and_value_round_trip_exactly() {
135 let scheme = IdentifierScheme::new("com.example.camera.serial").expect("valid scheme");
136 let identifier = ExternalIdentifier::new(
137 scheme,
138 " Vendor Value 01 ",
139 Some("camera-body".to_owned()),
140 )
141 .expect("valid identifier");
142
143 assert_eq!(identifier.scheme().as_str(), "com.example.camera.serial");
144 assert_eq!(identifier.value(), " Vendor Value 01 ");
145 assert_eq!(identifier.qualifier(), Some("camera-body"));
146 }
147
148 #[test]
149 fn same_scheme_can_describe_multiple_values() {
150 let scheme = IdentifierScheme::new("urn:smpte:umid").expect("valid scheme");
151 let first = ExternalIdentifier::new(scheme.clone(), "first", None).expect("valid id");
152 let second = ExternalIdentifier::new(scheme, "second", None).expect("valid id");
153
154 assert_ne!(first, second);
155 }
156
157 #[test]
158 fn generic_validation_rejects_unbounded_or_ambiguous_scheme_text() {
159 for invalid in ["", "urn:example:has space", "urn:example:\ncontrol"] {
160 let error = IdentifierScheme::new(invalid).expect_err("scheme must fail");
161 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
162 }
163
164 let oversized = "s".repeat(MAX_IDENTIFIER_SCHEME_BYTES + 1);
165 assert_eq!(
166 IdentifierScheme::new(oversized)
167 .expect_err("oversized scheme must fail")
168 .kind(),
169 ErrorKind::InvalidArgument
170 );
171 }
172
173 #[test]
174 fn generic_validation_bounds_values_and_qualifiers() {
175 let scheme = IdentifierScheme::new("com.example.id").expect("valid scheme");
176 assert_eq!(
177 ExternalIdentifier::new(scheme.clone(), "", None)
178 .expect_err("empty value must fail")
179 .kind(),
180 ErrorKind::InvalidArgument
181 );
182 assert_eq!(
183 ExternalIdentifier::new(scheme, "value", Some(String::new()))
184 .expect_err("empty qualifier must fail")
185 .kind(),
186 ErrorKind::InvalidArgument
187 );
188 let scheme = IdentifierScheme::new("com.example.id").expect("valid scheme");
189 assert_eq!(
190 ExternalIdentifier::new(scheme, "value\0suffix", None)
191 .expect_err("NUL-containing value must fail")
192 .kind(),
193 ErrorKind::InvalidArgument
194 );
195 }
196}