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}