Skip to main content

identity_kit/did/
syntax.rs

1//! `Did` and `DidUrl`: owned, validated strings plus the scanner that validates them.
2//!
3//! Both types store the input unchanged. The scanner only ever advances over ASCII bytes, so every
4//! offset it reports or stores is a `char` boundary of the input.
5
6use core::fmt;
7use core::hash::{Hash, Hasher};
8use core::str::FromStr;
9
10use serde::de::Error as _;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13use super::DidError;
14
15/// Default maximum input length in bytes for [`Did::parse`] and [`DidUrl::parse`]: 2 KiB.
16///
17/// [`Did::join`] keeps to it as well, so a DID URL it builds can always be parsed back with the
18/// default limit; [`DidUrl::with_fragment`] and [`DidUrl::with_did`], which cannot fail, may
19/// build a longer one.
20pub const DEFAULT_MAX_LEN: usize = 2048;
21
22/// A decentralized identifier such as `did:web:example.com`.
23///
24/// A `Did` is validated against the DID syntax of [W3C DID Core §3.1] when it is constructed and
25/// stores the input unchanged: no percent-decoding, no case folding, no normalization. Two DIDs are
26/// equal exactly when their strings are equal.
27///
28/// # Examples
29///
30/// ```
31/// use identity_kit::did::Did;
32///
33/// let did = Did::parse("did:web:example.com%3A8443:user:alice")?;
34/// assert_eq!(did.method(), "web");
35/// assert_eq!(did.method_specific_id(), "example.com%3A8443:user:alice");
36/// assert_eq!(did.as_str(), "did:web:example.com%3A8443:user:alice");
37///
38/// // The scheme and the method name are lowercase; a DID has no path, query or fragment.
39/// assert!(Did::parse("DID:web:example.com").is_err());
40/// assert!(Did::parse("did:Web:example.com").is_err());
41/// assert!(Did::parse("did:web:example.com#key-1").is_err());
42/// # Ok::<(), identity_kit::did::DidError>(())
43/// ```
44///
45/// A `Did` serializes as its string and deserialization validates:
46///
47/// ```
48/// use identity_kit::did::Did;
49/// use serde::Deserialize;
50/// use serde::de::IntoDeserializer;
51/// use serde::de::value::{Error, StrDeserializer};
52///
53/// let input: StrDeserializer<'_, Error> = "did:example:123".into_deserializer();
54/// let did = Did::deserialize(input)?;
55/// assert_eq!(did.as_str(), "did:example:123");
56///
57/// let input: StrDeserializer<'_, Error> = "did:example:".into_deserializer();
58/// assert!(Did::deserialize(input).is_err());
59/// # Ok::<(), Error>(())
60/// ```
61///
62/// [W3C DID Core §3.1]: https://www.w3.org/TR/did-core/#did-syntax
63#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub struct Did(String);
65
66impl Did {
67    /// Parses a DID, rejecting input longer than [`DEFAULT_MAX_LEN`] bytes.
68    ///
69    /// # Errors
70    ///
71    /// Returns a [`DidError`] describing the first violation of the DID syntax.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use identity_kit::did::{Did, DidError};
77    ///
78    /// let did = Did::parse("did:example:123456789abcdefghi")?;
79    /// assert_eq!(did.method(), "example");
80    /// assert_eq!(did.method_specific_id(), "123456789abcdefghi");
81    ///
82    /// assert_eq!(Did::parse("example:123"), Err(DidError::MissingScheme));
83    /// assert_eq!(Did::parse("did:example:a:"), Err(DidError::EmptyMethodSpecificId));
84    /// assert_eq!(
85    ///     Did::parse("did:example:ab%2"),
86    ///     Err(DidError::InvalidPercentEncoding { position: 14 })
87    /// );
88    /// # Ok::<(), DidError>(())
89    /// ```
90    pub fn parse(s: &str) -> Result<Self, DidError> {
91        Self::parse_with_max_len(s, DEFAULT_MAX_LEN)
92    }
93
94    /// Parses a DID, rejecting input longer than `max_len` bytes with [`DidError::TooLong`].
95    ///
96    /// # Errors
97    ///
98    /// Returns a [`DidError`] describing the first violation of the DID syntax.
99    pub fn parse_with_max_len(s: &str, max_len: usize) -> Result<Self, DidError> {
100        validate_did(s, max_len)?;
101        Ok(Self(s.to_owned()))
102    }
103
104    /// The method name, for example `web` in `did:web:example.com`.
105    pub fn method(&self) -> &str {
106        &self.0[SCHEME.len()..self.method_end()]
107    }
108
109    /// The method-specific id, for example `example.com` in `did:web:example.com`.
110    pub fn method_specific_id(&self) -> &str {
111        &self.0[self.method_end() + 1..]
112    }
113
114    /// The DID as it was parsed.
115    pub fn as_str(&self) -> &str {
116        &self.0
117    }
118
119    /// This DID as a [`DidUrl`] without path, query or fragment.
120    #[must_use]
121    pub fn to_url(&self) -> DidUrl {
122        DidUrl {
123            url: self.0.clone(),
124            did: self.clone(),
125            query_start: None,
126            fragment_start: None,
127        }
128    }
129
130    /// Appends a relative reference (`"#key-1"`, `"/path"`, `"?versionId=1"` or a combination
131    /// such as `"/path?versionId=1#key-1"`) to this DID.
132    ///
133    /// The relative reference is taken as already percent-encoded. An empty reference yields the
134    /// same as [`Did::to_url`].
135    ///
136    /// # Errors
137    ///
138    /// Returns [`DidError::InvalidPath`] when `relative` does not start with `/`, `?` or `#`,
139    /// the respective [`DidError`] when a component is not valid per RFC 3986, and
140    /// [`DidError::TooLong`] when the DID URL would exceed [`DEFAULT_MAX_LEN`] bytes.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use identity_kit::did::{Did, DidError};
146    ///
147    /// let did = Did::parse("did:example:123")?;
148    ///
149    /// let key = did.join("#key-1")?;
150    /// assert_eq!(key.fragment(), Some("key-1"));
151    /// assert_eq!(key.to_string(), "did:example:123#key-1");
152    ///
153    /// let versioned = did.join("/path?versionId=1")?;
154    /// assert_eq!(versioned.path(), Some("/path"));
155    /// assert_eq!(versioned.query(), Some("versionId=1"));
156    /// assert_eq!(versioned.fragment(), None);
157    ///
158    /// assert_eq!(did.join("key-1"), Err(DidError::InvalidPath));
159    /// # Ok::<(), DidError>(())
160    /// ```
161    pub fn join(&self, relative: &str) -> Result<DidUrl, DidError> {
162        if !relative.is_empty() && !relative.starts_with(['/', '?', '#']) {
163            return Err(DidError::InvalidPath);
164        }
165        let mut url = String::with_capacity(self.0.len() + relative.len());
166        url.push_str(&self.0);
167        url.push_str(relative);
168        check_len(&url, DEFAULT_MAX_LEN)?;
169        let (query_start, fragment_start) = scan_url_suffix(&url, self.0.len())?;
170        Ok(DidUrl {
171            url,
172            did: self.clone(),
173            query_start,
174            fragment_start,
175        })
176    }
177
178    /// Byte offset of the `:` that ends the method name.
179    fn method_end(&self) -> usize {
180        // Always present: construction guarantees `did:` + method name + `:`.
181        self.0[SCHEME.len()..]
182            .find(':')
183            .map_or(self.0.len(), |i| SCHEME.len() + i)
184    }
185}
186
187impl fmt::Display for Did {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        f.write_str(&self.0)
190    }
191}
192
193impl AsRef<str> for Did {
194    fn as_ref(&self) -> &str {
195        &self.0
196    }
197}
198
199impl FromStr for Did {
200    type Err = DidError;
201
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        Self::parse(s)
204    }
205}
206
207impl TryFrom<&str> for Did {
208    type Error = DidError;
209
210    fn try_from(s: &str) -> Result<Self, Self::Error> {
211        Self::parse(s)
212    }
213}
214
215impl TryFrom<String> for Did {
216    type Error = DidError;
217
218    fn try_from(s: String) -> Result<Self, Self::Error> {
219        validate_did(&s, DEFAULT_MAX_LEN)?;
220        Ok(Self(s))
221    }
222}
223
224impl Serialize for Did {
225    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
226        serializer.serialize_str(&self.0)
227    }
228}
229
230impl<'de> Deserialize<'de> for Did {
231    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
232        let s = String::deserialize(deserializer)?;
233        Self::try_from(s).map_err(D::Error::custom)
234    }
235}
236
237/// A DID URL such as `did:example:123/path?versionId=1#key-1`.
238///
239/// A `DidUrl` is a [`Did`] followed by an optional path, query and fragment as defined by
240/// [W3C DID Core §3.2] and RFC 3986. It is validated on construction and stores the input unchanged.
241/// A `DidUrl` without path, query or fragment displays as the bare DID.
242///
243/// Equality, ordering and hashing are those of the URL string.
244///
245/// # Examples
246///
247/// ```
248/// use identity_kit::did::DidUrl;
249///
250/// let url = DidUrl::parse("did:example:123/path/to?versionId=1#frag")?;
251/// assert_eq!(url.did().as_str(), "did:example:123");
252/// assert_eq!(url.path(), Some("/path/to"));
253/// assert_eq!(url.query(), Some("versionId=1"));
254/// assert_eq!(url.fragment(), Some("frag"));
255///
256/// let key = DidUrl::parse("did:example:123#key-1")?;
257/// assert_eq!(key.path(), None);
258/// assert_eq!(key.without_fragment().as_str(), "did:example:123");
259/// assert_eq!(key.with_fragment("key-2").as_str(), "did:example:123#key-2");
260/// # Ok::<(), identity_kit::did::DidError>(())
261/// ```
262///
263/// [W3C DID Core §3.2]: https://www.w3.org/TR/did-core/#did-url-syntax
264#[derive(Clone)]
265pub struct DidUrl {
266    /// The whole URL as parsed.
267    url: String,
268    /// The DID prefix of `url`.
269    did: Did,
270    /// Byte offset of the `?` that starts the query, if any.
271    query_start: Option<usize>,
272    /// Byte offset of the `#` that starts the fragment, if any.
273    fragment_start: Option<usize>,
274}
275
276impl DidUrl {
277    /// Parses a DID URL, rejecting input longer than [`DEFAULT_MAX_LEN`] bytes.
278    ///
279    /// # Errors
280    ///
281    /// Returns a [`DidError`] describing the first violation of the DID URL syntax.
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use identity_kit::did::{DidError, DidUrl};
287    ///
288    /// let url = DidUrl::parse("did:example:123?service=agent&relativeRef=%2Fsome%2Fpath")?;
289    /// assert_eq!(url.query(), Some("service=agent&relativeRef=%2Fsome%2Fpath"));
290    ///
291    /// // An empty fragment is present but empty, as in RFC 3986.
292    /// assert_eq!(DidUrl::parse("did:example:123#")?.fragment(), Some(""));
293    ///
294    /// assert_eq!(DidUrl::parse("did:example:123#a#b"), Err(DidError::InvalidFragment));
295    /// # Ok::<(), DidError>(())
296    /// ```
297    pub fn parse(s: &str) -> Result<Self, DidError> {
298        Self::parse_with_max_len(s, DEFAULT_MAX_LEN)
299    }
300
301    /// Parses a DID URL, rejecting input longer than `max_len` bytes with [`DidError::TooLong`].
302    ///
303    /// # Errors
304    ///
305    /// Returns a [`DidError`] describing the first violation of the DID URL syntax.
306    pub fn parse_with_max_len(s: &str, max_len: usize) -> Result<Self, DidError> {
307        Self::from_string(s.to_owned(), max_len)
308    }
309
310    /// The DID this URL refers to.
311    pub fn did(&self) -> &Did {
312        &self.did
313    }
314
315    /// The path, starting with `/`, or `None` when the URL has no path.
316    pub fn path(&self) -> Option<&str> {
317        let end = self
318            .query_start
319            .or(self.fragment_start)
320            .unwrap_or(self.url.len());
321        let path = &self.url[self.did.0.len()..end];
322        (!path.is_empty()).then_some(path)
323    }
324
325    /// The query without the leading `?`, or `None` when the URL has no `?`.
326    pub fn query(&self) -> Option<&str> {
327        self.query_start
328            .map(|start| &self.url[start + 1..self.fragment_start.unwrap_or(self.url.len())])
329    }
330
331    /// The fragment without the leading `#`, or `None` when the URL has no `#`.
332    pub fn fragment(&self) -> Option<&str> {
333        self.fragment_start.map(|start| &self.url[start + 1..])
334    }
335
336    /// The DID URL as it was parsed.
337    pub fn as_str(&self) -> &str {
338        &self.url
339    }
340
341    /// A copy of this URL with its fragment replaced by `fragment`.
342    ///
343    /// `fragment` is taken as already percent-encoded: a `%` followed by two hexadecimal digits is
344    /// kept, and every other byte that RFC 3986 does not allow in a fragment (including a stray
345    /// `%`) is percent-encoded, so the result is always a valid DID URL.
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// use identity_kit::did::DidUrl;
351    ///
352    /// let url = DidUrl::parse("did:example:123?versionId=1#key-1")?;
353    /// assert_eq!(url.with_fragment("key-2").as_str(), "did:example:123?versionId=1#key-2");
354    /// assert_eq!(url.with_fragment("a b").fragment(), Some("a%20b"));
355    /// # Ok::<(), identity_kit::did::DidError>(())
356    /// ```
357    #[must_use]
358    pub fn with_fragment(&self, fragment: &str) -> Self {
359        let base = self.fragment_start.unwrap_or(self.url.len());
360        let mut url = String::with_capacity(base + 1 + fragment.len());
361        url.push_str(&self.url[..base]);
362        url.push('#');
363        encode_fragment_into(&mut url, fragment);
364        Self {
365            url,
366            did: self.did.clone(),
367            query_start: self.query_start,
368            fragment_start: Some(base),
369        }
370    }
371
372    /// A copy of this URL with its DID replaced by `did`, keeping path, query and fragment.
373    ///
374    /// # Examples
375    ///
376    /// ```
377    /// use identity_kit::did::{Did, DidUrl};
378    ///
379    /// let url = DidUrl::parse("did:example:123/path?versionId=1#key-1")?;
380    /// let moved = url.with_did(Did::parse("did:web:example.com")?);
381    /// assert_eq!(moved.as_str(), "did:web:example.com/path?versionId=1#key-1");
382    /// assert_eq!(moved.fragment(), Some("key-1"));
383    /// # Ok::<(), identity_kit::did::DidError>(())
384    /// ```
385    #[must_use]
386    pub fn with_did(&self, did: Did) -> Self {
387        let old_len = self.did.0.len();
388        let suffix = &self.url[old_len..];
389        let mut url = String::with_capacity(did.0.len() + suffix.len());
390        url.push_str(&did.0);
391        url.push_str(suffix);
392        let shift = |offset: usize| offset - old_len + did.0.len();
393        Self {
394            url,
395            query_start: self.query_start.map(shift),
396            fragment_start: self.fragment_start.map(shift),
397            did,
398        }
399    }
400
401    /// A copy of this URL without a fragment.
402    #[must_use]
403    pub fn without_fragment(&self) -> Self {
404        let end = self.fragment_start.unwrap_or(self.url.len());
405        Self {
406            url: self.url[..end].to_owned(),
407            did: self.did.clone(),
408            query_start: self.query_start,
409            fragment_start: None,
410        }
411    }
412
413    /// Validates `url` and takes ownership of it.
414    fn from_string(url: String, max_len: usize) -> Result<Self, DidError> {
415        check_len(&url, max_len)?;
416        let did_end = scan_did(&url)?;
417        let (query_start, fragment_start) = scan_url_suffix(&url, did_end)?;
418        let did = Did(url[..did_end].to_owned());
419        Ok(Self {
420            url,
421            did,
422            query_start,
423            fragment_start,
424        })
425    }
426}
427
428impl fmt::Debug for DidUrl {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        f.debug_tuple("DidUrl").field(&self.url).finish()
431    }
432}
433
434impl fmt::Display for DidUrl {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        f.write_str(&self.url)
437    }
438}
439
440impl PartialEq for DidUrl {
441    fn eq(&self, other: &Self) -> bool {
442        self.url == other.url
443    }
444}
445
446impl Eq for DidUrl {}
447
448impl PartialOrd for DidUrl {
449    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
450        Some(self.cmp(other))
451    }
452}
453
454impl Ord for DidUrl {
455    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
456        self.url.cmp(&other.url)
457    }
458}
459
460impl Hash for DidUrl {
461    fn hash<H: Hasher>(&self, state: &mut H) {
462        self.url.hash(state);
463    }
464}
465
466impl AsRef<str> for DidUrl {
467    fn as_ref(&self) -> &str {
468        &self.url
469    }
470}
471
472impl FromStr for DidUrl {
473    type Err = DidError;
474
475    fn from_str(s: &str) -> Result<Self, Self::Err> {
476        Self::parse(s)
477    }
478}
479
480impl TryFrom<&str> for DidUrl {
481    type Error = DidError;
482
483    fn try_from(s: &str) -> Result<Self, Self::Error> {
484        Self::parse(s)
485    }
486}
487
488impl TryFrom<String> for DidUrl {
489    type Error = DidError;
490
491    fn try_from(s: String) -> Result<Self, Self::Error> {
492        Self::from_string(s, DEFAULT_MAX_LEN)
493    }
494}
495
496impl Serialize for DidUrl {
497    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
498        serializer.serialize_str(&self.url)
499    }
500}
501
502impl<'de> Deserialize<'de> for DidUrl {
503    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
504        let s = String::deserialize(deserializer)?;
505        Self::try_from(s).map_err(D::Error::custom)
506    }
507}
508
509// ---------------------------------------------------------------------------------------------
510// Scanner
511// ---------------------------------------------------------------------------------------------
512
513const SCHEME: &str = "did:";
514
515/// Checks that `s` is a complete DID of at most `max_len` bytes.
516fn validate_did(s: &str, max_len: usize) -> Result<(), DidError> {
517    check_len(s, max_len)?;
518    let end = scan_did(s)?;
519    if end < s.len() {
520        return Err(invalid_char(s, end));
521    }
522    Ok(())
523}
524
525pub(super) fn check_len(s: &str, max_len: usize) -> Result<(), DidError> {
526    if s.len() > max_len {
527        return Err(DidError::TooLong { max: max_len });
528    }
529    Ok(())
530}
531
532/// Scans the DID at the start of `s` and returns the byte offset where it ends.
533///
534/// The DID ends at the end of the input or at the first character that is neither an `idchar` nor
535/// `:`. The caller decides whether anything may follow.
536fn scan_did(s: &str) -> Result<usize, DidError> {
537    let bytes = s.as_bytes();
538    if !s.starts_with(SCHEME) {
539        return Err(DidError::MissingScheme);
540    }
541
542    // method-name = 1*method-char
543    let mut i = SCHEME.len();
544    while i < bytes.len() && is_method_char(bytes[i]) {
545        i += 1;
546    }
547    if i == SCHEME.len() {
548        return Err(DidError::InvalidMethodName { position: i });
549    }
550    match bytes.get(i) {
551        Some(b':') => i += 1,
552        None | Some(b'/' | b'?' | b'#') => return Err(DidError::EmptyMethodSpecificId),
553        Some(_) => return Err(DidError::InvalidMethodName { position: i }),
554    }
555
556    // method-specific-id = *( *idchar ":" ) 1*idchar
557    let mut segment_len = 0;
558    while i < bytes.len() {
559        match bytes[i] {
560            b':' => {
561                segment_len = 0;
562                i += 1;
563            }
564            b'%' => {
565                check_pct_encoded(bytes, i)?;
566                segment_len += 3;
567                i += 3;
568            }
569            c if is_idchar(c) => {
570                segment_len += 1;
571                i += 1;
572            }
573            _ => break,
574        }
575    }
576    if segment_len == 0 {
577        return match bytes.get(i) {
578            None | Some(b'/' | b'?' | b'#') => Err(DidError::EmptyMethodSpecificId),
579            Some(_) => Err(invalid_char(s, i)),
580        };
581    }
582    Ok(i)
583}
584
585/// Scans `path-abempty [ "?" query ] [ "#" fragment ]` from `start` to the end of `s`.
586///
587/// Returns the byte offsets of the `?` and the `#`, if present.
588pub(super) fn scan_url_suffix(
589    s: &str,
590    start: usize,
591) -> Result<(Option<usize>, Option<usize>), DidError> {
592    let bytes = s.as_bytes();
593    let mut i = start;
594
595    // path-abempty = *( "/" segment ); segment = *pchar
596    if bytes.get(i) == Some(&b'/') {
597        while i < bytes.len() {
598            match bytes[i] {
599                b'?' | b'#' => break,
600                b'%' => {
601                    check_pct_encoded(bytes, i)?;
602                    i += 3;
603                }
604                c if is_pchar(c) || c == b'/' => i += 1,
605                _ => return Err(DidError::InvalidPath),
606            }
607        }
608    }
609
610    // query = *( pchar / "/" / "?" )
611    let mut query_start = None;
612    if bytes.get(i) == Some(&b'?') {
613        query_start = Some(i);
614        i += 1;
615        while i < bytes.len() {
616            match bytes[i] {
617                b'#' => break,
618                b'%' => {
619                    check_pct_encoded(bytes, i)?;
620                    i += 3;
621                }
622                c if is_fragment_or_query_char(c) => i += 1,
623                _ => return Err(DidError::InvalidQuery),
624            }
625        }
626    }
627
628    // fragment = *( pchar / "/" / "?" )
629    let mut fragment_start = None;
630    if bytes.get(i) == Some(&b'#') {
631        fragment_start = Some(i);
632        i += 1;
633        while i < bytes.len() {
634            match bytes[i] {
635                b'%' => {
636                    check_pct_encoded(bytes, i)?;
637                    i += 3;
638                }
639                c if is_fragment_or_query_char(c) => i += 1,
640                _ => return Err(DidError::InvalidFragment),
641            }
642        }
643    }
644
645    if i < bytes.len() {
646        return Err(invalid_char(s, i));
647    }
648    Ok((query_start, fragment_start))
649}
650
651/// Checks that the `%` at `position` is followed by two hexadecimal digits.
652fn check_pct_encoded(bytes: &[u8], position: usize) -> Result<(), DidError> {
653    if is_pct_encoded_at(bytes, position) {
654        Ok(())
655    } else {
656        Err(DidError::InvalidPercentEncoding { position })
657    }
658}
659
660pub(super) fn is_pct_encoded_at(bytes: &[u8], position: usize) -> bool {
661    matches!(
662        bytes.get(position..position + 3),
663        Some([b'%', hi, lo]) if hi.is_ascii_hexdigit() && lo.is_ascii_hexdigit()
664    )
665}
666
667/// Percent-encodes `fragment` into `out`, keeping bytes that RFC 3986 allows in a fragment and
668/// complete percent-encoded triplets as they are.
669fn encode_fragment_into(out: &mut String, fragment: &str) {
670    let bytes = fragment.as_bytes();
671    let mut i = 0;
672    while i < bytes.len() {
673        let c = bytes[i];
674        if c == b'%' && is_pct_encoded_at(bytes, i) {
675            out.push_str(&fragment[i..i + 3]);
676            i += 3;
677        } else {
678            if is_fragment_or_query_char(c) {
679                out.push(char::from(c));
680            } else {
681                out.push('%');
682                out.push(char::from(HEX_UPPER[usize::from(c >> 4)]));
683                out.push(char::from(HEX_UPPER[usize::from(c & 0x0f)]));
684            }
685            i += 1;
686        }
687    }
688}
689
690const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF";
691
692/// The character at byte offset `position`, which the scanner guarantees is a `char` boundary.
693fn invalid_char(s: &str, position: usize) -> DidError {
694    let ch = s
695        .get(position..)
696        .and_then(|rest| rest.chars().next())
697        .unwrap_or(char::REPLACEMENT_CHARACTER);
698    DidError::InvalidChar { position, ch }
699}
700
701/// `method-char = %x61-7A / DIGIT`
702pub(super) fn is_method_char(c: u8) -> bool {
703    c.is_ascii_lowercase() || c.is_ascii_digit()
704}
705
706/// `idchar = ALPHA / DIGIT / "." / "-" / "_"`, without `pct-encoded`, which the scanner handles.
707pub(super) fn is_idchar(c: u8) -> bool {
708    c.is_ascii_alphanumeric() || matches!(c, b'.' | b'-' | b'_')
709}
710
711/// `pchar = unreserved / sub-delims / ":" / "@"`, without `pct-encoded`, which the scanner handles.
712fn is_pchar(c: u8) -> bool {
713    c.is_ascii_alphanumeric()
714        || matches!(
715            c,
716            b'-' | b'.'
717                | b'_'
718                | b'~'
719                | b'!'
720                | b'$'
721                | b'&'
722                | b'\''
723                | b'('
724                | b')'
725                | b'*'
726                | b'+'
727                | b','
728                | b';'
729                | b'='
730                | b':'
731                | b'@'
732        )
733}
734
735/// `pchar / "/" / "?"`, the characters allowed in a query and in a fragment.
736fn is_fragment_or_query_char(c: u8) -> bool {
737    is_pchar(c) || c == b'/' || c == b'?'
738}