identity_document/utils/
did_url_query.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::borrow::Cow;
5
6use identity_did::CoreDID;
7use identity_did::DIDUrl;
8use identity_did::RelativeDIDUrl;
9use identity_did::DID;
10
11/// Specifies a DIDUrl or fragment to query a service or method in a DID Document.
12#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
13#[repr(transparent)]
14pub struct DIDUrlQuery<'query>(Cow<'query, str>);
15
16impl DIDUrlQuery<'_> {
17  /// Returns whether this query matches the given DIDUrl.
18  pub(crate) fn matches(&self, did_url: &DIDUrl) -> bool {
19    // Ensure the DID matches if included in the query.
20    if let Some(did_str) = self.did_str() {
21      if did_str != did_url.did().as_str() {
22        return false;
23      }
24    }
25
26    // Compare fragments.
27    match self.fragment().zip(did_url.fragment()) {
28      Some((a, b)) => a == b,
29      None => false,
30    }
31  }
32
33  /// Extract the DID portion of the query if it exists.
34  fn did_str(&self) -> Option<&str> {
35    let query: &str = self.0.as_ref();
36    if !query.starts_with(CoreDID::SCHEME) {
37      return None;
38    }
39
40    // Find end of DID section.
41    // did:example:123/path?service=agent&relativeRef=/credentials#degree
42    let mut end_pos: usize = query.len();
43    end_pos = end_pos.min(query.find('?').unwrap_or(end_pos));
44    end_pos = end_pos.min(query.find('/').unwrap_or(end_pos));
45    end_pos = end_pos.min(query.find('#').unwrap_or(end_pos));
46    query.get(0..end_pos)
47  }
48
49  /// Extract the query fragment if it exists.
50  fn fragment(&self) -> Option<&str> {
51    let query: &str = self.0.as_ref();
52    let fragment_maybe: Option<&str> = if query.starts_with(CoreDID::SCHEME) {
53      // Extract the fragment from a full DID-Url-like string.
54      query.rfind('#').and_then(|index| query.get(index + 1..))
55    } else if let Some(fragment_delimiter_index) = query.rfind('#') {
56      // Extract the fragment from a relative DID-Url.
57      query.get(fragment_delimiter_index + 1..)
58    } else {
59      // Assume the entire string is a fragment.
60      Some(query)
61    };
62    fragment_maybe.filter(|fragment| !fragment.is_empty())
63  }
64}
65
66impl<'query> From<&'query str> for DIDUrlQuery<'query> {
67  fn from(other: &'query str) -> Self {
68    Self(Cow::Borrowed(other))
69  }
70}
71
72impl<'query> From<&'query String> for DIDUrlQuery<'query> {
73  fn from(other: &'query String) -> Self {
74    Self(Cow::Borrowed(&**other))
75  }
76}
77
78impl<'query> From<&'query DIDUrl> for DIDUrlQuery<'query> {
79  fn from(other: &'query DIDUrl) -> Self {
80    Self(Cow::Owned(other.to_string()))
81  }
82}
83
84impl From<DIDUrl> for DIDUrlQuery<'_> {
85  fn from(other: DIDUrl) -> Self {
86    Self(Cow::Owned(other.to_string()))
87  }
88}
89
90impl<'query> From<&'query RelativeDIDUrl> for DIDUrlQuery<'query> {
91  fn from(other: &'query RelativeDIDUrl) -> Self {
92    Self(Cow::Owned(other.to_string()))
93  }
94}
95
96#[cfg(test)]
97mod tests {
98  use identity_did::DIDUrl;
99  use std::ops::Not;
100
101  use super::*;
102
103  #[test]
104  fn test_did_str() {
105    assert!(DIDUrlQuery::from("").did_str().is_none());
106    assert!(DIDUrlQuery::from("fragment").did_str().is_none());
107    assert!(DIDUrlQuery::from("#fragment").did_str().is_none());
108    assert!(DIDUrlQuery::from("?query").did_str().is_none());
109    assert!(DIDUrlQuery::from("/path").did_str().is_none());
110    assert!(DIDUrlQuery::from("/path?query#fragment").did_str().is_none());
111    assert!(DIDUrlQuery::from("method:missingscheme123").did_str().is_none());
112    assert!(DIDUrlQuery::from("iota:example").did_str().is_none());
113    assert_eq!(
114      DIDUrlQuery::from("did:iota:example").did_str(),
115      Some("did:iota:example")
116    );
117    assert_eq!(
118      DIDUrlQuery::from("did:iota:example#fragment").did_str(),
119      Some("did:iota:example")
120    );
121    assert_eq!(
122      DIDUrlQuery::from("did:iota:example?query").did_str(),
123      Some("did:iota:example")
124    );
125    assert_eq!(
126      DIDUrlQuery::from("did:iota:example/path").did_str(),
127      Some("did:iota:example")
128    );
129    assert_eq!(
130      DIDUrlQuery::from("did:iota:example/path?query#fragment").did_str(),
131      Some("did:iota:example")
132    );
133    assert_eq!(
134      DIDUrlQuery::from("did:iota:example/path?query&relativeRef=/#fragment").did_str(),
135      Some("did:iota:example")
136    );
137  }
138
139  #[test]
140  fn test_fragment() {
141    assert!(DIDUrlQuery::from("").fragment().is_none());
142    assert_eq!(DIDUrlQuery::from("fragment").fragment(), Some("fragment"));
143    assert_eq!(DIDUrlQuery::from("#fragment").fragment(), Some("fragment"));
144    assert_eq!(DIDUrlQuery::from("/path?query#fragment").fragment(), Some("fragment"));
145    assert!(DIDUrlQuery::from("did:iota:example").fragment().is_none());
146    assert_eq!(
147      DIDUrlQuery::from("did:iota:example#fragment").fragment(),
148      Some("fragment")
149    );
150    assert!(DIDUrlQuery::from("did:iota:example?query").fragment().is_none());
151    assert!(DIDUrlQuery::from("did:iota:example/path").fragment().is_none());
152    assert_eq!(
153      DIDUrlQuery::from("did:iota:example/path?query#fragment").fragment(),
154      Some("fragment")
155    );
156    assert_eq!(
157      DIDUrlQuery::from("did:iota:example/path?query&relativeRef=/#fragment").fragment(),
158      Some("fragment")
159    );
160  }
161
162  #[test]
163  fn test_matches() {
164    let did_base: DIDUrl = DIDUrl::parse("did:iota:example").unwrap();
165    let did_path: DIDUrl = DIDUrl::parse("did:iota:example/path").unwrap();
166    let did_query: DIDUrl = DIDUrl::parse("did:iota:example?query").unwrap();
167    let did_fragment: DIDUrl = DIDUrl::parse("did:iota:example#fragment").unwrap();
168    let did_different_fragment: DIDUrl = DIDUrl::parse("did:iota:example#differentfragment").unwrap();
169    let did_url: DIDUrl = DIDUrl::parse("did:iota:example/path?query#fragment").unwrap();
170    let did_url_complex: DIDUrl = DIDUrl::parse("did:iota:example/path?query&relativeRef=/#fragment").unwrap();
171
172    // INVALID: empty query should not match anything.
173    {
174      let query_empty = DIDUrlQuery::from("");
175      assert!(query_empty.matches(&did_base).not());
176      assert!(query_empty.matches(&did_path).not());
177      assert!(query_empty.matches(&did_query).not());
178      assert!(query_empty.matches(&did_fragment).not());
179      assert!(query_empty.matches(&did_different_fragment).not());
180      assert!(query_empty.matches(&did_url).not());
181      assert!(query_empty.matches(&did_url_complex).not());
182    }
183
184    // VALID: query with only a fragment should match the same fragment.
185    {
186      let query_fragment_only = DIDUrlQuery::from("fragment");
187      assert!(query_fragment_only.matches(&did_base).not());
188      assert!(query_fragment_only.matches(&did_path).not());
189      assert!(query_fragment_only.matches(&did_query).not());
190      assert!(query_fragment_only.matches(&did_fragment));
191      assert!(query_fragment_only.matches(&did_different_fragment).not());
192      assert!(query_fragment_only.matches(&did_url));
193      assert!(query_fragment_only.matches(&did_url_complex));
194    }
195
196    // VALID: query with differentfragment should only match the same fragment.
197    {
198      let query_different_fragment = DIDUrlQuery::from("differentfragment");
199      assert!(query_different_fragment.matches(&did_base).not());
200      assert!(query_different_fragment.matches(&did_path).not());
201      assert!(query_different_fragment.matches(&did_query).not());
202      assert!(query_different_fragment.matches(&did_fragment).not());
203      assert!(query_different_fragment.matches(&did_different_fragment));
204      assert!(query_different_fragment.matches(&did_url).not());
205      assert!(query_different_fragment.matches(&did_url_complex).not());
206    }
207
208    // VALID: query with a #fragment should match the same fragment.
209    {
210      let query_fragment_delimiter = DIDUrlQuery::from("#fragment");
211      assert!(query_fragment_delimiter.matches(&did_base).not());
212      assert!(query_fragment_delimiter.matches(&did_path).not());
213      assert!(query_fragment_delimiter.matches(&did_query).not());
214      assert!(query_fragment_delimiter.matches(&did_fragment));
215      assert!(query_fragment_delimiter.matches(&did_different_fragment).not());
216      assert!(query_fragment_delimiter.matches(&did_url));
217      assert!(query_fragment_delimiter.matches(&did_url_complex));
218    }
219
220    // VALID: query with a relative DID Url should match the same fragment.
221    {
222      let query_relative_did_url = DIDUrlQuery::from("/path?query#fragment");
223      assert!(query_relative_did_url.matches(&did_base).not());
224      assert!(query_relative_did_url.matches(&did_path).not());
225      assert!(query_relative_did_url.matches(&did_query).not());
226      assert!(query_relative_did_url.matches(&did_fragment));
227      assert!(query_relative_did_url.matches(&did_different_fragment).not());
228      assert!(query_relative_did_url.matches(&did_url));
229      assert!(query_relative_did_url.matches(&did_url_complex));
230    }
231
232    // INVALID: query with DID and no fragment should not match anything.
233    {
234      let query_did = DIDUrlQuery::from("did:iota:example");
235      assert!(query_did.matches(&did_base).not());
236      assert!(query_did.matches(&did_path).not());
237      assert!(query_did.matches(&did_query).not());
238      assert!(query_did.matches(&did_fragment).not());
239      assert!(query_did.matches(&did_different_fragment).not());
240      assert!(query_did.matches(&did_url).not());
241      assert!(query_did.matches(&did_url_complex).not());
242    }
243
244    // VALID: query with a DID fragment should match the same fragment.
245    {
246      let query_did_fragment = DIDUrlQuery::from("did:iota:example#fragment");
247      assert!(query_did_fragment.matches(&did_base).not());
248      assert!(query_did_fragment.matches(&did_path).not());
249      assert!(query_did_fragment.matches(&did_query).not());
250      assert!(query_did_fragment.matches(&did_fragment));
251      assert!(query_did_fragment.matches(&did_different_fragment).not());
252      assert!(query_did_fragment.matches(&did_url));
253      assert!(query_did_fragment.matches(&did_url_complex));
254    }
255
256    // VALID: query with a DID Url with a fragment should match the same fragment.
257    {
258      let query_did_fragment = DIDUrlQuery::from("did:iota:example/path?query#fragment");
259      assert!(query_did_fragment.matches(&did_base).not());
260      assert!(query_did_fragment.matches(&did_path).not());
261      assert!(query_did_fragment.matches(&did_query).not());
262      assert!(query_did_fragment.matches(&did_fragment));
263      assert!(query_did_fragment.matches(&did_different_fragment).not());
264      assert!(query_did_fragment.matches(&did_url));
265      assert!(query_did_fragment.matches(&did_url_complex));
266    }
267
268    // VALID: query with a complex DID Url with a fragment should match the same fragment.
269    {
270      let query_did_fragment = DIDUrlQuery::from("did:iota:example/path?query&relativeRef=/#fragment");
271      assert!(query_did_fragment.matches(&did_base).not());
272      assert!(query_did_fragment.matches(&did_path).not());
273      assert!(query_did_fragment.matches(&did_query).not());
274      assert!(query_did_fragment.matches(&did_fragment));
275      assert!(query_did_fragment.matches(&did_different_fragment).not());
276      assert!(query_did_fragment.matches(&did_url));
277      assert!(query_did_fragment.matches(&did_url_complex));
278    }
279  }
280}