1use std::fmt;
2
3use super::{ErrorKind, XmlError, XmlResult};
4
5pub const XML_NAMESPACE_URI: &str = "http://www.w3.org/XML/1998/namespace";
6pub const XMLNS_NAMESPACE_URI: &str = "http://www.w3.org/2000/xmlns/";
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct NamespaceUri(String);
11
12impl NamespaceUri {
13 pub fn new(uri: impl Into<String>) -> XmlResult<Self> {
14 let uri = uri.into();
15 if uri.is_empty() {
16 return Err(XmlError::new(
17 ErrorKind::InvalidNamespace,
18 "namespace URI cannot be empty",
19 ));
20 }
21
22 Ok(Self(uri))
23 }
24
25 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28}
29
30impl fmt::Display for NamespaceUri {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.write_str(&self.0)
33 }
34}
35
36impl TryFrom<&str> for NamespaceUri {
37 type Error = XmlError;
38
39 fn try_from(value: &str) -> Result<Self, Self::Error> {
40 Self::new(value)
41 }
42}
43
44impl TryFrom<String> for NamespaceUri {
45 type Error = XmlError;
46
47 fn try_from(value: String) -> Result<Self, Self::Error> {
48 Self::new(value)
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct NamespacePrefix(String);
55
56impl NamespacePrefix {
57 pub fn new(prefix: impl Into<String>) -> XmlResult<Self> {
58 let prefix = prefix.into();
59 validate_xml_name(&prefix)?;
60 if prefix == "xmlns" {
61 return Err(XmlError::new(
62 ErrorKind::InvalidNamespace,
63 "namespace prefix `xmlns` is reserved",
64 ));
65 }
66
67 Ok(Self(prefix))
68 }
69
70 pub fn as_str(&self) -> &str {
71 &self.0
72 }
73}
74
75impl fmt::Display for NamespacePrefix {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.write_str(&self.0)
78 }
79}
80
81impl TryFrom<&str> for NamespacePrefix {
82 type Error = XmlError;
83
84 fn try_from(value: &str) -> Result<Self, Self::Error> {
85 Self::new(value)
86 }
87}
88
89impl TryFrom<String> for NamespacePrefix {
90 type Error = XmlError;
91
92 fn try_from(value: String) -> Result<Self, Self::Error> {
93 Self::new(value)
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99pub struct QName {
100 prefix: Option<NamespacePrefix>,
101 local: String,
102 namespace_uri: Option<NamespaceUri>,
103}
104
105impl QName {
106 pub fn new(local: impl Into<String>) -> XmlResult<Self> {
108 let local = local.into();
109 validate_xml_name(&local)?;
110
111 Ok(Self {
112 prefix: None,
113 local,
114 namespace_uri: None,
115 })
116 }
117
118 pub fn qualified(
120 prefix: impl Into<String>,
121 local: impl Into<String>,
122 namespace_uri: impl Into<String>,
123 ) -> XmlResult<Self> {
124 let prefix = prefix.into();
125 let local = local.into();
126 let namespace_uri = namespace_uri.into();
127 validate_namespace_binding(Some(&prefix), &namespace_uri)?;
128 let prefix = NamespacePrefix::new(prefix)?;
129 validate_xml_name(&local)?;
130 let namespace_uri = NamespaceUri::new(namespace_uri)?;
131
132 Ok(Self {
133 prefix: Some(prefix),
134 local,
135 namespace_uri: Some(namespace_uri),
136 })
137 }
138
139 pub fn namespaced(
141 local: impl Into<String>,
142 namespace_uri: impl Into<String>,
143 ) -> XmlResult<Self> {
144 let local = local.into();
145 let namespace_uri = namespace_uri.into();
146 validate_namespace_binding(None, &namespace_uri)?;
147 validate_xml_name(&local)?;
148
149 Ok(Self {
150 prefix: None,
151 local,
152 namespace_uri: Some(NamespaceUri::new(namespace_uri)?),
153 })
154 }
155
156 pub fn prefix(&self) -> Option<&NamespacePrefix> {
157 self.prefix.as_ref()
158 }
159
160 pub fn local(&self) -> &str {
161 &self.local
162 }
163
164 pub fn namespace_uri(&self) -> Option<&NamespaceUri> {
165 self.namespace_uri.as_ref()
166 }
167
168 pub fn lexical_name(&self) -> String {
169 match &self.prefix {
170 Some(prefix) => format!("{}:{}", prefix.as_str(), self.local),
171 None => self.local.clone(),
172 }
173 }
174}
175
176impl fmt::Display for QName {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.write_str(&self.lexical_name())
179 }
180}
181
182pub fn validate_xml_name(name: &str) -> XmlResult<()> {
189 if name.is_empty() {
190 return Err(XmlError::invalid_name(name, "name cannot be empty"));
191 }
192
193 let mut chars = name.chars();
194 let first = chars.next().expect("name is not empty");
195 if !is_name_start_char(first) {
196 return Err(XmlError::invalid_name(
197 name,
198 "name must start with an ASCII letter or underscore",
199 ));
200 }
201
202 if let Some(invalid) = chars.find(|ch| !is_name_char(*ch)) {
203 return Err(XmlError::invalid_name(
204 name,
205 format!("character `{invalid}` is not allowed"),
206 ));
207 }
208
209 Ok(())
210}
211
212pub fn validate_namespace_binding(prefix: Option<&str>, uri: &str) -> XmlResult<()> {
213 match prefix {
214 Some("xml") if uri == XML_NAMESPACE_URI => Ok(()),
215 Some("xml") => Err(XmlError::new(
216 ErrorKind::InvalidNamespace,
217 "namespace prefix `xml` must be bound to the XML namespace URI",
218 )),
219 Some("xmlns") => Err(XmlError::new(
220 ErrorKind::InvalidNamespace,
221 "namespace prefix `xmlns` cannot be declared or used as a qualified name prefix",
222 )),
223 Some(_) | None if uri == XML_NAMESPACE_URI => Err(XmlError::new(
224 ErrorKind::InvalidNamespace,
225 "the XML namespace URI can only be bound to prefix `xml`",
226 )),
227 Some(_) | None if uri == XMLNS_NAMESPACE_URI => Err(XmlError::new(
228 ErrorKind::InvalidNamespace,
229 "the XMLNS namespace URI cannot be declared explicitly",
230 )),
231 _ => Ok(()),
232 }
233}
234
235fn is_name_start_char(ch: char) -> bool {
236 ch == '_' || ch.is_ascii_alphabetic()
237}
238
239fn is_name_char(ch: char) -> bool {
240 is_name_start_char(ch) || ch.is_ascii_digit() || matches!(ch, '-' | '.')
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn qname_new_creates_unqualified_name() {
249 let name = QName::new("Invoice").expect("valid name");
250
251 assert_eq!(name.local(), "Invoice");
252 assert_eq!(name.prefix(), None);
253 assert_eq!(name.namespace_uri(), None);
254 assert_eq!(name.lexical_name(), "Invoice");
255 }
256
257 #[test]
258 fn qname_qualified_creates_prefixed_name() {
259 let name = QName::qualified("cbc", "ID", "urn:example:cbc").expect("valid name");
260
261 assert_eq!(name.prefix().map(NamespacePrefix::as_str), Some("cbc"));
262 assert_eq!(name.local(), "ID");
263 assert_eq!(
264 name.namespace_uri().map(NamespaceUri::as_str),
265 Some("urn:example:cbc")
266 );
267 assert_eq!(name.lexical_name(), "cbc:ID");
268 }
269
270 #[test]
271 fn qname_rejects_empty_local_name() {
272 let error = QName::new("").expect_err("empty local name must fail");
273
274 assert_eq!(error.kind(), &ErrorKind::InvalidName);
275 }
276
277 #[test]
278 fn qname_rejects_empty_prefix() {
279 let error = QName::qualified("", "ID", "urn:example").expect_err("empty prefix must fail");
280
281 assert_eq!(error.kind(), &ErrorKind::InvalidName);
282 }
283
284 #[test]
285 fn qname_rejects_colon_inside_structural_name_parts() {
286 let error = QName::new("cbc:ID").expect_err("colon must be structural");
287
288 assert_eq!(error.kind(), &ErrorKind::InvalidName);
289 }
290
291 #[test]
292 fn qname_accepts_reserved_xml_prefix_with_reserved_uri() {
293 let name = QName::qualified("xml", "lang", XML_NAMESPACE_URI).expect("xml name");
294
295 assert_eq!(name.lexical_name(), "xml:lang");
296 assert_eq!(
297 name.namespace_uri().map(NamespaceUri::as_str),
298 Some(XML_NAMESPACE_URI)
299 );
300 }
301
302 #[test]
303 fn qname_rejects_reserved_namespace_misuse() {
304 assert_eq!(
305 QName::qualified("xml", "lang", "urn:wrong")
306 .expect_err("xml prefix must use reserved URI")
307 .kind(),
308 &ErrorKind::InvalidNamespace
309 );
310 assert_eq!(
311 QName::qualified("doc", "ID", XML_NAMESPACE_URI)
312 .expect_err("xml URI must only use xml prefix")
313 .kind(),
314 &ErrorKind::InvalidNamespace
315 );
316 assert_eq!(
317 QName::qualified("xmlns", "ID", XMLNS_NAMESPACE_URI)
318 .expect_err("xmlns prefix is reserved")
319 .kind(),
320 &ErrorKind::InvalidNamespace
321 );
322 assert_eq!(
323 QName::namespaced("Root", XML_NAMESPACE_URI)
324 .expect_err("xml URI cannot be default namespace")
325 .kind(),
326 &ErrorKind::InvalidNamespace
327 );
328 }
329
330 #[test]
331 fn core_validates_basic_xml_names() {
332 assert!(validate_xml_name("_valid-Name.1").is_ok());
333 assert!(validate_xml_name("1invalid").is_err());
334 assert!(validate_xml_name("invalid name").is_err());
335 }
336}