identity_did/
did_url.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use core::convert::TryFrom;
5use core::fmt::Debug;
6use core::fmt::Display;
7use core::fmt::Formatter;
8use core::str::FromStr;
9use std::cmp::Ordering;
10use std::hash::Hash;
11use std::hash::Hasher;
12
13use did_url_parser::DID as BaseDIDUrl;
14
15use identity_core::common::KeyComparable;
16use identity_core::common::Url;
17
18use crate::did::is_char_method_id;
19use crate::did::CoreDID;
20use crate::did::DID;
21use crate::Error;
22
23/// A [DID Url]: a [CoreDID] with [RelativeDIDUrl] components.
24///
25/// E.g. "did:iota:H3C2AVvLMv6gmMNam3uVAjZar3cJCwDwnZn6z3wXmqPV/path?query1=a&query2=b#fragment"
26///
27/// [DID Url]: https://www.w3.org/TR/did-core/#did-url-syntax
28#[derive(Clone, serde::Deserialize, serde::Serialize)]
29#[serde(into = "String", try_from = "String")]
30pub struct DIDUrl {
31  did: CoreDID,
32  url: RelativeDIDUrl,
33}
34
35/// A [relative DID Url] with the [path], [query], and [fragment] components defined according
36/// to [URI syntax](https://datatracker.ietf.org/doc/html/rfc5234).
37///
38/// E.g.
39/// - `"/path?query#fragment"`
40/// - `"/path"`
41/// - `"?query"`
42/// - `"#fragment"`
43///
44/// [relative DID Url]: https://www.w3.org/TR/did-core/#relative-did-urls
45/// [path]: https://www.w3.org/TR/did-core/#path
46/// [query]: https://www.w3.org/TR/did-core/#query
47/// [fragment]: https://www.w3.org/TR/did-core/#fragment
48#[derive(Clone, Default)]
49pub struct RelativeDIDUrl {
50  // Path including the leading '/'
51  path: Option<String>,
52  // Query including the leading '?'
53  query: Option<String>,
54  // Fragment including the leading '#'
55  fragment: Option<String>,
56}
57
58impl RelativeDIDUrl {
59  /// Create an empty [`RelativeDIDUrl`].
60  pub fn new() -> Self {
61    Self {
62      path: None,
63      query: None,
64      fragment: None,
65    }
66  }
67
68  /// Returns whether all URL segments are empty.
69  pub fn is_empty(&self) -> bool {
70    self.path.as_deref().unwrap_or_default().is_empty()
71      && self.query.as_deref().unwrap_or_default().is_empty()
72      && self.fragment.as_deref().unwrap_or_default().is_empty()
73  }
74
75  /// Return the [path](https://www.w3.org/TR/did-core/#path) component,
76  /// including the leading '/'.
77  ///
78  /// E.g. `"/path/sub-path/resource"`
79  pub fn path(&self) -> Option<&str> {
80    self.path.as_deref()
81  }
82
83  /// Attempt to set the [path](https://www.w3.org/TR/did-core/#path) component.
84  /// The path must start with a '/'.
85  ///
86  /// # Example
87  ///
88  /// ```
89  /// # use identity_did::RelativeDIDUrl;
90  /// # let mut url = RelativeDIDUrl::new();
91  /// url.set_path(Some("/path/sub-path/resource")).unwrap();
92  /// assert_eq!(url.path().unwrap(), "/path/sub-path/resource");
93  /// assert_eq!(url.to_string(), "/path/sub-path/resource");
94  /// ```
95  pub fn set_path(&mut self, value: Option<&str>) -> Result<(), Error> {
96    self.path = value
97      .filter(|s| !s.is_empty())
98      .map(|s| {
99        if s.starts_with('/') && is_valid_url_segment(s, is_char_path) {
100          Ok(s.to_owned())
101        } else {
102          Err(Error::InvalidPath)
103        }
104      })
105      .transpose()?;
106    Ok(())
107  }
108
109  /// Return the [path](https://www.w3.org/TR/did-core/#query) component,
110  /// excluding the leading '?' delimiter.
111  ///
112  /// E.g. `"?query1=a&query2=b" -> "query1=a&query2=b"`
113  pub fn query(&self) -> Option<&str> {
114    self.query.as_deref().and_then(|query| query.strip_prefix('?'))
115  }
116
117  /// Attempt to set the [query](https://www.w3.org/TR/did-core/#query) component.
118  /// A leading '?' is ignored.
119  ///
120  /// # Example
121  ///
122  /// ```
123  /// # use identity_did::RelativeDIDUrl;
124  /// # let mut url = RelativeDIDUrl::new();
125  /// // Set the query with a leading '?'
126  /// url.set_query(Some("?query1=a")).unwrap();
127  /// assert_eq!(url.query().unwrap(), "query1=a");
128  /// assert_eq!(url.to_string(), "?query1=a");
129  ///
130  /// // Set the query without a leading '?'
131  /// url.set_query(Some("query1=a&query2=b")).unwrap();
132  /// assert_eq!(url.query().unwrap(), "query1=a&query2=b");
133  /// assert_eq!(url.to_string(), "?query1=a&query2=b");
134  /// ```
135  pub fn set_query(&mut self, value: Option<&str>) -> Result<(), Error> {
136    self.query = value
137      .filter(|s| !s.is_empty())
138      .map(|mut s| {
139        // Ignore leading '?' during validation.
140        s = s.strip_prefix('?').unwrap_or(s);
141        if s.is_empty() || !is_valid_url_segment(s, is_char_query) {
142          return Err(Error::InvalidQuery);
143        }
144        Ok(format!("?{s}"))
145      })
146      .transpose()?;
147    Ok(())
148  }
149
150  /// Return an iterator of `(name, value)` pairs in the query string.
151  ///
152  /// E.g. `"query1=a&query2=b" -> [("query1", "a"), ("query2", "b")]`
153  ///
154  /// See [form_urlencoded::parse].
155  pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> {
156    form_urlencoded::parse(self.query().unwrap_or_default().as_bytes())
157  }
158
159  /// Return the [fragment](https://www.w3.org/TR/did-core/#fragment) component,
160  /// excluding the leading '#' delimiter.
161  ///
162  /// E.g. `"#fragment" -> "fragment"`
163  pub fn fragment(&self) -> Option<&str> {
164    self.fragment.as_deref().and_then(|fragment| fragment.strip_prefix('#'))
165  }
166
167  /// Attempt to set the [fragment](https://www.w3.org/TR/did-core/#fragment) component.
168  /// A leading '#' is ignored.
169  ///
170  /// # Example
171  ///
172  /// ```
173  /// # use identity_did::RelativeDIDUrl;
174  /// # let mut url = RelativeDIDUrl::new();
175  /// // Set the fragment with a leading '#'
176  /// url.set_fragment(Some("#fragment1")).unwrap();
177  /// assert_eq!(url.fragment().unwrap(), "fragment1");
178  /// assert_eq!(url.to_string(), "#fragment1");
179  ///
180  /// // Set the fragment without a leading '#'
181  /// url.set_fragment(Some("fragment2")).unwrap();
182  /// assert_eq!(url.fragment().unwrap(), "fragment2");
183  /// assert_eq!(url.to_string(), "#fragment2");
184  /// ```
185  pub fn set_fragment(&mut self, value: Option<&str>) -> Result<(), Error> {
186    self.fragment = value
187      .filter(|s| !s.is_empty())
188      .map(|mut s| {
189        // Ignore leading '#' during validation.
190        s = s.strip_prefix('#').unwrap_or(s);
191        if s.is_empty() || !is_valid_url_segment(s, is_char_fragment) {
192          return Err(Error::InvalidFragment);
193        }
194        Ok(format!("#{s}"))
195      })
196      .transpose()?;
197    Ok(())
198  }
199}
200
201impl Display for RelativeDIDUrl {
202  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
203    f.write_fmt(format_args!(
204      "{}{}{}",
205      self.path.as_deref().unwrap_or_default(),
206      self.query.as_deref().unwrap_or_default(),
207      self.fragment.as_deref().unwrap_or_default()
208    ))
209  }
210}
211
212impl Debug for RelativeDIDUrl {
213  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
214    f.write_fmt(format_args!("{self}"))
215  }
216}
217
218impl PartialEq for RelativeDIDUrl {
219  fn eq(&self, other: &Self) -> bool {
220    self.path.as_deref().unwrap_or_default() == other.path.as_deref().unwrap_or_default()
221      && self.query.as_deref().unwrap_or_default() == other.query.as_deref().unwrap_or_default()
222      && self.fragment.as_deref().unwrap_or_default() == other.fragment.as_deref().unwrap_or_default()
223  }
224}
225
226impl Eq for RelativeDIDUrl {}
227
228impl PartialOrd for RelativeDIDUrl {
229  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
230    Some(self.cmp(other))
231  }
232}
233
234impl Ord for RelativeDIDUrl {
235  fn cmp(&self, other: &Self) -> Ordering {
236    // Compare path, query, then fragment in that order
237    let path_cmp = self
238      .path
239      .as_deref()
240      .unwrap_or_default()
241      .cmp(other.path.as_deref().unwrap_or_default());
242
243    if path_cmp == Ordering::Equal {
244      let query_cmp = self
245        .query
246        .as_deref()
247        .unwrap_or_default()
248        .cmp(other.query.as_deref().unwrap_or_default());
249
250      if query_cmp == Ordering::Equal {
251        return self
252          .fragment
253          .as_deref()
254          .unwrap_or_default()
255          .cmp(other.fragment.as_deref().unwrap_or_default());
256      }
257
258      return query_cmp;
259    }
260
261    path_cmp
262  }
263}
264
265impl Hash for RelativeDIDUrl {
266  fn hash<H: Hasher>(&self, state: &mut H) {
267    self.to_string().hash(state)
268  }
269}
270
271impl DIDUrl {
272  /// Construct a new [`DIDUrl`] with optional [`RelativeDIDUrl`].
273  pub fn new(did: CoreDID, url: Option<RelativeDIDUrl>) -> Self {
274    Self {
275      did,
276      url: url.unwrap_or_default(),
277    }
278  }
279
280  /// Parse a [`DIDUrl`] from a string.
281  pub fn parse(input: impl AsRef<str>) -> Result<Self, Error> {
282    let did_url: BaseDIDUrl = BaseDIDUrl::parse(input)?;
283    Self::from_base_did_url(did_url)
284  }
285
286  fn from_base_did_url(did_url: BaseDIDUrl) -> Result<Self, Error> {
287    // Extract relative DID URL
288    let url: RelativeDIDUrl = {
289      let mut url: RelativeDIDUrl = RelativeDIDUrl::new();
290      url.set_path(Some(did_url.path()))?;
291      url.set_query(did_url.query())?;
292      url.set_fragment(did_url.fragment())?;
293      url
294    };
295
296    // Extract base DID
297    let did: CoreDID = {
298      let mut base_did: BaseDIDUrl = did_url;
299      base_did.set_path("");
300      base_did.set_query(None);
301      base_did.set_fragment(None);
302      CoreDID::try_from(base_did).map_err(|_| Error::Other("invalid DID"))?
303    };
304
305    Ok(Self { did, url })
306  }
307
308  /// Returns the [`did`][CoreDID].
309  pub fn did(&self) -> &CoreDID {
310    &self.did
311  }
312
313  /// Returns the [`RelativeDIDUrl`].
314  pub fn url(&self) -> &RelativeDIDUrl {
315    &self.url
316  }
317
318  /// Sets the [`RelativeDIDUrl`].
319  pub fn set_url(&mut self, url: RelativeDIDUrl) {
320    self.url = url
321  }
322
323  /// Returns the [`DIDUrl`] `fragment` component.
324  ///
325  /// See [`RelativeDIDUrl::fragment`].
326  pub fn fragment(&self) -> Option<&str> {
327    self.url.fragment()
328  }
329
330  /// Sets the `fragment` component of the [`DIDUrl`].
331  ///
332  /// See [`RelativeDIDUrl::set_fragment`].
333  pub fn set_fragment(&mut self, value: Option<&str>) -> Result<(), Error> {
334    self.url.set_fragment(value)
335  }
336
337  /// Returns the [`DIDUrl`] `path` component.
338  ///
339  /// See [`RelativeDIDUrl::path`].
340  pub fn path(&self) -> Option<&str> {
341    self.url.path()
342  }
343
344  /// Sets the `path` component of the [`DIDUrl`].
345  ///
346  /// See [`RelativeDIDUrl::set_path`].
347  pub fn set_path(&mut self, value: Option<&str>) -> Result<(), Error> {
348    self.url.set_path(value)
349  }
350
351  /// Returns the [`DIDUrl`] `query` component.
352  ///
353  /// See [`RelativeDIDUrl::query`].
354  pub fn query(&self) -> Option<&str> {
355    self.url.query()
356  }
357
358  /// Sets the `query` component of the [`DIDUrl`].
359  ///
360  /// See [`RelativeDIDUrl::set_query`].
361  pub fn set_query(&mut self, value: Option<&str>) -> Result<(), Error> {
362    self.url.set_query(value)
363  }
364
365  /// Parses the [`DIDUrl`] query and returns an iterator of (key, value) pairs.
366  ///
367  /// See [`RelativeDIDUrl::query_pairs`].
368  pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> {
369    self.url.query_pairs()
370  }
371
372  /// Append a string representing a `path`, `query`, and/or `fragment`, returning a new [`DIDUrl`].
373  ///
374  /// Must begin with a valid delimiter character: '/', '?', '#'. Overwrites the existing URL
375  /// segment and any following segments in order of path, query, then fragment.
376  ///
377  /// I.e.
378  /// - joining a path will overwrite the path and clear the query and fragment.
379  /// - joining a query will overwrite the query and clear the fragment.
380  /// - joining a fragment will only overwrite the fragment.
381  pub fn join(&self, segment: impl AsRef<str>) -> Result<Self, Error> {
382    let segment: &str = segment.as_ref();
383
384    // Accept only a relative path, query, or fragment to reject altering the method id segment.
385    if !segment.starts_with('/') && !segment.starts_with('?') && !segment.starts_with('#') {
386      return Err(Error::InvalidPath);
387    }
388
389    // Parse DID Url.
390    let base_did_url: BaseDIDUrl = BaseDIDUrl::parse(self.to_string())?.join(segment)?;
391    Self::from_base_did_url(base_did_url)
392  }
393
394  /// Maps a [`DIDUrl`] by applying a function to the [`CoreDID`] part of the [`DIDUrl`], the [`RelativeDIDUrl`]
395  /// components are left untouched.
396  pub fn map<F>(self, f: F) -> DIDUrl
397  where
398    F: FnOnce(CoreDID) -> CoreDID,
399  {
400    DIDUrl {
401      did: f(self.did),
402      url: self.url,
403    }
404  }
405
406  /// Fallible version of [`DIDUrl::map`].
407  pub fn try_map<F, E>(self, f: F) -> Result<DIDUrl, E>
408  where
409    F: FnOnce(CoreDID) -> Result<CoreDID, E>,
410  {
411    Ok(DIDUrl {
412      did: f(self.did)?,
413      url: self.url,
414    })
415  }
416}
417
418impl<D> From<D> for DIDUrl
419where
420  D: Into<CoreDID>,
421{
422  fn from(did: D) -> Self {
423    Self::new(did.into(), None)
424  }
425}
426
427impl FromStr for DIDUrl {
428  type Err = Error;
429
430  fn from_str(string: &str) -> Result<Self, Self::Err> {
431    Self::parse(string)
432  }
433}
434
435impl TryFrom<String> for DIDUrl {
436  type Error = Error;
437
438  fn try_from(other: String) -> Result<Self, Self::Error> {
439    Self::parse(other)
440  }
441}
442
443impl From<DIDUrl> for String {
444  fn from(did_url: DIDUrl) -> Self {
445    did_url.to_string()
446  }
447}
448
449impl From<DIDUrl> for Url {
450  fn from(did_url: DIDUrl) -> Self {
451    Url::parse(did_url.to_string()).expect("a DIDUrl should be a valid Url")
452  }
453}
454
455impl AsRef<CoreDID> for DIDUrl {
456  fn as_ref(&self) -> &CoreDID {
457    &self.did
458  }
459}
460
461impl AsRef<DIDUrl> for DIDUrl {
462  fn as_ref(&self) -> &DIDUrl {
463    self
464  }
465}
466
467impl PartialEq for DIDUrl {
468  fn eq(&self, other: &Self) -> bool {
469    self.did().eq(other.did()) && self.url() == other.url()
470  }
471}
472
473impl Eq for DIDUrl {}
474
475impl PartialOrd for DIDUrl {
476  #[inline]
477  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
478    Some(self.cmp(other))
479  }
480}
481
482impl Ord for DIDUrl {
483  #[inline]
484  fn cmp(&self, other: &Self) -> Ordering {
485    match self.did().cmp(other.did()) {
486      Ordering::Equal => self.url().cmp(other.url()),
487      ord => ord,
488    }
489  }
490}
491
492impl Hash for DIDUrl {
493  fn hash<H: Hasher>(&self, state: &mut H) {
494    self.to_string().hash(state)
495  }
496}
497
498impl Debug for DIDUrl {
499  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
500    f.write_fmt(format_args!("{self}"))
501  }
502}
503
504impl Display for DIDUrl {
505  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
506    f.write_fmt(format_args!("{}{}", self.did.as_str(), self.url))
507  }
508}
509
510impl KeyComparable for DIDUrl {
511  type Key = Self;
512
513  fn key(&self) -> &Self::Key {
514    self
515  }
516}
517
518/// Checks whether a character satisfies DID Url path constraints.
519#[inline(always)]
520#[rustfmt::skip]
521pub(crate) const fn is_char_path(ch: char) -> bool {
522  is_char_method_id(ch) || matches!(ch, '~' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '@' | '/')
523}
524
525/// Checks whether a character satisfies DID Url query constraints.
526#[inline(always)]
527pub(crate) const fn is_char_query(ch: char) -> bool {
528  is_char_path(ch) || ch == '?'
529}
530
531/// Checks whether a character satisfies DID Url fragment constraints.
532#[inline(always)]
533pub(crate) const fn is_char_fragment(ch: char) -> bool {
534  is_char_path(ch) || ch == '?'
535}
536
537pub(crate) fn is_valid_percent_encoded_char(s: &str) -> bool {
538  let mut chars = s.chars();
539  let Some('%') = chars.next() else { return false };
540  s.len() >= 3 && chars.take(2).all(|c| c.is_ascii_hexdigit())
541}
542
543pub(crate) fn is_valid_url_segment<F>(segment: &str, char_predicate: F) -> bool
544where
545  F: Fn(char) -> bool,
546{
547  let mut chars = segment.char_indices();
548  while let Some((i, c)) = chars.next() {
549    if c == '%' {
550      if !is_valid_percent_encoded_char(&segment[i..]) {
551        return false;
552      }
553      // skip the two HEX digits
554      chars.next();
555      chars.next();
556    } else if !char_predicate(c) {
557      return false;
558    }
559  }
560
561  true
562}
563
564#[cfg(test)]
565mod tests {
566  use super::*;
567
568  #[rustfmt::skip]
569  #[test]
570  fn test_did_url_parse_valid() {
571    let did_url = DIDUrl::parse("did:example:1234567890").unwrap();
572    assert_eq!(did_url.to_string(), "did:example:1234567890");
573    assert!(did_url.url().is_empty());
574    assert!(did_url.path().is_none());
575    assert!(did_url.query().is_none());
576    assert!(did_url.fragment().is_none());
577
578    assert_eq!(DIDUrl::parse("did:example:1234567890/path").unwrap().to_string(), "did:example:1234567890/path");
579    assert_eq!(DIDUrl::parse("did:example:1234567890?query").unwrap().to_string(), "did:example:1234567890?query");
580    assert_eq!(DIDUrl::parse("did:example:1234567890#fragment").unwrap().to_string(), "did:example:1234567890#fragment");
581
582    assert_eq!(DIDUrl::parse("did:example:1234567890/path?query").unwrap().to_string(), "did:example:1234567890/path?query");
583    assert_eq!(DIDUrl::parse("did:example:1234567890/path#fragment").unwrap().to_string(), "did:example:1234567890/path#fragment");
584    assert_eq!(DIDUrl::parse("did:example:1234567890?query#fragment").unwrap().to_string(), "did:example:1234567890?query#fragment");
585
586    let did_url = DIDUrl::parse("did:example:1234567890/path?query#fragment").unwrap();
587    assert!(!did_url.url().is_empty());
588    assert_eq!(did_url.to_string(), "did:example:1234567890/path?query#fragment");
589    assert_eq!(did_url.path().unwrap(), "/path");
590    assert_eq!(did_url.query().unwrap(), "query");
591    assert_eq!(did_url.fragment().unwrap(), "fragment");
592  }
593
594  #[rustfmt::skip]
595  #[test]
596  fn test_join_valid() {
597    let did_url = DIDUrl::parse("did:example:1234567890").unwrap();
598    assert_eq!(did_url.join("/path").unwrap().to_string(), "did:example:1234567890/path");
599    assert_eq!(did_url.join("?query").unwrap().to_string(), "did:example:1234567890?query");
600    assert_eq!(did_url.join("#fragment").unwrap().to_string(), "did:example:1234567890#fragment");
601
602    assert_eq!(did_url.join("/path?query").unwrap().to_string(), "did:example:1234567890/path?query");
603    assert_eq!(did_url.join("/path#fragment").unwrap().to_string(), "did:example:1234567890/path#fragment");
604    assert_eq!(did_url.join("?query#fragment").unwrap().to_string(), "did:example:1234567890?query#fragment");
605
606    let did_url = did_url.join("/path?query#fragment").unwrap();
607    assert_eq!(did_url.to_string(), "did:example:1234567890/path?query#fragment");
608    assert_eq!(did_url.path().unwrap(), "/path");
609    assert_eq!(did_url.query().unwrap(), "query");
610    assert_eq!(did_url.fragment().unwrap(), "fragment");
611  }
612
613  #[test]
614  fn test_did_url_invalid() {
615    assert!(DIDUrl::parse("did:example:1234567890/invalid{path}").is_err());
616    assert!(DIDUrl::parse("did:example:1234567890?invalid{query}").is_err());
617    assert!(DIDUrl::parse("did:example:1234567890#invalid{fragment}").is_err());
618
619    let did_url = DIDUrl::parse("did:example:1234567890").unwrap();
620    assert!(did_url.join("noleadingdelimiter").is_err());
621    assert!(did_url.join("/invalid{path}").is_err());
622    assert!(did_url.join("?invalid{query}").is_err());
623    assert!(did_url.join("#invalid{fragment}").is_err());
624  }
625
626  #[test]
627  fn test_did_url_basic_comparisons() {
628    let did_url1 = DIDUrl::parse("did:example:1234567890").unwrap();
629    let did_url1_copy = DIDUrl::parse("did:example:1234567890").unwrap();
630    assert_eq!(did_url1, did_url1_copy);
631
632    let did_url2 = DIDUrl::parse("did:example:0987654321").unwrap();
633    assert_ne!(did_url1, did_url2);
634    assert!(did_url1 > did_url2);
635
636    let did_url3 = DIDUrl::parse("did:fxample:1234567890").unwrap();
637    assert_ne!(did_url1, did_url3);
638    assert!(did_url1 < did_url3);
639
640    let did_url4 = DIDUrl::parse("did:example:1234567890/path").unwrap();
641    assert_ne!(did_url1, did_url4);
642    assert_ne!(did_url1.url(), did_url4.url());
643    assert_eq!(did_url1.did(), did_url4.did());
644    assert!(did_url1 < did_url4);
645
646    let did_url5 = DIDUrl::parse("did:example:1234567890/zero").unwrap();
647    assert_ne!(did_url4, did_url5);
648    assert_ne!(did_url4.url(), did_url5.url());
649    assert_eq!(did_url4.did(), did_url5.did());
650    assert!(did_url4 < did_url5);
651  }
652
653  #[test]
654  fn test_path_valid() {
655    let mut relative_url = RelativeDIDUrl::new();
656
657    // Simple path.
658    assert!(relative_url.set_path(Some("/path")).is_ok());
659    assert_eq!(relative_url.path().unwrap(), "/path");
660    assert!(relative_url.set_path(Some("/path/sub-path/resource")).is_ok());
661    assert_eq!(relative_url.path().unwrap(), "/path/sub-path/resource");
662
663    // Empty path.
664    assert!(relative_url.set_path(Some("")).is_ok());
665    assert!(relative_url.path().is_none());
666    assert!(relative_url.set_path(None).is_ok());
667    assert!(relative_url.path().is_none());
668
669    // Percent encoded path.
670    assert!(relative_url.set_path(Some("/p%AAth")).is_ok());
671    assert_eq!(relative_url.path().unwrap(), "/p%AAth");
672  }
673
674  #[rustfmt::skip]
675  #[test]
676  fn test_path_invalid() {
677    let mut relative_url = RelativeDIDUrl::new();
678
679    // Invalid symbols.
680    assert!(matches!(relative_url.set_path(Some("/white space")), Err(Error::InvalidPath)));
681    assert!(matches!(relative_url.set_path(Some("/white\tspace")), Err(Error::InvalidPath)));
682    assert!(matches!(relative_url.set_path(Some("/white\nspace")), Err(Error::InvalidPath)));
683    assert!(matches!(relative_url.set_path(Some("/path{invalid_brackets}")), Err(Error::InvalidPath)));
684
685    // Missing leading '/'.
686    assert!(matches!(relative_url.set_path(Some("path")), Err(Error::InvalidPath)));
687    assert!(matches!(relative_url.set_path(Some("p/")), Err(Error::InvalidPath)));
688    assert!(matches!(relative_url.set_path(Some("p/ath")), Err(Error::InvalidPath)));
689    assert!(matches!(relative_url.set_path(Some("path/")), Err(Error::InvalidPath)));
690    assert!(matches!(relative_url.set_path(Some("path/sub-path/")), Err(Error::InvalidPath)));
691
692    // Reject query delimiter '?'.
693    assert!(matches!(relative_url.set_path(Some("?query")), Err(Error::InvalidPath)));
694    assert!(matches!(relative_url.set_path(Some("some?query")), Err(Error::InvalidPath)));
695    assert!(matches!(relative_url.set_path(Some("/path?")), Err(Error::InvalidPath)));
696    assert!(matches!(relative_url.set_path(Some("/path?query")), Err(Error::InvalidPath)));
697    assert!(matches!(relative_url.set_path(Some("/path/query?")), Err(Error::InvalidPath)));
698
699    // Reject fragment delimiter '#'.
700    assert!(matches!(relative_url.set_path(Some("#fragment")), Err(Error::InvalidPath)));
701    assert!(matches!(relative_url.set_path(Some("some#fragment")), Err(Error::InvalidPath)));
702    assert!(matches!(relative_url.set_path(Some("/path#")), Err(Error::InvalidPath)));
703    assert!(matches!(relative_url.set_path(Some("/path#fragment")), Err(Error::InvalidPath)));
704    assert!(matches!(relative_url.set_path(Some("/path/fragment#")), Err(Error::InvalidPath)));
705  }
706
707  #[test]
708  fn test_query_valid() {
709    let mut relative_url = RelativeDIDUrl::new();
710
711    // Empty query.
712    assert!(relative_url.set_query(Some("")).is_ok());
713    assert!(relative_url.query().is_none());
714
715    // With leading '?'.
716    assert!(relative_url.set_query(Some("?query")).is_ok());
717    assert_eq!(relative_url.query().unwrap(), "query");
718    assert!(relative_url.set_query(Some("?name=value")).is_ok());
719    assert_eq!(relative_url.query().unwrap(), "name=value");
720    assert!(relative_url.set_query(Some("?name=value&name2=value2")).is_ok());
721    assert_eq!(relative_url.query().unwrap(), "name=value&name2=value2");
722    assert!(relative_url.set_query(Some("?name=value&name2=value2&3=true")).is_ok());
723    assert_eq!(relative_url.query().unwrap(), "name=value&name2=value2&3=true");
724
725    // Without leading '?'.
726    assert!(relative_url.set_query(Some("query")).is_ok());
727    assert_eq!(relative_url.query().unwrap(), "query");
728    assert!(relative_url.set_query(Some("name=value&name2=value2&3=true")).is_ok());
729    assert_eq!(relative_url.query().unwrap(), "name=value&name2=value2&3=true");
730
731    // With percent encoded char.
732    assert!(relative_url.set_query(Some("qu%EEry")).is_ok());
733    assert_eq!(relative_url.query().unwrap(), "qu%EEry");
734  }
735
736  #[rustfmt::skip]
737  #[test]
738  fn test_query_invalid() {
739    let mut relative_url = RelativeDIDUrl::new();
740
741    // Delimiter-only.
742    assert!(matches!(relative_url.set_query(Some("?")), Err(Error::InvalidQuery)));
743
744    // Invalid symbols.
745    assert!(matches!(relative_url.set_query(Some("?white space")), Err(Error::InvalidQuery)));
746    assert!(matches!(relative_url.set_query(Some("?white\tspace")), Err(Error::InvalidQuery)));
747    assert!(matches!(relative_url.set_query(Some("?white\nspace")), Err(Error::InvalidQuery)));
748    assert!(matches!(relative_url.set_query(Some("?query{invalid_brackets}")), Err(Error::InvalidQuery)));
749
750    // Reject fragment delimiter '#'.
751    assert!(matches!(relative_url.set_query(Some("#fragment")), Err(Error::InvalidQuery)));
752    assert!(matches!(relative_url.set_query(Some("some#fragment")), Err(Error::InvalidQuery)));
753    assert!(matches!(relative_url.set_query(Some("?query#fragment")), Err(Error::InvalidQuery)));
754    assert!(matches!(relative_url.set_query(Some("?query=a#fragment")), Err(Error::InvalidQuery)));
755    assert!(matches!(relative_url.set_query(Some("?query=#fragment")), Err(Error::InvalidQuery)));
756    assert!(matches!(relative_url.set_query(Some("?query=frag#ment")), Err(Error::InvalidQuery)));
757    assert!(matches!(relative_url.set_query(Some("?query=fragment#")), Err(Error::InvalidQuery)));
758  }
759
760  #[rustfmt::skip]
761  #[test]
762  fn test_fragment_valid() {
763    let mut relative_url = RelativeDIDUrl::new();
764
765    // With leading '#'.
766    assert!(relative_url.set_fragment(Some("#fragment")).is_ok());
767    assert_eq!(relative_url.fragment().unwrap(), "fragment");
768    assert!(relative_url.set_fragment(Some("#longer_fragment?and/other-delimiters:valid")).is_ok());
769    assert_eq!(relative_url.fragment().unwrap(), "longer_fragment?and/other-delimiters:valid");
770
771    // Without leading '#'.
772    assert!(relative_url.set_fragment(Some("fragment")).is_ok());
773    assert_eq!(relative_url.fragment().unwrap(), "fragment");
774    assert!(relative_url.set_fragment(Some("longer_fragment?and/other-delimiters:valid")).is_ok());
775    assert_eq!(relative_url.fragment().unwrap(), "longer_fragment?and/other-delimiters:valid");
776
777    // Empty fragment.
778    assert!(relative_url.set_fragment(Some("")).is_ok());
779    assert!(relative_url.fragment().is_none());
780    assert!(relative_url.set_fragment(None).is_ok());
781    assert!(relative_url.fragment().is_none());
782
783    // Percent encoded fragment.
784    assert!(relative_url.set_fragment(Some("fr%AAgm%EEnt")).is_ok());
785    assert_eq!(relative_url.fragment().unwrap(), "fr%AAgm%EEnt");
786  }
787
788  #[rustfmt::skip]
789  #[test]
790  fn test_fragment_invalid() {
791    let mut relative_url = RelativeDIDUrl::new();
792
793    // Delimiter only.
794    assert!(matches!(relative_url.set_fragment(Some("#")), Err(Error::InvalidFragment)));
795
796    // Invalid symbols.
797    assert!(matches!(relative_url.set_fragment(Some("#white space")), Err(Error::InvalidFragment)));
798    assert!(matches!(relative_url.set_fragment(Some("#white\tspace")), Err(Error::InvalidFragment)));
799    assert!(matches!(relative_url.set_fragment(Some("#white\nspace")), Err(Error::InvalidFragment)));
800    assert!(matches!(relative_url.set_fragment(Some("#fragment{invalid_brackets}")), Err(Error::InvalidFragment)));
801    assert!(matches!(relative_url.set_fragment(Some("#fragment\"other\"")), Err(Error::InvalidFragment)));
802  }
803
804  proptest::proptest! {
805    #[test]
806    fn test_fuzz_join_no_panic(s in "\\PC*") {
807      let did_url = DIDUrl::parse("did:example:1234567890").unwrap();
808      let _ = did_url.join(s);
809    }
810
811    #[test]
812    fn test_fuzz_path_no_panic(s in "\\PC*") {
813      let mut url = RelativeDIDUrl::new();
814      let _ = url.set_path(Some(&s));
815    }
816
817    #[test]
818    fn test_fuzz_query_no_panic(s in "\\PC*") {
819      let mut url = RelativeDIDUrl::new();
820      let _ = url.set_query(Some(&s));
821    }
822
823    #[test]
824    fn test_fuzz_fragment_no_panic(s in "\\PC*") {
825      let mut url = RelativeDIDUrl::new();
826      let _ = url.set_fragment(Some(&s));
827    }
828  }
829}