Skip to main content

urn_rs/
lib.rs

1//! A crate for handling [URNs](https://datatracker.ietf.org/doc/html/rfc8141).
2//!
3//! Features
4//! - `serde` - [Serde](https://serde.rs) support
5//! - `std` (enabled by default) - [`std::error::Error`] integration
6//! - `alloc` (enabled by default) - [alloc](https://doc.rust-lang.org/alloc/index.html) support
7//!   (you probably want to keep this enabled)
8//!
9//! # Example
10//! ```
11//! # #[cfg(not(feature = "std"))]
12//! # fn main() { }
13//! # #[cfg(feature = "std")]
14//! # use urn_rs::{Urn, UrnSlice, UrnBuilder};
15//! # #[cfg(feature = "std")]
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! let urn = UrnBuilder::new("example", "1234:5678").build()?;
18//! assert_eq!(urn.as_str(), "urn:example:1234:5678");
19//! assert_eq!(urn, "urn:example:1234:5678".parse::<Urn>()?); // Using std::str::parse
20//! assert_eq!(urn.nss(), "1234:5678");
21//! # Ok(())
22//! # }
23//! ```
24#![allow(clippy::missing_panics_doc)]
25#![cfg_attr(not(feature = "std"), no_std)]
26#![cfg_attr(docsrs, feature(doc_cfg))]
27#[cfg(not(feature = "std"))]
28extern crate alloc;
29#[cfg(all(not(feature = "std"), feature = "alloc"))]
30use alloc::{borrow::ToOwned, string::String, vec::Vec};
31#[cfg(feature = "alloc")]
32use core::str::FromStr;
33use core::{
34    cmp::Ordering,
35    convert::{TryFrom, TryInto},
36    fmt,
37    hash::{self, Hash},
38    num::{NonZeroU8, NonZeroU32},
39    ops::Range,
40};
41
42#[cfg(feature = "std")]
43use std::borrow::ToOwned;
44
45mod cow;
46use cow::TriCow;
47
48mod tables;
49
50#[cfg(feature = "alloc")]
51mod owned;
52#[cfg(feature = "alloc")]
53#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
54pub use owned::Urn;
55
56#[cfg(feature = "alloc")]
57#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
58pub mod percent;
59#[cfg(not(feature = "alloc"))]
60mod percent;
61use percent::{
62    normalize_range,
63    parse_f_component,
64    parse_nss,
65    parse_q_component,
66    parse_r_component,
67    validate_f_component,
68    validate_nss,
69    validate_q_component,
70    validate_r_component,
71};
72
73#[cfg(feature = "serde")]
74mod serde;
75
76/// Checks whether a string is a valid NID
77fn is_valid_nid(s: &str) -> bool {
78    check_nid(s).is_ok()
79}
80
81/// Single-pass NID validation plus ASCII-uppercase detection. Returns
82/// `Ok(has_upper)` for a valid NID, `Err(InvalidNid)` otherwise.
83fn check_nid(s: &str) -> Result<bool> {
84    let bytes = s.as_bytes();
85    if bytes.len() < 2 || bytes.len() > 32 || bytes[0] == b'-' {
86        return Err(Error::InvalidNid);
87    }
88    let mut has_upper = false;
89    for &b in bytes {
90        if tables::BYTE_CLASS[b as usize] & tables::NID == 0 {
91            return Err(Error::InvalidNid);
92        }
93        has_upper |= b.is_ascii_uppercase();
94    }
95    Ok(has_upper)
96}
97
98const URN_PREFIX: &str = "urn:";
99const NID_NSS_SEPARATOR: &str = ":";
100const RCOMP_PREFIX: &str = "?+";
101const QCOMP_PREFIX: &str = "?=";
102const FCOMP_PREFIX: &str = "#";
103
104fn parse_urn(mut s: TriCow) -> Result<UrnSlice> {
105    // ensure that the first 4 bytes are a valid substring
106    if !s.is_char_boundary(URN_PREFIX.len()) {
107        return Err(Error::InvalidScheme);
108    }
109
110    s.make_lowercase(..URN_PREFIX.len())?;
111
112    if &s[..URN_PREFIX.len()] != URN_PREFIX {
113        return Err(Error::InvalidScheme);
114    }
115
116    let nid_start = URN_PREFIX.len();
117    let nid_end = nid_start
118        + s[nid_start..].find(NID_NSS_SEPARATOR).ok_or_else(|| {
119            if is_valid_nid(&s[nid_start..]) {
120                // If NID is present, but the NSS and its separator aren't, it counts as an NSS error
121                Error::InvalidNss
122            } else {
123                // the NSS separator couldn't be found, but whatever has been found doesn't even count as a valid NID
124                Error::InvalidNid
125            }
126        })?;
127
128    if !is_valid_nid(&s[nid_start..nid_end]) {
129        return Err(Error::InvalidNid);
130    }
131
132    // Now that we know the NID is valid, normalize it
133    s.make_lowercase(nid_start..nid_end)?;
134
135    let nss_start = nid_end + NID_NSS_SEPARATOR.len();
136    let nss_end = parse_nss(&mut s, nss_start)?;
137
138    // NSS must be at least one character long
139    if nss_end == nss_start {
140        return Err(Error::InvalidNss);
141    }
142
143    let mut end = nss_end;
144    let mut last_component_error = Error::InvalidNss;
145
146    let r_component_len = if s[end..].starts_with(RCOMP_PREFIX) {
147        let rc_start = end + RCOMP_PREFIX.len();
148        end = parse_r_component(&mut s, rc_start)?;
149        last_component_error = Error::InvalidRComponent;
150        Some((end - rc_start).try_into().ok().and_then(NonZeroU32::new).ok_or(last_component_error)?)
151    } else {
152        None
153    };
154
155    let q_component_len = if s[end..].starts_with(QCOMP_PREFIX) {
156        let qc_start = end + QCOMP_PREFIX.len();
157        end = parse_q_component(&mut s, qc_start)?;
158        last_component_error = Error::InvalidQComponent;
159        Some((end - qc_start).try_into().ok().and_then(NonZeroU32::new).ok_or(last_component_error)?)
160    } else {
161        None
162    };
163
164    if s[end..].starts_with(FCOMP_PREFIX) {
165        let fc_start = end + FCOMP_PREFIX.len();
166        end = parse_f_component(&mut s, fc_start)?;
167        last_component_error = Error::InvalidFComponent;
168    }
169
170    if end < s.len() {
171        return Err(last_component_error);
172    }
173
174    // NID length range is 2..=32 bytes, so it always fits into non-zero u8.
175    let nid_len = u8::try_from(nid_end - nid_start).ok().and_then(NonZeroU8::new).ok_or(Error::InvalidNid)?;
176    // NSS always has non-zero length.
177    let nss_len = u32::try_from(nss_end - nss_start).ok().and_then(NonZeroU32::new).ok_or(Error::InvalidNss)?;
178    Ok(UrnSlice {
179        urn: s,
180        nid_len,
181        nss_len,
182        r_component_len,
183        q_component_len,
184    })
185}
186
187/// A URN validation error.
188#[non_exhaustive]
189#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
190pub enum Error {
191    /// The URN has an invalid scheme.
192    #[error("invalid urn scheme")]
193    InvalidScheme,
194    /// The URN has an invalid NID (Namespace ID).
195    #[error("invalid urn nid (namespace id)")]
196    InvalidNid,
197    /// The URN has an invalid NSS (Namespace-specific string).
198    #[error("invalid urn nss (namespace-specific string)")]
199    InvalidNss,
200    /// The URN has an invalid r-component.
201    #[error("invalid urn r-component")]
202    InvalidRComponent,
203    /// The URN has an invalid q-component.
204    #[error("invalid urn q-component")]
205    InvalidQComponent,
206    /// The URN has an invalid f-component.
207    #[error("invalid urn f-component (fragment)")]
208    InvalidFComponent,
209    /// Allocation is required, but not possible. This is only ever created when `alloc` feature
210    /// is disabled.
211    #[error("an allocation was required, but not possible")]
212    AllocRequired,
213    /// The input bytes are not valid UTF-8.
214    #[error("urn contains invalid utf-8")]
215    InvalidUtf8,
216}
217
218type Result<T, E = Error> = core::result::Result<T, E>;
219
220/// A borrowed RFC2141/8141 URN (Uniform Resource Name). This is a copy-on-write type.
221///
222/// It will have to allocate if you call any of the setters. If you create it via
223/// `TryFrom<String>`, the provided buffer will be used. If you disable `alloc` feature, this stops
224/// being a copy-on-write type and starts being a regular borrow.
225///
226/// **Note:** the equivalence checks are done
227/// [according to the specification](https://www.rfc-editor.org/rfc/rfc8141.html#section-3),
228/// only taking the NID and NSS into account! If you need exact equivalence checks, consider
229/// comparing using `Urn::as_str()` as the key, or enable the `exact-eq` feature to make
230/// `PartialEq`, `Ord`, and `Hash` operate on the whole (normalized) URN string including
231/// r-, q-, and f-components. Some namespaces may define additional lexical
232/// equivalence rules, these aren't accounted for in this implementation (Meaning there might be
233/// false negatives for some namespaces). There will, however, be no false positives.
234///
235/// # Equivalence and the `Borrow`/`Hash` contract
236///
237/// With the default (`exact-eq` **off**) equivalence, two URNs can compare `Eq`-equal while
238/// holding distinct `as_str()` representations (they may differ in r/q/f-components). A
239/// hypothetical `Borrow<str>` impl would therefore violate the [`Borrow`] / [`Eq`] / [`Hash`]
240/// contract, so this crate intentionally does not provide one. Callers who need `HashMap`
241/// lookup keyed by the raw URN string should either key on `String` directly, call
242/// `.as_str().to_owned()` after parsing, or enable the `exact-eq` feature — the latter makes
243/// the hash contract match `as_str()`, at the cost of RFC 8141 §3 equivalence.
244///
245/// [`Borrow`]: core::borrow::Borrow
246///
247/// Unlike [`Urn`]:
248/// - When created via `TryFrom<&str>`, allocations only occur if the URN isn't normalized
249///   (uppercase percent-encoded characters and lowercase `urn` scheme and NID)
250/// - When created via `TryFrom<&mut str>`, no allocations are done at all.
251///
252/// `FromStr` is always required to allocate, so you should use `TryFrom` when possible.
253pub struct UrnSlice<'a> {
254    // Entire URN string
255    urn: TriCow<'a>,
256    nid_len: NonZeroU8,
257    nss_len: NonZeroU32,
258    r_component_len: Option<NonZeroU32>,
259    q_component_len: Option<NonZeroU32>,
260}
261
262impl UrnSlice<'static> {
263    /// Parse a `&'static str` into a `UrnSlice<'static>`.
264    ///
265    /// When the input is already normalized (lowercase `urn:` scheme + NID, uppercase hex in
266    /// every `%XX` triplet), this is zero-allocation — the returned slice borrows the input.
267    /// Otherwise the parser promotes to an owned buffer to perform the normalization.
268    ///
269    /// ```
270    /// # use urn_rs::UrnSlice;
271    /// let u = UrnSlice::from_static("urn:example:foo").unwrap();
272    /// assert_eq!(u.as_str(), "urn:example:foo");
273    /// ```
274    ///
275    /// # Errors
276    /// Returns the component-specific error variant on validation failure.
277    pub fn from_static(s: &'static str) -> Result<UrnSlice<'static>> {
278        parse_urn(TriCow::Borrowed(s))
279    }
280}
281
282impl<'a> UrnSlice<'a> {
283    #[inline]
284    const fn nid_range(&self) -> Range<usize> {
285        // urn:<nid>
286        let start = URN_PREFIX.len();
287        start..start + self.nid_len.get() as usize
288    }
289
290    #[inline]
291    const fn nss_range(&self) -> Range<usize> {
292        // ...<nid>:<nss>
293        let start = self.nid_range().end + NID_NSS_SEPARATOR.len();
294        start..start + self.nss_len.get() as usize
295    }
296
297    #[inline]
298    fn r_component_range(&self) -> Option<Range<usize>> {
299        self.r_component_len.map(|r_component_len| {
300            // ...<nss>[?+<r-component>]
301            let start = self.nss_range().end + RCOMP_PREFIX.len();
302            start..start + r_component_len.get() as usize
303        })
304    }
305
306    /// end of the last component before q-component
307    #[inline]
308    fn pre_q_component_end(&self) -> usize {
309        self.r_component_range().unwrap_or_else(|| self.nss_range()).end
310    }
311
312    #[inline]
313    fn q_component_range(&self) -> Option<Range<usize>> {
314        self.q_component_len.map(|q_component_len| {
315            // ...<nss>[?+<r-component>][?=<q-component>]
316            let start = self.pre_q_component_end() + QCOMP_PREFIX.len();
317            start..start + q_component_len.get() as usize
318        })
319    }
320
321    /// end of the last component before f-component
322    #[inline]
323    fn pre_f_component_end(&self) -> usize {
324        self.q_component_range()
325            .or_else(|| self.r_component_range())
326            .unwrap_or_else(|| self.nss_range())
327            .end
328    }
329
330    /// Consume self, returning an owned `Urn`. Moves buffer when already owned.
331    #[cfg(feature = "alloc")]
332    #[inline]
333    pub(crate) fn into_owned(self) -> Urn {
334        Urn(UrnSlice {
335            urn: match self.urn {
336                TriCow::Owned(s) => TriCow::Owned(s),
337                TriCow::Borrowed(s) => TriCow::Owned(s.to_owned()),
338                TriCow::MutBorrowed(s) => TriCow::Owned(s.to_owned()),
339            },
340            nid_len: self.nid_len,
341            nss_len: self.nss_len,
342            r_component_len: self.r_component_len,
343            q_component_len: self.q_component_len,
344        })
345    }
346
347    #[inline]
348    fn f_component_start(&self) -> Option<usize> {
349        // ...[#<f-component>]
350        Some(self.pre_f_component_end()).filter(|x| *x < self.urn.len()).map(|x| x + FCOMP_PREFIX.len())
351    }
352
353    /// Normalized string representation of this URN.
354    ///
355    /// The returned string is *not* a verbatim copy of the input. Per RFC 8141, parsing
356    /// applies these transformations:
357    /// - the `urn:` scheme and the NID are lowercased;
358    /// - percent-encoded triplets (`%XX`) have their hex digits uppercased.
359    ///
360    /// The NSS and the r/q/f components are otherwise preserved as-given.
361    ///
362    /// ```
363    /// # #[cfg(not(feature = "alloc"))] fn main() {}
364    /// # #[cfg(feature = "alloc")]
365    /// # fn main() { test_main().unwrap() }
366    /// # #[cfg(feature = "alloc")]
367    /// # fn test_main() -> Result<(), urn_rs::Error> {
368    /// use urn_rs::UrnSlice;
369    /// let urn = UrnSlice::try_from("uRn:eXaMpLe:%3d%3a")?;
370    /// assert_eq!(urn.as_str(), "urn:example:%3D%3A");
371    /// # Ok(()) }
372    /// ```
373    #[must_use]
374    #[inline]
375    pub fn as_str(&self) -> &str {
376        &self.urn
377    }
378
379    /// NID (Namespace identifier), the first part of the URN.
380    ///
381    /// For example, in `urn:ietf:rfc:2648`, `ietf` is the namespace.
382    #[must_use]
383    #[inline]
384    pub fn nid(&self) -> &str {
385        &self.urn[self.nid_range()]
386    }
387    /// Set the NID (must be [a valid
388    /// NID](https://datatracker.ietf.org/doc/html/rfc8141#section-2)).
389    ///
390    /// # Errors
391    /// Returns [`Error::InvalidNid`] in case of a validation failure.
392    pub fn set_nid(&mut self, nid: &str) -> Result<()> {
393        let has_upper = check_nid(nid)?;
394        // check_nid enforced 2..=32 byte length, so this always fits into non-zero u8.
395        let nid_len = u8::try_from(nid.len()).ok().and_then(NonZeroU8::new).ok_or(Error::InvalidNid)?;
396        let range = self.nid_range();
397        let start = range.start;
398        self.urn.replace_range(range, nid)?;
399        if has_upper {
400            // After a successful replace_range, self.urn is Owned or MutBorrowed; make_lowercase
401            // on those variants is infallible.
402            self.urn.make_lowercase(start..start + nid.len())?;
403        }
404        self.nid_len = nid_len;
405        Ok(())
406    }
407    /// Percent-encoded NSS (Namespace-specific string) identifying the resource.
408    ///
409    /// For example, in `urn:ietf:rfc:2648`, `rfs:2648` is the NSS.
410    ///
411    /// # See also
412    /// - [`percent::decode_nss`]
413    #[must_use]
414    #[inline]
415    pub fn nss(&self) -> &str {
416        &self.urn[self.nss_range()]
417    }
418    /// Set the NSS (must be [a valid NSS](https://datatracker.ietf.org/doc/html/rfc8141#section-2)
419    /// and use percent-encoding).
420    ///
421    /// # Errors
422    /// Returns [`Error::InvalidNss`] in case of a validation failure.
423    ///
424    /// # See also
425    /// - [`percent::encode_nss`]
426    pub fn set_nss(&mut self, nss: &str) -> Result<()> {
427        let (end, needs_norm) = validate_nss(nss);
428        if nss.is_empty() || end != nss.len() {
429            return Err(Error::InvalidNss);
430        }
431        // NSS length is non-zero as checked above.
432        let nss_len = u32::try_from(nss.len()).ok().and_then(NonZeroU32::new).ok_or(Error::InvalidNss)?;
433        let range = self.nss_range();
434        let start = range.start;
435        self.urn.replace_range(range, nss)?;
436        if needs_norm {
437            normalize_range(&mut self.urn, start..start + nss.len())?;
438        }
439        self.nss_len = nss_len;
440        Ok(())
441    }
442    /// Percent-encoded r-component, following the `?+` character sequence, to be used for passing
443    /// parameters to URN resolution services.
444    ///
445    /// In `urn:example:foo-bar-baz-qux?+CCResolve:cc=uk`, the r-component is `CCResolve:cc=uk`.
446    ///
447    /// Should not be used for equivalence checks. As of the time of writing this, exact semantics
448    /// aren't in the RFC.
449    ///
450    /// # See also
451    /// - [`percent::decode_r_component`]
452    #[must_use]
453    #[inline]
454    pub fn r_component(&self) -> Option<&str> {
455        self.r_component_range().map(|range| &self.urn[range])
456    }
457    /// Set the r-component (must be [a valid
458    /// r-component](https://datatracker.ietf.org/doc/html/rfc8141#section-2) and use
459    /// percent-encoding).
460    ///
461    /// # Errors
462    /// Returns [`Error::InvalidRComponent`] in case of a validation failure.
463    ///
464    /// # See also
465    /// - [`percent::encode_r_component`]
466    pub fn set_r_component(&mut self, r_component: Option<&str>) -> Result<()> {
467        if let Some(rc) = r_component {
468            let (end, needs_norm) = validate_r_component(rc);
469            if rc.is_empty() || end != rc.len() {
470                return Err(Error::InvalidRComponent);
471            }
472            let rc_len = u32::try_from(rc.len()).ok().and_then(NonZeroU32::new).ok_or(Error::InvalidRComponent)?;
473            let range = if let Some(range) = self.r_component_range() {
474                range
475            } else {
476                // insert RCOMP_PREFIX if r-component doesn't already exist
477                let nss_end = self.nss_range().end;
478                self.urn.replace_range(nss_end..nss_end, RCOMP_PREFIX)?;
479                nss_end + RCOMP_PREFIX.len()..nss_end + RCOMP_PREFIX.len()
480            };
481            let start = range.start;
482            self.urn.replace_range(range, rc)?;
483            if needs_norm {
484                normalize_range(&mut self.urn, start..start + rc.len())?;
485            }
486            self.r_component_len = Some(rc_len);
487        } else if let Some(mut range) = self.r_component_range() {
488            range.start -= RCOMP_PREFIX.len();
489            self.urn.replace_range(range, "")?;
490            self.r_component_len = None;
491        }
492        Ok(())
493    }
494    /// Percent-encoded q-component, following the `?=` character sequence. Has a similar function to the URL query
495    /// string.
496    ///
497    /// In `urn:example:weather?=op=map&lat=39.56&lon=-104.85`,
498    /// the q-component is `op=map&lat=39.56&lon=-104.85`.
499    ///
500    /// Should not be used for equivalence checks.
501    ///
502    /// # See also
503    /// - [`percent::decode_q_component`]
504    #[must_use]
505    #[inline]
506    pub fn q_component(&self) -> Option<&str> {
507        self.q_component_range().map(|range| &self.urn[range])
508    }
509    /// Set the q-component (must be [a valid
510    /// q-component](https://datatracker.ietf.org/doc/html/rfc8141#section-2) and use
511    /// percent-encoding).
512    ///
513    /// # Errors
514    /// Returns [`Error::InvalidQComponent`] in case of a validation failure.
515    ///
516    /// # See also
517    /// - [`percent::encode_q_component`]
518    pub fn set_q_component(&mut self, q_component: Option<&str>) -> Result<()> {
519        if let Some(qc) = q_component {
520            let (end, needs_norm) = validate_q_component(qc);
521            if qc.is_empty() || end != qc.len() {
522                return Err(Error::InvalidQComponent);
523            }
524            let qc_len = u32::try_from(qc.len()).ok().and_then(NonZeroU32::new).ok_or(Error::InvalidQComponent)?;
525            let range = if let Some(range) = self.q_component_range() {
526                range
527            } else {
528                // insert QCOMP_PREFIX if q-component doesn't already exist
529                let pre_qc_end = self.pre_q_component_end();
530                self.urn.replace_range(pre_qc_end..pre_qc_end, QCOMP_PREFIX)?;
531                pre_qc_end + QCOMP_PREFIX.len()..pre_qc_end + QCOMP_PREFIX.len()
532            };
533            let start = range.start;
534            self.urn.replace_range(range, qc)?;
535            if needs_norm {
536                normalize_range(&mut self.urn, start..start + qc.len())?;
537            }
538            self.q_component_len = Some(qc_len);
539        } else if let Some(mut range) = self.q_component_range() {
540            range.start -= QCOMP_PREFIX.len();
541            self.urn.replace_range(range, "")?;
542            self.q_component_len = None;
543        }
544        Ok(())
545    }
546    /// Percent-encoded f-component following the `#` character at the end of the URN. Has a
547    /// similar function to the URL fragment.
548    ///
549    /// In `urn:example:a123,z456#789`, the f-component is `789`.
550    ///
551    /// Should not be used for equivalence checks.
552    ///
553    /// # See also
554    /// - [`percent::decode_f_component`]
555    #[must_use]
556    #[inline]
557    pub fn f_component(&self) -> Option<&str> {
558        self.f_component_start().map(|start| &self.urn[start..])
559    }
560    /// Set the f-component (must be [a valid
561    /// f-component](https://datatracker.ietf.org/doc/html/rfc8141#section-2) and use
562    /// percent-encoding).
563    ///
564    /// # Errors
565    /// Returns [`Error::InvalidFComponent`] in case of a validation failure.
566    ///
567    /// # See also
568    /// - [`percent::encode_f_component`]
569    pub fn set_f_component(&mut self, f_component: Option<&str>) -> Result<()> {
570        if let Some(fc) = f_component {
571            let (end, needs_norm) = validate_f_component(fc);
572            if end != fc.len() {
573                return Err(Error::InvalidFComponent);
574            }
575            let start = if let Some(start) = self.f_component_start() {
576                start
577            } else {
578                let range = self.urn.len()..self.urn.len();
579                self.urn.replace_range(range, FCOMP_PREFIX)?;
580                self.urn.len()
581            };
582            let len = self.urn.len();
583            self.urn.replace_range(start..len, fc)?;
584            if needs_norm {
585                normalize_range(&mut self.urn, start..start + fc.len())?;
586            }
587        } else if let Some(start) = self.f_component_start() {
588            let len = self.urn.len();
589            self.urn.replace_range(start - FCOMP_PREFIX.len()..len, "")?;
590        }
591        Ok(())
592    }
593}
594
595#[cfg(feature = "alloc")]
596#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
597impl<'a> ToOwned for UrnSlice<'a> {
598    type Owned = Urn;
599    fn to_owned(&self) -> Self::Owned {
600        Urn::from(self)
601    }
602}
603
604impl fmt::Debug for UrnSlice<'_> {
605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606        write!(f, "UrnSlice({})", self.as_str())
607    }
608}
609
610#[cfg(feature = "alloc")]
611impl PartialEq<Urn> for UrnSlice<'_> {
612    fn eq(&self, other: &Urn) -> bool {
613        self == &other.0
614    }
615}
616
617impl AsRef<[u8]> for UrnSlice<'_> {
618    fn as_ref(&self) -> &[u8] {
619        self.urn.as_bytes()
620    }
621}
622
623impl AsRef<str> for UrnSlice<'_> {
624    fn as_ref(&self) -> &str {
625        &self.urn
626    }
627}
628
629impl UrnSlice<'_> {
630    /// Portion of the normalized URN string used for equivalence (`PartialEq`,
631    /// `Ord`, `Hash`). With the `exact-eq` feature this is the whole URN
632    /// including r/q/f-components; otherwise just scheme + NID + NSS per
633    /// RFC 8141 §3.
634    #[inline]
635    fn eq_slice(&self) -> &str {
636        #[cfg(feature = "exact-eq")]
637        {
638            &self.urn[..]
639        }
640        #[cfg(not(feature = "exact-eq"))]
641        {
642            &self.urn[..self.nss_range().end]
643        }
644    }
645}
646
647impl PartialEq for UrnSlice<'_> {
648    fn eq(&self, other: &Self) -> bool {
649        self.eq_slice() == other.eq_slice()
650    }
651}
652
653impl Eq for UrnSlice<'_> {}
654
655impl Ord for UrnSlice<'_> {
656    fn cmp(&self, other: &Self) -> Ordering {
657        self.eq_slice().cmp(other.eq_slice())
658    }
659}
660
661impl PartialOrd for UrnSlice<'_> {
662    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
663        Some(self.cmp(other))
664    }
665}
666
667impl Hash for UrnSlice<'_> {
668    fn hash<H: hash::Hasher>(&self, state: &mut H) {
669        self.eq_slice().hash(state);
670    }
671}
672
673impl fmt::Display for UrnSlice<'_> {
674    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
675        f.write_str(&self.urn)
676    }
677}
678
679#[cfg(feature = "alloc")]
680#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
681impl FromStr for UrnSlice<'_> {
682    type Err = Error;
683    fn from_str(s: &str) -> Result<Self> {
684        parse_urn(TriCow::Owned(s.to_owned()))
685    }
686}
687
688impl<'a> TryFrom<&'a str> for UrnSlice<'a> {
689    type Error = Error;
690    fn try_from(value: &'a str) -> Result<Self> {
691        parse_urn(TriCow::Borrowed(value))
692    }
693}
694
695/// Construct a `UrnSlice` from a mutable string slice, normalizing in place with no allocation.
696///
697/// The input buffer is mutated directly (lowercasing the scheme/NID and uppercasing
698/// percent-encoded triplets), and the returned `UrnSlice` borrows it — so no heap
699/// allocation occurs on any input.
700///
701/// ```
702/// # use urn_rs::UrnSlice;
703/// let mut buf = *b"uRn:eXaMpLe:Foo-Bar";
704/// let s = core::str::from_utf8_mut(&mut buf[..]).unwrap();
705/// let urn = UrnSlice::try_from(s).unwrap();
706/// assert_eq!(urn.as_str(), "urn:example:Foo-Bar");
707/// ```
708impl<'a> TryFrom<&'a mut str> for UrnSlice<'a> {
709    type Error = Error;
710    fn try_from(value: &'a mut str) -> Result<Self> {
711        parse_urn(TriCow::MutBorrowed(value))
712    }
713}
714
715#[cfg(feature = "alloc")]
716#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
717impl TryFrom<String> for UrnSlice<'static> {
718    type Error = Error;
719    fn try_from(value: String) -> Result<Self> {
720        parse_urn(TriCow::Owned(value))
721    }
722}
723
724impl<'a> TryFrom<&'a [u8]> for UrnSlice<'a> {
725    type Error = Error;
726    fn try_from(value: &'a [u8]) -> Result<Self> {
727        let s = core::str::from_utf8(value).map_err(|_| Error::InvalidUtf8)?;
728        parse_urn(TriCow::Borrowed(s))
729    }
730}
731
732impl<'a> TryFrom<&'a mut [u8]> for UrnSlice<'a> {
733    type Error = Error;
734    fn try_from(value: &'a mut [u8]) -> Result<Self> {
735        let s = core::str::from_utf8_mut(value).map_err(|_| Error::InvalidUtf8)?;
736        parse_urn(TriCow::MutBorrowed(s))
737    }
738}
739
740#[cfg(feature = "alloc")]
741#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
742impl TryFrom<Vec<u8>> for UrnSlice<'static> {
743    type Error = Error;
744    fn try_from(value: Vec<u8>) -> Result<Self> {
745        let s = String::from_utf8(value).map_err(|_| Error::InvalidUtf8)?;
746        parse_urn(TriCow::Owned(s))
747    }
748}
749
750/// A struct used for constructing URNs.
751///
752/// # Example
753/// ```
754/// # #[cfg(not(feature = "std"))]
755/// # fn main() { }
756/// # #[cfg(feature = "std")]
757/// # use urn_rs::{Urn, UrnBuilder};
758/// # #[cfg(feature = "std")]
759/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
760/// let urn = UrnBuilder::new("example", "1234:5678").build()?;
761/// assert_eq!(urn.as_str(), "urn:example:1234:5678");
762/// assert_eq!(urn, "urn:example:1234:5678".parse::<Urn>()?); // Using std::str::parse
763/// assert_eq!(urn.nss(), "1234:5678");
764/// # Ok(())
765/// # }
766/// ```
767#[cfg(feature = "alloc")]
768#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
769#[derive(Debug)]
770#[must_use]
771pub struct UrnBuilder<'a> {
772    nid: &'a str,
773    nss: &'a str,
774    r_component: Option<&'a str>,
775    q_component: Option<&'a str>,
776    f_component: Option<&'a str>,
777}
778
779#[cfg(feature = "alloc")]
780impl<'a> UrnBuilder<'a> {
781    /// Create a new `UrnBuilder`.
782    ///
783    /// - `nid`: the namespace identifier
784    /// - `nss`: the percent-encoded NSS (namespace-specific string)
785    ///
786    /// # See also
787    /// - [`percent::encode_nss`]
788    pub const fn new(nid: &'a str, nss: &'a str) -> Self {
789        Self {
790            nid,
791            nss,
792            r_component: None,
793            q_component: None,
794            f_component: None,
795        }
796    }
797    /// Change the namespace identifier.
798    pub const fn nid(mut self, nid: &'a str) -> Self {
799        self.nid = nid;
800        self
801    }
802    /// Change the namespace-specific string (must be percent encoded).
803    ///
804    /// # See also
805    /// - [`percent::encode_nss`]
806    pub const fn nss(mut self, nss: &'a str) -> Self {
807        self.nss = nss;
808        self
809    }
810    /// Change the r-component (must be percent encoded).
811    ///
812    /// # See also
813    /// - [`percent::encode_r_component`]
814    pub const fn r_component(mut self, r_component: Option<&'a str>) -> Self {
815        self.r_component = r_component;
816        self
817    }
818    /// Change the q-component (must be percent encoded).
819    ///
820    /// # See also
821    /// - [`percent::encode_q_component`]
822    pub const fn q_component(mut self, q_component: Option<&'a str>) -> Self {
823        self.q_component = q_component;
824        self
825    }
826    /// Change the f-component (must be percent encoded).
827    ///
828    /// # See also
829    /// - [`percent::encode_f_component`]
830    pub const fn f_component(mut self, f_component: Option<&'a str>) -> Self {
831        self.f_component = f_component;
832        self
833    }
834    /// [Validate the data](https://datatracker.ietf.org/doc/html/rfc8141#section-2) and create the URN.
835    ///
836    /// # Errors
837    ///
838    /// In case of a validation failure, returns an error specifying the component that failed
839    /// validation
840    pub fn build(self) -> Result<Urn> {
841        self.build_inner().map(Urn)
842    }
843
844    /// Same as [`build`](Self::build), but returns a [`UrnSlice<'static>`] directly — useful for
845    /// callers storing `UrnSlice<'static>` who don't want to go through the [`Urn`] newtype.
846    ///
847    /// # Errors
848    ///
849    /// In case of a validation failure, returns an error specifying the component that failed
850    /// validation
851    pub fn build_slice(self) -> Result<UrnSlice<'static>> {
852        self.build_inner()
853    }
854
855    fn build_inner(self) -> Result<UrnSlice<'static>> {
856        if !is_valid_nid(self.nid) {
857            return Err(Error::InvalidNid);
858        }
859        if self.nss.is_empty() {
860            return Err(Error::InvalidNss);
861        }
862
863        // Validate each component on the input slices, so we never allocate a buffer
864        // for a URN that would be rejected.
865        let (nss_end_in, nss_needs_norm) = validate_nss(self.nss);
866        if nss_end_in != self.nss.len() {
867            return Err(Error::InvalidNss);
868        }
869        let rc_needs_norm = if let Some(rc) = self.r_component {
870            let (end, needs_norm) = validate_r_component(rc);
871            if rc.is_empty() || end != rc.len() {
872                return Err(Error::InvalidRComponent);
873            }
874            needs_norm
875        } else {
876            false
877        };
878        let qc_needs_norm = if let Some(qc) = self.q_component {
879            let (end, needs_norm) = validate_q_component(qc);
880            if qc.is_empty() || end != qc.len() {
881                return Err(Error::InvalidQComponent);
882            }
883            needs_norm
884        } else {
885            false
886        };
887        let fc_needs_norm = if let Some(fc) = self.f_component {
888            let (end, needs_norm) = validate_f_component(fc);
889            if end != fc.len() {
890                return Err(Error::InvalidFComponent);
891            }
892            needs_norm
893        } else {
894            false
895        };
896
897        let total = URN_PREFIX.len()
898            + self.nid.len()
899            + NID_NSS_SEPARATOR.len()
900            + self.nss.len()
901            + self.r_component.map_or(0, |x| RCOMP_PREFIX.len() + x.len())
902            + self.q_component.map_or(0, |x| QCOMP_PREFIX.len() + x.len())
903            + self.f_component.map_or(0, |x| FCOMP_PREFIX.len() + x.len());
904        let mut buf = String::with_capacity(total);
905        buf.push_str(URN_PREFIX);
906        buf.push_str(self.nid);
907        buf.push_str(NID_NSS_SEPARATOR);
908        let nss_start = buf.len();
909        buf.push_str(self.nss);
910        let nss_end = buf.len();
911
912        let rc_range = self.r_component.map(|rc| {
913            buf.push_str(RCOMP_PREFIX);
914            let start = buf.len();
915            buf.push_str(rc);
916            start..buf.len()
917        });
918        let qc_range = self.q_component.map(|qc| {
919            buf.push_str(QCOMP_PREFIX);
920            let start = buf.len();
921            buf.push_str(qc);
922            start..buf.len()
923        });
924        let fc_range = self.f_component.map(|fc| {
925            buf.push_str(FCOMP_PREFIX);
926            let start = buf.len();
927            buf.push_str(fc);
928            start..buf.len()
929        });
930
931        let mut s = TriCow::Owned(buf);
932        if nss_needs_norm {
933            normalize_range(&mut s, nss_start..nss_end)?;
934        }
935        if let Some(range) = rc_range.as_ref().filter(|_| rc_needs_norm) {
936            normalize_range(&mut s, range.clone())?;
937        }
938        if let Some(range) = qc_range.as_ref().filter(|_| qc_needs_norm) {
939            normalize_range(&mut s, range.clone())?;
940        }
941        if let Some(range) = fc_range.as_ref().filter(|_| fc_needs_norm) {
942            normalize_range(&mut s, range.clone())?;
943        }
944        // NID length range is 2..=32 bytes, so it always fits into non-zero u8.
945        let nid_len = u8::try_from(self.nid.len()).ok().and_then(NonZeroU8::new).ok_or(Error::InvalidNid)?;
946        // NSS length is non-zero as checked above.
947        let nss_len = u32::try_from(self.nss.len()).ok().and_then(NonZeroU32::new).ok_or(Error::InvalidNss)?;
948        let r_component_len = self
949            .r_component
950            .map(|x| u32::try_from(x.len()).ok().and_then(NonZeroU32::new).ok_or(Error::InvalidRComponent))
951            .transpose()?;
952        let q_component_len = self
953            .q_component
954            .map(|x| u32::try_from(x.len()).ok().and_then(NonZeroU32::new).ok_or(Error::InvalidQComponent))
955            .transpose()?;
956        Ok(UrnSlice {
957            // we already had to allocate since we use a builder, obviously allocations are allowed
958            urn: s,
959            nid_len,
960            nss_len,
961            r_component_len,
962            q_component_len,
963        })
964    }
965}
966
967#[cfg(test)]
968#[allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
969mod tests {
970    use super::*;
971
972    #[cfg(not(feature = "std"))]
973    use super::alloc::string::ToString;
974    #[cfg(all(not(feature = "std"), feature = "alloc"))]
975    use super::alloc::{vec, vec::Vec};
976
977    #[test]
978    fn it_works() {
979        UrnSlice::try_from("6򭞦*�").unwrap_err();
980        #[cfg(feature = "alloc")]
981        assert_eq!(
982            UrnSlice::try_from("urn:nbn:de:bvb:19-146642").unwrap(),
983            UrnBuilder::new("nbn", "de:bvb:19-146642").build().unwrap()
984        );
985        assert_eq!(UrnSlice::try_from("urn:nbn:de:bvb:19-146642").unwrap().to_string(), "urn:nbn:de:bvb:19-146642");
986
987        #[cfg(feature = "alloc")]
988        assert_eq!(
989            UrnSlice::try_from("urn:example:foo-bar-baz-qux?+CCResolve:cc=uk#test").unwrap(),
990            UrnBuilder::new("example", "foo-bar-baz-qux")
991                .r_component(Some("CCResolve:cc=uk"))
992                .f_component(Some("test"))
993                .build()
994                .unwrap()
995        );
996        assert_eq!(
997            UrnSlice::try_from("urn:example:foo-bar-baz-qux?+CCResolve:cc=uk#test")
998                .unwrap()
999                .f_component()
1000                .unwrap(),
1001            "test"
1002        );
1003        assert_eq!(
1004            UrnSlice::try_from("urn:example:foo-bar-baz-qux?+CCResolve:cc=uk#test")
1005                .unwrap()
1006                .r_component()
1007                .unwrap(),
1008            "CCResolve:cc=uk"
1009        );
1010        assert_eq!(
1011            UrnSlice::try_from("urn:example:foo-bar-baz-qux?+CCResolve:cc=uk#test").unwrap().to_string(),
1012            "urn:example:foo-bar-baz-qux?+CCResolve:cc=uk#test",
1013        );
1014
1015        #[cfg(feature = "alloc")]
1016        assert_eq!(
1017            "urn:example:weather?=op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z"
1018                .parse::<UrnSlice>()
1019                .unwrap(),
1020            UrnBuilder::new("example", "weather")
1021                .q_component(Some("op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z"))
1022                .build()
1023                .unwrap()
1024        );
1025        assert_eq!(
1026            UrnSlice::try_from("urn:example:weather?=op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z")
1027                .unwrap()
1028                .to_string(),
1029            "urn:example:weather?=op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z"
1030        );
1031
1032        // This equality relies on q-component being excluded from eq (RFC 8141 §3).
1033        // With `exact-eq`, the q-component participates in eq, so the assertion no longer holds.
1034        #[cfg(all(feature = "alloc", not(feature = "exact-eq")))]
1035        assert_eq!(
1036            "uRn:eXaMpLe:%3d%3a?=aoiwnfuafo".parse::<UrnSlice>().unwrap(),
1037            UrnBuilder::new("example", "%3D%3a").build().unwrap()
1038        );
1039        let mut arr = *b"uRn:eXaMpLe:%3d%3a?=aoiwnfuafo";
1040        assert_eq!(
1041            UrnSlice::try_from(core::str::from_utf8_mut(&mut arr[..]).unwrap()).unwrap().as_str(),
1042            "urn:example:%3D%3A?=aoiwnfuafo",
1043        );
1044
1045        assert_eq!(UrnSlice::try_from("urn:-example:abcd"), Err(Error::InvalidNid));
1046        assert_eq!(UrnSlice::try_from("urn:example:/abcd"), Err(Error::InvalidNss));
1047        assert_eq!(UrnSlice::try_from("urn:a:abcd"), Err(Error::InvalidNid));
1048        assert_eq!(UrnSlice::try_from("urn:0123456789abcdef0123456789abcdef0:abcd"), Err(Error::InvalidNid));
1049        let _ = UrnSlice::try_from("urn:0123456789abcdef0123456789abcdef:abcd").unwrap();
1050        assert_eq!(UrnSlice::try_from("urn:example"), Err(Error::InvalidNss));
1051        assert_eq!(UrnSlice::try_from("urn:example:"), Err(Error::InvalidNss));
1052        assert_eq!(UrnSlice::try_from("urn:example:%"), Err(Error::InvalidNss));
1053        assert_eq!(UrnSlice::try_from("urn:example:%a"), Err(Error::InvalidNss));
1054        assert_eq!(UrnSlice::try_from("urn:example:%a_"), Err(Error::InvalidNss));
1055        let mut arr = *b"urn:example:%a0?+";
1056        assert_eq!(
1057            UrnSlice::try_from(core::str::from_utf8_mut(&mut arr[..]).unwrap()),
1058            Err(Error::InvalidRComponent)
1059        );
1060        let mut arr = *b"urn:example:%a0?+%a0?=";
1061        assert_eq!(
1062            UrnSlice::try_from(core::str::from_utf8_mut(&mut arr[..]).unwrap()),
1063            Err(Error::InvalidQComponent)
1064        );
1065        let mut arr = *b"urn:example:%a0?+%a0?=a";
1066        assert_eq!(
1067            UrnSlice::try_from(core::str::from_utf8_mut(&mut arr[..]).unwrap())
1068                .unwrap()
1069                .r_component()
1070                .unwrap(),
1071            "%A0",
1072        );
1073
1074        #[cfg(feature = "alloc")]
1075        {
1076            let mut urn = "urn:example:test".parse::<UrnSlice>().unwrap();
1077            urn.set_f_component(Some("f-component")).unwrap();
1078            assert_eq!(urn.f_component(), Some("f-component"));
1079            assert_eq!(urn.as_str(), "urn:example:test#f-component");
1080            urn.set_f_component(Some("")).unwrap();
1081            assert_eq!(urn.f_component(), Some(""));
1082            assert_eq!(urn.as_str(), "urn:example:test#");
1083            urn.set_q_component(Some("abcd")).unwrap();
1084            assert_eq!(urn.q_component(), Some("abcd"));
1085            assert_eq!(urn.as_str(), "urn:example:test?=abcd#");
1086            assert!(urn.set_q_component(Some("")).is_err());
1087            urn.set_r_component(Some("%2a")).unwrap();
1088            assert_eq!(urn.r_component(), Some("%2A"));
1089            assert_eq!(urn.as_str(), "urn:example:test?+%2A?=abcd#");
1090            urn.set_nid("a-b").unwrap();
1091            assert_eq!(urn.as_str(), "urn:a-b:test?+%2A?=abcd#");
1092            urn.set_r_component(None).unwrap();
1093            assert_eq!(urn.as_str(), "urn:a-b:test?=abcd#");
1094            assert_eq!(urn.r_component(), None);
1095        }
1096    }
1097
1098    /// Zero-copy `&mut str` parse must mutate in place without heap-allocating;
1099    /// the resulting `UrnSlice` must point at the original buffer.
1100    #[test]
1101    fn mut_str_normalizes_in_place() {
1102        let mut buf = *b"uRn:eXaMpLe:Foo-Bar";
1103        let buf_ptr = buf.as_ptr();
1104        let s = core::str::from_utf8_mut(&mut buf[..]).unwrap();
1105        let urn = UrnSlice::try_from(s).unwrap();
1106        assert_eq!(urn.as_str().as_ptr(), buf_ptr);
1107        assert_eq!(urn.as_str(), "urn:example:Foo-Bar");
1108    }
1109
1110    #[cfg(all(feature = "alloc", not(feature = "exact-eq")))]
1111    #[test]
1112    fn ord_matches_eq() {
1113        use core::cmp::Ordering;
1114        let a = UrnSlice::try_from("urn:example:abc").unwrap();
1115        let b = UrnSlice::try_from("urn:example:abd").unwrap();
1116        let c = UrnSlice::try_from("urn:example:abc?=q").unwrap(); // q-component excluded from cmp
1117        assert_eq!(a.cmp(&b), Ordering::Less);
1118        assert_eq!(b.cmp(&a), Ordering::Greater);
1119        assert_eq!(a.cmp(&c), Ordering::Equal);
1120        assert_eq!(a, c);
1121        // Urn delegates
1122        let ao = Urn::try_from("urn:example:abc").unwrap();
1123        let bo = Urn::try_from("urn:example:abd").unwrap();
1124        assert!(ao < bo);
1125    }
1126
1127    #[cfg(all(feature = "alloc", feature = "exact-eq"))]
1128    #[test]
1129    fn exact_eq_includes_rqf() {
1130        use core::cmp::Ordering;
1131        #[cfg(feature = "std")]
1132        use core::hash::BuildHasher;
1133        #[cfg(feature = "std")]
1134        use std::collections::hash_map::RandomState;
1135
1136        let a = UrnSlice::try_from("urn:example:abc").unwrap();
1137        let b = UrnSlice::try_from("urn:example:abc?=q").unwrap();
1138        let c = UrnSlice::try_from("urn:example:abc?=q").unwrap();
1139        let d = UrnSlice::try_from("urn:example:abc#frag").unwrap();
1140        let e = UrnSlice::try_from("urn:example:abc?+r").unwrap();
1141
1142        // r/q/f now affect equality & ordering.
1143        assert_ne!(a, b);
1144        assert_ne!(a, d);
1145        assert_ne!(a, e);
1146        assert_ne!(b, d);
1147        assert_eq!(b, c);
1148        assert_eq!(a.cmp(&b), Ordering::Less);
1149
1150        // Hash/Eq contract: equal values hash identically.
1151        #[cfg(feature = "std")]
1152        {
1153            let rs = RandomState::new();
1154            assert_eq!(rs.hash_one(&b), rs.hash_one(&c));
1155            assert_ne!(rs.hash_one(&a), rs.hash_one(&b));
1156        }
1157    }
1158
1159    #[cfg(feature = "alloc")]
1160    #[test]
1161    fn decode_iter_matches_decode() {
1162        use crate::percent::{decode_nss, decode_nss_iter};
1163        let input = "hello%20world%21";
1164        let via_iter: Result<Vec<u8>, _> = decode_nss_iter(input).collect();
1165        assert_eq!(via_iter.unwrap(), decode_nss(input).unwrap().into_bytes());
1166
1167        // Validation error surfaces via iterator.
1168        let bad = "%zz";
1169        let res: Result<Vec<u8>, _> = decode_nss_iter(bad).collect();
1170        assert_eq!(res, Err(Error::InvalidNss));
1171    }
1172
1173    #[test]
1174    fn byte_slice_try_from_roundtrip() {
1175        let bytes: &[u8] = b"urn:example:foo";
1176        let urn = UrnSlice::try_from(bytes).unwrap();
1177        assert_eq!(urn.as_str(), "urn:example:foo");
1178
1179        // invalid UTF-8 rejected
1180        let bad: &[u8] = &[0xFFu8, b'u', b'r', b'n'];
1181        assert_eq!(UrnSlice::try_from(bad).unwrap_err(), Error::InvalidUtf8);
1182    }
1183
1184    #[test]
1185    fn mut_byte_slice_normalizes_in_place() {
1186        let mut buf = *b"uRn:eXaMpLe:Foo-Bar";
1187        let buf_ptr = buf.as_ptr();
1188        let urn = UrnSlice::try_from(&mut buf[..]).unwrap();
1189        assert_eq!(urn.as_str().as_ptr(), buf_ptr);
1190        assert_eq!(urn.as_str(), "urn:example:Foo-Bar");
1191    }
1192
1193    #[cfg(feature = "alloc")]
1194    #[test]
1195    fn vec_try_from_roundtrip() {
1196        let v: Vec<u8> = b"urn:example:foo".to_vec();
1197        let urn = UrnSlice::try_from(v).unwrap();
1198        assert_eq!(urn.as_str(), "urn:example:foo");
1199
1200        let bad: Vec<u8> = vec![0xFFu8];
1201        assert_eq!(UrnSlice::try_from(bad).unwrap_err(), Error::InvalidUtf8);
1202    }
1203
1204    #[cfg(feature = "alloc")]
1205    #[test]
1206    fn build_slice_matches_build() {
1207        let a = UrnBuilder::new("example", "1234:5678").build_slice().unwrap();
1208        let b = UrnBuilder::new("example", "1234:5678").build().unwrap();
1209        assert_eq!(a.as_str(), b.as_str());
1210        assert_eq!(a.as_str(), "urn:example:1234:5678");
1211    }
1212
1213    #[test]
1214    fn from_static_normalized_borrows() {
1215        // Normalized input → parser stays on Borrowed path (zero-alloc).
1216        let s: &'static str = "urn:example:foo";
1217        let urn = UrnSlice::from_static(s).unwrap();
1218        // Same allocation: as_str() pointer equals the static string pointer.
1219        assert_eq!(urn.as_str().as_ptr(), s.as_ptr());
1220    }
1221
1222    #[cfg(feature = "alloc")]
1223    #[test]
1224    fn from_static_denormalized_still_ok() {
1225        // Denormalized: parser promotes to Owned, still yields normalized output.
1226        let urn = UrnSlice::from_static("uRn:eXaMpLe:%3d%3a").unwrap();
1227        assert_eq!(urn.as_str(), "urn:example:%3D%3A");
1228    }
1229
1230    #[cfg(feature = "alloc")]
1231    #[test]
1232    fn encode_iter_matches_encode() {
1233        use crate::percent::{
1234            encode_f_component,
1235            encode_f_component_iter,
1236            encode_nss,
1237            encode_nss_iter,
1238            encode_q_component,
1239            encode_q_component_iter,
1240            encode_r_component,
1241            encode_r_component_iter,
1242        };
1243        let input = "hello world!😂";
1244
1245        let via_iter: Vec<u8> = encode_nss_iter(input).collect();
1246        assert_eq!(via_iter, encode_nss(input).unwrap().into_bytes());
1247
1248        let via_iter: Vec<u8> = encode_r_component_iter(input).collect();
1249        assert_eq!(via_iter, encode_r_component(input).unwrap().into_bytes());
1250
1251        let via_iter: Vec<u8> = encode_q_component_iter(input).collect();
1252        assert_eq!(via_iter, encode_q_component(input).unwrap().into_bytes());
1253
1254        let via_iter: Vec<u8> = encode_f_component_iter(input).collect();
1255        assert_eq!(via_iter, encode_f_component(input).unwrap().into_bytes());
1256
1257        // Context-dependent handling: leading '?' in r-component must be encoded;
1258        // non-leading '?' (when not followed by '=') passes through.
1259        let r = encode_r_component_iter("?x").collect::<Vec<u8>>();
1260        assert_eq!(r, encode_r_component("?x").unwrap().into_bytes());
1261        let r = encode_r_component_iter("x?y").collect::<Vec<u8>>();
1262        assert_eq!(r, encode_r_component("x?y").unwrap().into_bytes());
1263    }
1264}