ssi_dids_core/did/url/
relative.rs

1use core::fmt;
2use std::ops::Deref;
3
4use serde::{Deserialize, Serialize};
5
6use crate::{DIDURLBuf, DID};
7
8use super::{Fragment, Query, Unexpected};
9
10/// Error raised when a conversion to a relative DID URL fails.
11#[derive(Debug, thiserror::Error)]
12#[error("invalid relative DID URL `{0}`: {1}")]
13pub struct InvalidRelativeDIDURL<T>(pub T, pub Unexpected);
14
15impl<T> InvalidRelativeDIDURL<T> {
16    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> InvalidRelativeDIDURL<U> {
17        InvalidRelativeDIDURL(f(self.0), self.1)
18    }
19}
20
21/// Relative DID URL.
22#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[repr(transparent)]
24pub struct RelativeDIDURL([u8]);
25
26impl RelativeDIDURL {
27    /// Converts the input `data` to a relative DID URL.
28    ///
29    /// Fails if the data is not a relative DID URL according to the
30    /// [DID Syntax](https://w3c.github.io/did-core/#did-url-syntax).
31    pub fn new(data: &[u8]) -> Result<&Self, InvalidRelativeDIDURL<&[u8]>> {
32        match Self::validate(data) {
33            Ok(()) => Ok(unsafe {
34                // SAFETY: DID is a transparent wrapper over `[u8]`,
35                //         and we just checked that `data` is a relative DID
36                //         URL.
37                std::mem::transmute::<&[u8], &Self>(data)
38            }),
39            Err(e) => Err(InvalidRelativeDIDURL(data, e)),
40        }
41    }
42
43    /// Converts the input `data` to a relative DID URL without validation.
44    ///
45    /// # Safety
46    ///
47    /// The input `data` must be a relative DID URL according to the
48    /// [DID Syntax](https://w3c.github.io/did-core/#did-url-syntax).
49    pub unsafe fn new_unchecked(data: &[u8]) -> &Self {
50        // SAFETY: DID URL is a transparent wrapper over `[u8]`,
51        //         but we didn't check if it is actually a relative DID URL.
52        std::mem::transmute::<&[u8], &Self>(data)
53    }
54
55    /// Returns the relative DID URL as a string.
56    pub fn as_str(&self) -> &str {
57        unsafe {
58            // SAFETY: a relative DID URL is a valid ASCII string.
59            std::str::from_utf8_unchecked(&self.0)
60        }
61    }
62
63    /// Returns the relative DID URL as a byte string.
64    pub fn as_bytes(&self) -> &[u8] {
65        &self.0
66    }
67
68    fn query_delimiter_offset(&self) -> usize {
69        self.0
70            .iter()
71            .position(|&b| matches!(b, b'?' | b'#'))
72            .unwrap_or(self.0.len())
73    }
74
75    fn fragment_delimiter_offset(&self) -> usize {
76        self.0
77            .iter()
78            .position(|&b| matches!(b, b'#'))
79            .unwrap_or(self.0.len())
80    }
81
82    fn fragment_delimiter_offset_from(&self, offset: usize) -> usize {
83        self.0[offset..]
84            .iter()
85            .position(|&b| matches!(b, b'#'))
86            .map(|o| o + offset)
87            .unwrap_or(self.0.len())
88    }
89
90    pub fn path(&self) -> &RelativePath {
91        let end = self.query_delimiter_offset();
92        unsafe { RelativePath::new_unchecked(&self.0[..end]) }
93    }
94
95    pub fn query(&self) -> Option<&Query> {
96        let start = self.query_delimiter_offset();
97        let end = self.fragment_delimiter_offset_from(start);
98        if start == end {
99            None
100        } else {
101            Some(unsafe { Query::new_unchecked(&self.0[(start + 1)..end]) })
102        }
103    }
104
105    pub fn fragment(&self) -> Option<&Fragment> {
106        let start = self.fragment_delimiter_offset();
107        let end = self.fragment_delimiter_offset_from(start);
108        if start == end {
109            None
110        } else {
111            Some(unsafe { Fragment::new_unchecked(&self.0[(start + 1)..end]) })
112        }
113    }
114
115    // /// Convert a DID URL to a absolute DID URL, given a DID as base URI,
116    // /// according to [DID Core - Relative DID URLs](https://w3c.github.io/did-core/#relative-did-urls).
117    // pub fn to_absolute(&self, base_did: &DID) -> DIDURLBuf {
118    //     // // TODO: support [Reference Resolution](https://tools.ietf.org/html/rfc3986#section-5) more
119    //     // // generally, e.g. when base is not a DID
120    //     // DIDURL {
121    //     //     did: base_did.to_string(),
122    //     //     path_abempty: self.path.to_string(),
123    //     //     query: self.query.as_ref().cloned(),
124    //     //     fragment: self.fragment.as_ref().cloned(),
125    //     // }
126    // 	todo!()
127    // }
128
129    pub fn resolve(&self, base_id: &DID) -> DIDURLBuf {
130        let mut bytes = base_id.as_bytes().to_vec();
131        bytes.extend_from_slice(&self.0);
132        unsafe { DIDURLBuf::new_unchecked(bytes) }
133    }
134}
135
136/// DID URL path.
137#[repr(transparent)]
138pub struct RelativePath([u8]);
139
140impl RelativePath {
141    /// Creates a relative DID URL path from the given data without validation.
142    ///
143    /// # Safety
144    ///
145    /// The input data must be a valid relative DID URL path.
146    pub unsafe fn new_unchecked(data: &[u8]) -> &Self {
147        std::mem::transmute(data)
148    }
149
150    /// Returns the relative DID URL path as a string.
151    pub fn as_str(&self) -> &str {
152        unsafe {
153            // SAFETY: a relative DID URL path is a valid ASCII string.
154            std::str::from_utf8_unchecked(&self.0)
155        }
156    }
157
158    /// Returns the relative DID URL path as a byte string.
159    pub fn as_bytes(&self) -> &[u8] {
160        &self.0
161    }
162}
163
164#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
165pub struct RelativeDIDURLBuf(Vec<u8>);
166
167impl RelativeDIDURLBuf {
168    pub fn new(data: Vec<u8>) -> Result<Self, InvalidRelativeDIDURL<Vec<u8>>> {
169        match RelativeDIDURL::validate(&data) {
170            Ok(()) => Ok(Self(data)),
171            Err(e) => Err(InvalidRelativeDIDURL(data, e)),
172        }
173    }
174
175    pub fn as_relative_did_url(&self) -> &RelativeDIDURL {
176        unsafe { RelativeDIDURL::new_unchecked(&self.0) }
177    }
178}
179
180impl TryFrom<String> for RelativeDIDURLBuf {
181    type Error = InvalidRelativeDIDURL<String>;
182
183    fn try_from(value: String) -> Result<Self, Self::Error> {
184        RelativeDIDURLBuf::new(value.into_bytes()).map_err(|e| {
185            e.map(|bytes| unsafe {
186                // SAFETY: `bytes` comes from the `value` string, which is UTF-8
187                //         encoded by definition.
188                String::from_utf8_unchecked(bytes)
189            })
190        })
191    }
192}
193
194impl Deref for RelativeDIDURLBuf {
195    type Target = RelativeDIDURL;
196
197    fn deref(&self) -> &Self::Target {
198        self.as_relative_did_url()
199    }
200}
201
202impl fmt::Display for RelativeDIDURLBuf {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        self.as_str().fmt(f)
205    }
206}
207
208impl fmt::Debug for RelativeDIDURLBuf {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        self.as_str().fmt(f)
211    }
212}
213
214impl Serialize for RelativeDIDURLBuf {
215    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
216    where
217        S: serde::Serializer,
218    {
219        self.as_str().serialize(serializer)
220    }
221}
222
223impl<'de> Deserialize<'de> for RelativeDIDURLBuf {
224    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225    where
226        D: serde::Deserializer<'de>,
227    {
228        struct Visitor;
229
230        impl serde::de::Visitor<'_> for Visitor {
231            type Value = RelativeDIDURLBuf;
232
233            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
234                write!(f, "a DID URL")
235            }
236
237            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
238            where
239                E: serde::de::Error,
240            {
241                v.try_into().map_err(|e| E::custom(e))
242            }
243
244            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
245            where
246                E: serde::de::Error,
247            {
248                self.visit_string(v.to_string())
249            }
250        }
251
252        deserializer.deserialize_string(Visitor)
253    }
254}
255
256impl RelativeDIDURL {
257    /// Validates a DID URL string.
258    fn validate(data: &[u8]) -> Result<(), Unexpected> {
259        let mut bytes = data.iter().copied();
260        match Self::validate_from(0, &mut bytes)? {
261            (_, None) => Ok(()),
262            (i, Some(c)) => Err(Unexpected(i, Some(c))),
263        }
264    }
265
266    /// Validates a DID URL string.
267    fn validate_from(
268        mut i: usize,
269        bytes: &mut impl Iterator<Item = u8>,
270    ) -> Result<(usize, Option<u8>), Unexpected> {
271        enum State {
272            Path,
273            PathSegment,
274            PathSegmentNc,
275            PathSegmentNzNc,
276            Query,
277            Fragment,
278            Pct1(Part),
279            Pct2(Part),
280        }
281
282        enum Part {
283            PathSegment,
284            PathSegmentNc,
285            Query,
286            Fragment,
287        }
288
289        impl Part {
290            pub fn state(&self) -> State {
291                match self {
292                    Self::PathSegment => State::PathSegment,
293                    Self::PathSegmentNc => State::PathSegmentNc,
294                    Self::Query => State::Query,
295                    Self::Fragment => State::Fragment,
296                }
297            }
298        }
299
300        fn is_unreserved(b: u8) -> bool {
301            b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~')
302        }
303
304        fn is_sub_delims(b: u8) -> bool {
305            matches!(
306                b,
307                b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
308            )
309        }
310
311        fn is_pchar(b: u8) -> bool {
312            is_unreserved(b) || is_sub_delims(b) || matches!(b, b':' | b'@')
313        }
314
315        let mut state = State::Path;
316
317        loop {
318            match state {
319                State::Path => match bytes.next() {
320                    Some(b'/') => state = State::PathSegmentNzNc, // absolute path
321                    Some(b'?') => state = State::Query,           // path-empty
322                    Some(b'#') => state = State::Fragment,        // path-empty
323                    Some(b'%') => state = State::Pct1(Part::PathSegmentNc), // path-noscheme
324                    Some(b':') => break Ok((i, Some(b':'))),
325                    Some(c) if is_pchar(c) => (), // path-noscheme
326                    c => break Ok((i, c)),        // path-empty
327                },
328                State::PathSegment => match bytes.next() {
329                    Some(b'/') => (), // next segment.
330                    Some(b'?') => state = State::Query,
331                    Some(b'#') => state = State::Fragment,
332                    Some(b'%') => state = State::Pct1(Part::PathSegment),
333                    Some(c) if is_pchar(c) => (),
334                    c => break Ok((i, c)),
335                },
336                State::PathSegmentNc => match bytes.next() {
337                    Some(b'/') => state = State::PathSegment,
338                    Some(b'?') => state = State::Query,
339                    Some(b'#') => state = State::Fragment,
340                    Some(b'%') => state = State::Pct1(Part::PathSegmentNc),
341                    Some(b':') => break Ok((i, Some(b':'))),
342                    Some(c) if is_pchar(c) => (),
343                    c => break Ok((i, c)),
344                },
345                State::PathSegmentNzNc => match bytes.next() {
346                    Some(b'?') => state = State::Query,
347                    Some(b'#') => state = State::Fragment,
348                    Some(b'%') => state = State::Pct1(Part::PathSegmentNc),
349                    Some(b':') => break Ok((i, Some(b':'))),
350                    Some(c) if is_pchar(c) => state = State::PathSegmentNc,
351                    c => break Ok((i, c)),
352                },
353                State::Query => match bytes.next() {
354                    Some(b'#') => state = State::Fragment,
355                    Some(b'%') => state = State::Pct1(Part::Query),
356                    Some(c) if is_pchar(c) || matches!(c, b'/' | b'?') => (),
357                    c => break Ok((i, c)),
358                },
359                State::Fragment => match bytes.next() {
360                    Some(b'%') => state = State::Pct1(Part::Fragment),
361                    Some(c) if is_pchar(c) || matches!(c, b'/' | b'?' | b'#') => (),
362                    c => break Ok((i, c)),
363                },
364                State::Pct1(q) => match bytes.next() {
365                    Some(c) if c.is_ascii_hexdigit() => state = State::Pct2(q),
366                    c => break Err(Unexpected(i, c)),
367                },
368                State::Pct2(q) => match bytes.next() {
369                    Some(c) if c.is_ascii_hexdigit() => state = q.state(),
370                    c => break Err(Unexpected(i, c)),
371                },
372            }
373
374            i += 1
375        }
376    }
377}