Skip to main content

identity_kit/did/
uri.rs

1//! `Uri`: an RFC 3986 URI kept exactly as written.
2
3use core::fmt;
4use core::str::FromStr;
5
6use serde::de::Error as _;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use super::{Did, DidError, DidUrl};
10
11/// A URI (RFC 3986) stored exactly as written: no normalization, no percent-decoding, no case
12/// folding.
13///
14/// DID documents carry URIs in `alsoKnownAs` and `serviceEndpoint`, and DID Core requires a
15/// consumer to hand them on unchanged, so this type checks the generic syntax and nothing more:
16/// a scheme, then characters from the RFC 3986 sets (`unreserved`, `reserved`, `pct-encoded`),
17/// with at most one `#`. Two URIs are equal exactly when their strings are equal.
18///
19/// Every [`Did`] and [`DidUrl`] is a `Uri`; use `From` to convert.
20///
21/// # Examples
22///
23/// ```
24/// use identity_kit::did::Uri;
25///
26/// let uri = Uri::parse("https://bar.example.com")?;
27/// assert_eq!(uri.as_str(), "https://bar.example.com"); // no trailing slash added
28/// assert_eq!(uri.scheme(), "https");
29///
30/// assert!(Uri::parse("not a uri").is_err());
31/// assert!(Uri::parse("/relative/path").is_err());
32/// # Ok::<(), identity_kit::did::DidError>(())
33/// ```
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct Uri(String);
36
37impl Uri {
38    /// Parses a URI, checking the RFC 3986 generic syntax.
39    ///
40    /// # Errors
41    ///
42    /// [`DidError::InvalidChar`] names the first byte that is not allowed in a URI, including a
43    /// missing or malformed scheme; [`DidError::InvalidPercentEncoding`] an incomplete `%xx`.
44    pub fn parse(s: &str) -> Result<Self, DidError> {
45        validate_uri(s)?;
46        Ok(Self(s.to_owned()))
47    }
48
49    /// The URI as written.
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53
54    /// The scheme, without the `:`.
55    pub fn scheme(&self) -> &str {
56        // Construction guarantees a `:` after the scheme.
57        self.0.split(':').next().unwrap_or_default()
58    }
59
60    /// Whether this URI is a DID or a DID URL.
61    pub fn is_did_url(&self) -> bool {
62        self.scheme() == "did"
63    }
64}
65
66/// Checks the RFC 3986 generic syntax: `scheme ":" *( unreserved / reserved / pct-encoded )`
67/// with at most one `#`.
68fn validate_uri(s: &str) -> Result<(), DidError> {
69    let bytes = s.as_bytes();
70    let colon = bytes
71        .iter()
72        .position(|&b| b == b':')
73        .ok_or(DidError::InvalidChar {
74            position: bytes.len(),
75            ch: ':',
76        })?;
77    if colon == 0 || !bytes[0].is_ascii_alphabetic() {
78        return Err(DidError::InvalidChar {
79            position: 0,
80            ch: s.chars().next().unwrap_or(':'),
81        });
82    }
83    if let Some(i) = bytes[..colon]
84        .iter()
85        .position(|&b| !(b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.')))
86    {
87        return Err(DidError::InvalidChar {
88            position: i,
89            ch: char::from(bytes[i]),
90        });
91    }
92    let mut hashes = 0;
93    let mut i = colon + 1;
94    while i < bytes.len() {
95        match bytes[i] {
96            b'%' => {
97                let ok = bytes.len() >= i + 3
98                    && bytes[i + 1].is_ascii_hexdigit()
99                    && bytes[i + 2].is_ascii_hexdigit();
100                if !ok {
101                    return Err(DidError::InvalidPercentEncoding { position: i });
102                }
103                i += 3;
104                continue;
105            }
106            b'#' => {
107                hashes += 1;
108                if hashes > 1 {
109                    return Err(DidError::InvalidChar {
110                        position: i,
111                        ch: '#',
112                    });
113                }
114            }
115            b if is_uri_char(b) => {}
116            _ => {
117                // Report the whole character, not its first UTF-8 byte.
118                let ch = s[i..].chars().next().unwrap_or('\0');
119                return Err(DidError::InvalidChar { position: i, ch });
120            }
121        }
122        i += 1;
123    }
124    Ok(())
125}
126
127/// `unreserved / gen-delims / sub-delims` of RFC 3986 ยง2, less `%` and `#` (handled apart).
128fn is_uri_char(b: u8) -> bool {
129    b.is_ascii_alphanumeric()
130        || matches!(
131            b,
132            b'-' | b'.'
133                | b'_'
134                | b'~'
135                | b':'
136                | b'/'
137                | b'?'
138                | b'['
139                | b']'
140                | b'@'
141                | b'!'
142                | b'$'
143                | b'&'
144                | b'\''
145                | b'('
146                | b')'
147                | b'*'
148                | b'+'
149                | b','
150                | b';'
151                | b'='
152        )
153}
154
155impl fmt::Display for Uri {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.write_str(&self.0)
158    }
159}
160
161impl AsRef<str> for Uri {
162    fn as_ref(&self) -> &str {
163        &self.0
164    }
165}
166
167impl FromStr for Uri {
168    type Err = DidError;
169
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        Self::parse(s)
172    }
173}
174
175impl TryFrom<&str> for Uri {
176    type Error = DidError;
177
178    fn try_from(s: &str) -> Result<Self, Self::Error> {
179        Self::parse(s)
180    }
181}
182
183impl TryFrom<String> for Uri {
184    type Error = DidError;
185
186    fn try_from(s: String) -> Result<Self, Self::Error> {
187        validate_uri(&s)?;
188        Ok(Self(s))
189    }
190}
191
192impl From<Did> for Uri {
193    fn from(did: Did) -> Self {
194        Self(did.as_str().to_owned())
195    }
196}
197
198impl From<DidUrl> for Uri {
199    fn from(url: DidUrl) -> Self {
200        Self(url.as_str().to_owned())
201    }
202}
203
204impl From<&Did> for Uri {
205    fn from(did: &Did) -> Self {
206        Self(did.as_str().to_owned())
207    }
208}
209
210impl From<&DidUrl> for Uri {
211    fn from(url: &DidUrl) -> Self {
212        Self(url.as_str().to_owned())
213    }
214}
215
216impl PartialEq<str> for Uri {
217    fn eq(&self, other: &str) -> bool {
218        self.0 == other
219    }
220}
221
222impl PartialEq<&str> for Uri {
223    fn eq(&self, other: &&str) -> bool {
224        self.0 == *other
225    }
226}
227
228impl Serialize for Uri {
229    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
230        serializer.serialize_str(&self.0)
231    }
232}
233
234impl<'de> Deserialize<'de> for Uri {
235    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
236        let s = String::deserialize(deserializer)?;
237        Self::try_from(s).map_err(D::Error::custom)
238    }
239}