xml_string/names/
ncname.rs

1//! [`NCName`].
2//!
3//! [`NCName`]: https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-NCName
4
5use core::convert::TryFrom;
6
7use crate::names::chars;
8use crate::names::error::{NameError, TargetNameType};
9use crate::names::{Eqname, Name, Nmtoken, Qname};
10
11/// String slice for [`NCName`].
12///
13/// [`NCName`]: https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-NCName
14#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[repr(transparent)]
16pub struct Ncname(str);
17
18#[allow(clippy::len_without_is_empty)]
19impl Ncname {
20    /// Creates a new `&Ncname`.
21    ///
22    /// # Failures
23    ///
24    /// Fails if the given string is not a valid [`NCName`].
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// # use xml_string::names::Ncname;
30    /// let name = Ncname::from_str("hello")?;
31    /// assert_eq!(name, "hello");
32    ///
33    /// assert!(Ncname::from_str("").is_err(), "Empty string is not an NCName");
34    /// assert!(Ncname::from_str("foo bar").is_err(), "Whitespace is not allowed");
35    /// assert!(Ncname::from_str("foo:bar").is_err(), "Colon is not allowed");
36    /// assert!(Ncname::from_str("0foo").is_err(), "ASCII digit at the beginning is not allowed");
37    /// # Ok::<_, xml_string::names::NameError>(())
38    /// ```
39    ///
40    /// [`NCName`]: https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-NCName
41    // `FromStr` can be implemented only for types with static lifetime.
42    #[allow(clippy::should_implement_trait)]
43    #[inline]
44    pub fn from_str(s: &str) -> Result<&Self, NameError> {
45        <&Self>::try_from(s)
46    }
47
48    /// Creates a new `&Ncname` without validation.
49    ///
50    /// # Safety
51    ///
52    /// The given string should be a valid [`NCName`].
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// # use xml_string::names::Ncname;
58    /// let name = unsafe {
59    ///     Ncname::new_unchecked("hello")
60    /// };
61    /// assert_eq!(name, "hello");
62    /// ```
63    ///
64    /// [`NCName`]: https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-NCName
65    #[inline]
66    #[must_use]
67    pub unsafe fn new_unchecked(s: &str) -> &Self {
68        &*(s as *const str as *const Self)
69    }
70
71    /// Validates the given string.
72    fn validate(s: &str) -> Result<(), NameError> {
73        let mut chars = s.char_indices();
74
75        // Check the first character.
76        if !chars
77            .next()
78            .map_or(false, |(_, c)| chars::is_ncname_start(c))
79        {
80            return Err(NameError::new(TargetNameType::Ncname, 0));
81        }
82
83        // Check the following characters.
84        if let Some((i, _)) = chars.find(|(_, c)| !chars::is_ncname_continue(*c)) {
85            return Err(NameError::new(TargetNameType::Ncname, i));
86        }
87
88        Ok(())
89    }
90
91    /// Returns the string as `&str`.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// # use xml_string::names::Ncname;
97    /// let name = Ncname::from_str("hello")?;
98    /// assert_eq!(name, "hello");
99    ///
100    /// let s: &str = name.as_str();
101    /// assert_eq!(s, "hello");
102    /// # Ok::<_, xml_string::names::NameError>(())
103    /// ```
104    #[inline]
105    #[must_use]
106    pub fn as_str(&self) -> &str {
107        &self.0
108    }
109
110    /// Returns the length of the string in bytes.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// # use xml_string::names::Ncname;
116    /// let name = Ncname::from_str("foo")?;
117    /// assert_eq!(name.len(), 3);
118    /// # Ok::<_, xml_string::names::NameError>(())
119    /// ```
120    #[inline]
121    #[must_use]
122    pub fn len(&self) -> usize {
123        self.0.len()
124    }
125
126    /// Parses the leading `Ncname` and returns the value and the rest input.
127    ///
128    /// # Exmaples
129    ///
130    /// ```
131    /// # use xml_string::names::Ncname;
132    /// let input = "hello:world";
133    /// let expected = Ncname::from_str("hello").expect("valid NCName");
134    /// assert_eq!(
135    ///     Ncname::parse_next(input),
136    ///     Ok((expected, ":world"))
137    /// );
138    /// # Ok::<_, xml_string::names::NameError>(())
139    /// ```
140    ///
141    /// ```
142    /// # use xml_string::names::Ncname;
143    /// let input = "012";
144    /// assert!(Ncname::parse_next(input).is_err());
145    /// # Ok::<_, xml_string::names::NameError>(())
146    /// ```
147    pub fn parse_next(s: &str) -> Result<(&Self, &str), NameError> {
148        match Self::from_str(s) {
149            Ok(v) => Ok((v, &s[s.len()..])),
150            Err(e) if e.valid_up_to() == 0 => Err(e),
151            Err(e) => {
152                let valid_up_to = e.valid_up_to();
153                let v = unsafe {
154                    let valid = &s[..valid_up_to];
155                    debug_assert!(Self::validate(valid).is_ok());
156                    // This is safe because the substring is valid.
157                    Self::new_unchecked(valid)
158                };
159                Ok((v, &s[valid_up_to..]))
160            }
161        }
162    }
163
164    /// Converts a `Box<Ncname>` into a `Box<str>` without copying or allocating.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// # use xml_string::names::Ncname;
170    /// let name = Ncname::from_str("ncname")?;
171    /// let boxed_name: Box<Ncname> = name.into();
172    /// assert_eq!(&*boxed_name, name);
173    /// let boxed_str: Box<str> = boxed_name.into_boxed_str();
174    /// assert_eq!(&*boxed_str, name.as_str());
175    /// # Ok::<_, xml_string::names::NameError>(())
176    /// ```
177    #[cfg(feature = "alloc")]
178    pub fn into_boxed_str(self: alloc::boxed::Box<Self>) -> Box<str> {
179        unsafe {
180            // This is safe because `Ncname` has the same memory layout as `str`
181            // (thanks to `#[repr(transparent)]`).
182            alloc::boxed::Box::<str>::from_raw(alloc::boxed::Box::<Self>::into_raw(self) as *mut str)
183        }
184    }
185}
186
187impl_traits_for_custom_string_slice!(Ncname);
188
189impl AsRef<Nmtoken> for Ncname {
190    #[inline]
191    fn as_ref(&self) -> &Nmtoken {
192        unsafe {
193            debug_assert!(
194                Nmtoken::from_str(self.as_str()).is_ok(),
195                "NCName {:?} must be a valid Nmtoken",
196                self.as_str()
197            );
198            // This is safe because an NCName is also a valid Nmtoken.
199            Nmtoken::new_unchecked(self.as_str())
200        }
201    }
202}
203
204impl AsRef<Name> for Ncname {
205    #[inline]
206    fn as_ref(&self) -> &Name {
207        unsafe {
208            debug_assert!(
209                Name::from_str(self.as_str()).is_ok(),
210                "An NCName is also a Name"
211            );
212            Name::new_unchecked(self.as_str())
213        }
214    }
215}
216
217impl AsRef<Qname> for Ncname {
218    #[inline]
219    fn as_ref(&self) -> &Qname {
220        unsafe {
221            debug_assert!(
222                Qname::from_str(self.as_str()).is_ok(),
223                "An NCName is also a Qname"
224            );
225            Qname::new_unchecked(self.as_str())
226        }
227    }
228}
229
230impl AsRef<Eqname> for Ncname {
231    #[inline]
232    fn as_ref(&self) -> &Eqname {
233        unsafe {
234            debug_assert!(
235                Eqname::from_str(self.as_str()).is_ok(),
236                "An NCName is also a Eqname"
237            );
238            Eqname::new_unchecked(self.as_str())
239        }
240    }
241}
242
243impl<'a> TryFrom<&'a str> for &'a Ncname {
244    type Error = NameError;
245
246    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
247        Ncname::validate(s)?;
248        Ok(unsafe {
249            // This is safe because the string is validated.
250            Ncname::new_unchecked(s)
251        })
252    }
253}
254
255impl<'a> TryFrom<&'a Name> for &'a Ncname {
256    type Error = NameError;
257
258    fn try_from(s: &'a Name) -> Result<Self, Self::Error> {
259        if let Some(colon_pos) = s.as_str().find(':') {
260            return Err(NameError::new(TargetNameType::Ncname, colon_pos));
261        }
262
263        unsafe {
264            debug_assert!(
265                Ncname::validate(s.as_str()).is_ok(),
266                "Name {:?} without colons is also a valid NCName",
267                s.as_str()
268            );
269            Ok(Ncname::new_unchecked(s.as_str()))
270        }
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    fn ensure_eq(s: &str) {
279        assert_eq!(
280            Ncname::from_str(s).expect("Should not fail"),
281            s,
282            "String: {:?}",
283            s
284        );
285    }
286
287    fn ensure_error_at(s: &str, valid_up_to: usize) {
288        let err = Ncname::from_str(s).expect_err("Should fail");
289        assert_eq!(err.valid_up_to(), valid_up_to, "String: {:?}", s);
290    }
291
292    #[test]
293    fn ncname_str_valid() {
294        ensure_eq("hello");
295        ensure_eq("abc123");
296    }
297
298    #[test]
299    fn ncname_str_invalid() {
300        ensure_error_at("", 0);
301        ensure_error_at("-foo", 0);
302        ensure_error_at("0foo", 0);
303        ensure_error_at("foo bar", 3);
304        ensure_error_at("foo/bar", 3);
305
306        ensure_error_at("foo:bar", 3);
307        ensure_error_at(":foo", 0);
308        ensure_error_at("foo:", 3);
309    }
310}