ssi_dids_core/did/
url.rs

1use core::fmt;
2use std::{borrow::Borrow, ops::Deref, str::FromStr};
3
4use crate::DIDBuf;
5
6use super::{Unexpected, DID};
7
8mod primary;
9mod reference;
10mod relative;
11
12use iref::{Iri, IriBuf, Uri, UriBuf};
13pub use primary::*;
14pub use reference::*;
15pub use relative::*;
16use serde::{Deserialize, Serialize};
17
18/// Error raised when a conversion to a DID URL fails.
19#[derive(Debug, thiserror::Error)]
20#[error("invalid DID URL `{0}`: {1}")]
21pub struct InvalidDIDURL<T>(pub T, pub Unexpected);
22
23impl<T> InvalidDIDURL<T> {
24    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> InvalidDIDURL<U> {
25        InvalidDIDURL(f(self.0), self.1)
26    }
27}
28
29/// DID URL.
30#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
31#[repr(transparent)]
32pub struct DIDURL([u8]);
33
34impl DIDURL {
35    /// Converts the input `data` to a DID URL.
36    ///
37    /// Fails if the data is not a DID URL according to the
38    /// [DID Syntax](https://w3c.github.io/did-core/#did-url-syntax).
39    pub fn new<T: ?Sized + AsRef<[u8]>>(did_url: &T) -> Result<&Self, InvalidDIDURL<&T>> {
40        let data = did_url.as_ref();
41        match Self::validate(data) {
42            Ok(()) => Ok(unsafe {
43                // SAFETY: DID is a transparent wrapper over `[u8]`,
44                //         and we just checked that `data` is a DID URL.
45                std::mem::transmute::<&[u8], &Self>(data)
46            }),
47            Err(e) => Err(InvalidDIDURL(did_url, e)),
48        }
49    }
50
51    /// Converts the input `data` to a DID URL without validation.
52    ///
53    /// # Safety
54    ///
55    /// The input `data` must be a DID URL according to the
56    /// [DID Syntax](https://w3c.github.io/did-core/#did-url-syntax).
57    pub unsafe fn new_unchecked(data: &[u8]) -> &Self {
58        // SAFETY: DID URL is a transparent wrapper over `[u8]`,
59        //         but we didn't check if it is actually a DID URL.
60        std::mem::transmute::<&[u8], &Self>(data)
61    }
62
63    pub fn as_iri(&self) -> &Iri {
64        unsafe {
65            // SAFETY: a DID URL is an IRI.
66            Iri::new_unchecked(self.as_str())
67        }
68    }
69
70    pub fn as_uri(&self) -> &Uri {
71        unsafe {
72            // SAFETY: a DID URL is an URI.
73            Uri::new_unchecked(&self.0)
74        }
75    }
76
77    /// Returns the DID URL as a string.
78    pub fn as_str(&self) -> &str {
79        unsafe {
80            // SAFETY: a DID URL is a valid ASCII string.
81            std::str::from_utf8_unchecked(&self.0)
82        }
83    }
84
85    /// Returns the DID URL as a byte string.
86    pub fn as_bytes(&self) -> &[u8] {
87        &self.0
88    }
89
90    fn path_offset(&self) -> usize {
91        self.0
92            .iter()
93            .position(|&b| matches!(b, b'/' | b'?' | b'#'))
94            .unwrap_or(self.0.len())
95    }
96
97    fn query_delimiter_offset(&self) -> usize {
98        self.0
99            .iter()
100            .position(|&b| matches!(b, b'?' | b'#'))
101            .unwrap_or(self.0.len())
102    }
103
104    fn query_delimiter_offset_from(&self, offset: usize) -> usize {
105        self.0[offset..]
106            .iter()
107            .position(|&b| matches!(b, b'?' | b'#'))
108            .map(|o| o + offset)
109            .unwrap_or(self.0.len())
110    }
111
112    fn fragment_delimiter_offset(&self) -> usize {
113        self.0
114            .iter()
115            .position(|&b| matches!(b, b'#'))
116            .unwrap_or(self.0.len())
117    }
118
119    fn fragment_delimiter_offset_from(&self, offset: usize) -> usize {
120        self.0[offset..]
121            .iter()
122            .position(|&b| matches!(b, b'#'))
123            .map(|o| o + offset)
124            .unwrap_or(self.0.len())
125    }
126
127    pub fn did(&self) -> &DID {
128        unsafe { DID::new_unchecked(&self.0[..self.path_offset()]) }
129    }
130
131    pub fn path(&self) -> &Path {
132        let start = self.path_offset();
133        let end = self.query_delimiter_offset_from(start);
134        unsafe { Path::new_unchecked(&self.0[start..end]) }
135    }
136
137    pub fn query(&self) -> Option<&Query> {
138        let start = self.query_delimiter_offset();
139        let end = self.fragment_delimiter_offset_from(start);
140        if start == end {
141            None
142        } else {
143            Some(unsafe { Query::new_unchecked(&self.0[(start + 1)..end]) })
144        }
145    }
146
147    pub fn fragment(&self) -> Option<&Fragment> {
148        let start = self.fragment_delimiter_offset();
149        let end = self.0.len();
150        if start == end {
151            None
152        } else {
153            Some(unsafe { Fragment::new_unchecked(&self.0[(start + 1)..end]) })
154        }
155    }
156
157    /// Convert a DID URL to a relative DID URL, given a base DID.
158    pub fn to_relative(&self, base_did: &DID) -> Option<&RelativeDIDURL> {
159        if self.did() != base_did {
160            None
161        } else {
162            let offset = self.path_offset();
163            Some(unsafe { RelativeDIDURL::new_unchecked(&self.0[offset..]) })
164        }
165    }
166
167    /// Convert to a fragment-less DID URL and return the removed fragment.
168    ///
169    /// The DID URL can be reconstructed using [PrimaryDIDURLBuf::with_fragment].
170    pub fn without_fragment(&self) -> (&PrimaryDIDURL, Option<&Fragment>) {
171        let fragment_start = self.fragment_delimiter_offset();
172        let fragment_end = self.0.len();
173
174        let fragment = if fragment_start == fragment_end {
175            None
176        } else {
177            Some(unsafe { Fragment::new_unchecked(&self.0[(fragment_start + 1)..fragment_end]) })
178        };
179
180        unsafe {
181            (
182                PrimaryDIDURL::new_unchecked(&self.0[..fragment_start]),
183                fragment,
184            )
185        }
186    }
187}
188
189impl PartialEq<DIDURLBuf> for DIDURL {
190    fn eq(&self, other: &DIDURLBuf) -> bool {
191        self == other.as_did_url()
192    }
193}
194
195impl PartialEq<DID> for DIDURL {
196    fn eq(&self, other: &DID) -> bool {
197        self.path().is_empty()
198            && self.query().is_none()
199            && self.fragment().is_none()
200            && self.did() == other
201    }
202}
203
204impl PartialEq<DIDBuf> for DIDURL {
205    fn eq(&self, other: &DIDBuf) -> bool {
206        self == other.as_did()
207    }
208}
209
210impl ToOwned for DIDURL {
211    type Owned = DIDURLBuf;
212
213    fn to_owned(&self) -> Self::Owned {
214        unsafe { DIDURLBuf::new_unchecked(self.0.to_vec()) }
215    }
216}
217
218impl Deref for DIDURL {
219    type Target = str;
220
221    fn deref(&self) -> &Self::Target {
222        self.as_str()
223    }
224}
225
226impl Borrow<Uri> for DIDURL {
227    fn borrow(&self) -> &Uri {
228        self.as_uri()
229    }
230}
231
232impl Borrow<Iri> for DIDURL {
233    fn borrow(&self) -> &Iri {
234        self.as_iri()
235    }
236}
237
238/// DID URL path.
239#[repr(transparent)]
240pub struct Path([u8]);
241
242impl Path {
243    /// Creates a new DID URL path from the given data without validation.
244    ///
245    /// # Safety
246    ///
247    /// The input data must be a valid DID URL path.
248    pub unsafe fn new_unchecked(data: &[u8]) -> &Self {
249        std::mem::transmute(data)
250    }
251
252    /// Returns the DID URL as a string.
253    pub fn as_str(&self) -> &str {
254        unsafe {
255            // SAFETY: a DID URL is a valid ASCII string.
256            std::str::from_utf8_unchecked(&self.0)
257        }
258    }
259
260    /// Returns the DID URL as a byte string.
261    pub fn as_bytes(&self) -> &[u8] {
262        &self.0
263    }
264}
265
266impl Deref for Path {
267    type Target = str;
268
269    fn deref(&self) -> &Self::Target {
270        self.as_str()
271    }
272}
273
274/// DID URL query.
275#[repr(transparent)]
276pub struct Query([u8]);
277
278impl Query {
279    /// Creates a new DID URL query from the given data without validation.
280    ///
281    /// # Safety
282    ///
283    /// The input data must be a valid DID URL query.
284    pub unsafe fn new_unchecked(data: &[u8]) -> &Self {
285        std::mem::transmute(data)
286    }
287
288    /// Returns the DID URL as a string.
289    pub fn as_str(&self) -> &str {
290        unsafe {
291            // SAFETY: a DID URL is a valid ASCII string.
292            std::str::from_utf8_unchecked(&self.0)
293        }
294    }
295
296    /// Returns the DID URL as a byte string.
297    pub fn as_bytes(&self) -> &[u8] {
298        &self.0
299    }
300}
301
302impl Deref for Query {
303    type Target = str;
304
305    fn deref(&self) -> &Self::Target {
306        self.as_str()
307    }
308}
309
310#[repr(transparent)]
311pub struct Fragment([u8]);
312
313impl Fragment {
314    /// Creates a new DID URL fragment from the given data without validation.
315    ///
316    /// # Safety
317    ///
318    /// The input data must be a valid DID URL fragment.
319    pub unsafe fn new_unchecked(data: &[u8]) -> &Self {
320        std::mem::transmute(data)
321    }
322
323    /// Returns the DID URL as a string.
324    pub fn as_str(&self) -> &str {
325        unsafe {
326            // SAFETY: a DID URL is a valid ASCII string.
327            std::str::from_utf8_unchecked(&self.0)
328        }
329    }
330
331    /// Returns the DID URL as a byte string.
332    pub fn as_bytes(&self) -> &[u8] {
333        &self.0
334    }
335}
336
337impl Deref for Fragment {
338    type Target = str;
339
340    fn deref(&self) -> &Self::Target {
341        self.as_str()
342    }
343}
344
345impl ToOwned for Fragment {
346    type Owned = FragmentBuf;
347
348    fn to_owned(&self) -> Self::Owned {
349        unsafe { FragmentBuf::new_unchecked(self.0.to_vec()) }
350    }
351}
352
353pub struct FragmentBuf(Vec<u8>);
354
355impl FragmentBuf {
356    /// Creates a new DID URL fragment from the given data without validation.
357    ///
358    /// # Safety
359    ///
360    /// The input data must be a valid DID URL fragment.
361    pub unsafe fn new_unchecked(data: Vec<u8>) -> Self {
362        Self(data)
363    }
364
365    pub fn as_fragment(&self) -> &Fragment {
366        unsafe { Fragment::new_unchecked(&self.0) }
367    }
368}
369
370impl Deref for FragmentBuf {
371    type Target = Fragment;
372
373    fn deref(&self) -> &Self::Target {
374        self.as_fragment()
375    }
376}
377
378impl Borrow<Fragment> for FragmentBuf {
379    fn borrow(&self) -> &Fragment {
380        self.as_fragment()
381    }
382}
383
384#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
385pub struct DIDURLBuf(Vec<u8>);
386
387impl DIDURLBuf {
388    pub fn new(data: Vec<u8>) -> Result<Self, InvalidDIDURL<Vec<u8>>> {
389        match DIDURL::validate(&data) {
390            Ok(()) => Ok(Self(data)),
391            Err(e) => Err(InvalidDIDURL(data, e)),
392        }
393    }
394
395    pub fn from_string(data: String) -> Result<Self, InvalidDIDURL<String>> {
396        Self::new(data.into_bytes()).map_err(|InvalidDIDURL(bytes, e)| {
397            InvalidDIDURL(unsafe { String::from_utf8_unchecked(bytes) }, e)
398        })
399    }
400
401    /// Creates a new DID URL from the given data without validation.
402    ///
403    /// # Safety
404    ///
405    /// The input data must be a valid DID URL.
406    pub unsafe fn new_unchecked(data: Vec<u8>) -> Self {
407        Self(data)
408    }
409
410    pub fn as_did_url(&self) -> &DIDURL {
411        unsafe { DIDURL::new_unchecked(&self.0) }
412    }
413
414    pub fn into_iri(self) -> IriBuf {
415        unsafe { IriBuf::new_unchecked(String::from_utf8_unchecked(self.0)) }
416    }
417
418    pub fn into_uri(self) -> UriBuf {
419        unsafe { UriBuf::new_unchecked(self.0) }
420    }
421
422    pub fn into_string(self) -> String {
423        unsafe { String::from_utf8_unchecked(self.0) }
424    }
425}
426
427impl FromStr for DIDURLBuf {
428    type Err = InvalidDIDURL<String>;
429
430    fn from_str(s: &str) -> Result<Self, Self::Err> {
431        s.to_owned().try_into()
432    }
433}
434
435impl TryFrom<String> for DIDURLBuf {
436    type Error = InvalidDIDURL<String>;
437
438    fn try_from(value: String) -> Result<Self, Self::Error> {
439        DIDURLBuf::new(value.into_bytes()).map_err(|e| {
440            e.map(|bytes| unsafe {
441                // SAFETY: `bytes` comes from the `value` string, which is UTF-8
442                //         encoded by definition.
443                String::from_utf8_unchecked(bytes)
444            })
445        })
446    }
447}
448
449impl From<DIDURLBuf> for UriBuf {
450    fn from(value: DIDURLBuf) -> Self {
451        unsafe { UriBuf::new_unchecked(value.0) }
452    }
453}
454
455impl From<DIDURLBuf> for IriBuf {
456    fn from(value: DIDURLBuf) -> Self {
457        unsafe { IriBuf::new_unchecked(String::from_utf8_unchecked(value.0)) }
458    }
459}
460
461impl From<DIDURLBuf> for String {
462    fn from(value: DIDURLBuf) -> Self {
463        value.into_string()
464    }
465}
466
467impl Deref for DIDURLBuf {
468    type Target = DIDURL;
469
470    fn deref(&self) -> &Self::Target {
471        self.as_did_url()
472    }
473}
474
475impl Borrow<DIDURL> for DIDURLBuf {
476    fn borrow(&self) -> &DIDURL {
477        self.as_did_url()
478    }
479}
480
481impl Borrow<Uri> for DIDURLBuf {
482    fn borrow(&self) -> &Uri {
483        self.as_uri()
484    }
485}
486
487impl Borrow<Iri> for DIDURLBuf {
488    fn borrow(&self) -> &Iri {
489        self.as_iri()
490    }
491}
492
493impl AsRef<[u8]> for DIDURLBuf {
494    fn as_ref(&self) -> &[u8] {
495        self.as_bytes()
496    }
497}
498
499impl fmt::Display for DIDURLBuf {
500    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501        self.as_str().fmt(f)
502    }
503}
504
505impl fmt::Debug for DIDURLBuf {
506    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
507        self.as_str().fmt(f)
508    }
509}
510
511impl PartialEq<str> for DIDURLBuf {
512    fn eq(&self, other: &str) -> bool {
513        self.as_str() == other
514    }
515}
516
517impl<'a> PartialEq<&'a str> for DIDURLBuf {
518    fn eq(&self, other: &&'a str) -> bool {
519        self.as_str() == *other
520    }
521}
522
523impl PartialEq<String> for DIDURLBuf {
524    fn eq(&self, other: &String) -> bool {
525        self.as_str() == *other
526    }
527}
528
529impl PartialEq<DIDURL> for DIDURLBuf {
530    fn eq(&self, other: &DIDURL) -> bool {
531        self.as_did_url() == other
532    }
533}
534
535impl<'a> PartialEq<&'a DIDURL> for DIDURLBuf {
536    fn eq(&self, other: &&'a DIDURL) -> bool {
537        self.as_did_url() == *other
538    }
539}
540
541impl PartialEq<DID> for DIDURLBuf {
542    fn eq(&self, other: &DID) -> bool {
543        self.as_did_url() == other
544    }
545}
546
547impl<'a> PartialEq<&'a DID> for DIDURLBuf {
548    fn eq(&self, other: &&'a DID) -> bool {
549        self.as_did_url() == *other
550    }
551}
552
553impl PartialEq<DIDBuf> for DIDURLBuf {
554    fn eq(&self, other: &DIDBuf) -> bool {
555        self.as_did_url() == other
556    }
557}
558
559impl Serialize for DIDURLBuf {
560    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
561    where
562        S: serde::Serializer,
563    {
564        self.as_str().serialize(serializer)
565    }
566}
567
568impl<'de> Deserialize<'de> for DIDURLBuf {
569    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
570    where
571        D: serde::Deserializer<'de>,
572    {
573        struct Visitor;
574
575        impl serde::de::Visitor<'_> for Visitor {
576            type Value = DIDURLBuf;
577
578            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
579                write!(f, "a relative DID URL")
580            }
581
582            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
583            where
584                E: serde::de::Error,
585            {
586                v.try_into().map_err(|e| E::custom(e))
587            }
588
589            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
590            where
591                E: serde::de::Error,
592            {
593                self.visit_string(v.to_string())
594            }
595        }
596
597        deserializer.deserialize_string(Visitor)
598    }
599}
600
601impl DIDURL {
602    /// Validates a DID URL string.
603    fn validate(data: &[u8]) -> Result<(), Unexpected> {
604        let mut bytes = data.iter().copied();
605        match Self::validate_from(0, &mut bytes)? {
606            (_, None) => Ok(()),
607            (i, Some(c)) => Err(Unexpected(i, Some(c))),
608        }
609    }
610
611    /// Validates a DID URL string.
612    fn validate_from(
613        i: usize,
614        bytes: &mut impl Iterator<Item = u8>,
615    ) -> Result<(usize, Option<u8>), Unexpected> {
616        match DID::validate_from(i, bytes)? {
617            (i, None) => Ok((i, None)),
618            (mut i, Some(c)) => {
619                enum State {
620                    PathSegment,
621                    Query,
622                    Fragment,
623                    Pct1(Part),
624                    Pct2(Part),
625                }
626
627                enum Part {
628                    PathSegment,
629                    Query,
630                    Fragment,
631                }
632
633                impl Part {
634                    pub fn state(&self) -> State {
635                        match self {
636                            Self::PathSegment => State::PathSegment,
637                            Self::Query => State::Query,
638                            Self::Fragment => State::Fragment,
639                        }
640                    }
641                }
642
643                let mut state = match c {
644                    b'/' => State::PathSegment,
645                    b'?' => State::Query,
646                    b'#' => State::Fragment,
647                    c => return Err(Unexpected(i, Some(c))),
648                };
649
650                fn is_unreserved(b: u8) -> bool {
651                    b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~')
652                }
653
654                fn is_sub_delims(b: u8) -> bool {
655                    matches!(
656                        b,
657                        b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
658                    )
659                }
660
661                fn is_pchar(b: u8) -> bool {
662                    is_unreserved(b) || is_sub_delims(b) || matches!(b, b':' | b'@')
663                }
664
665                loop {
666                    match state {
667                        State::PathSegment => match bytes.next() {
668                            Some(b'/') => (), // next segment.
669                            Some(b'?') => state = State::Query,
670                            Some(b'#') => state = State::Fragment,
671                            Some(b'%') => state = State::Pct1(Part::PathSegment),
672                            Some(c) if is_pchar(c) => (),
673                            c => break Ok((i, c)),
674                        },
675                        State::Query => match bytes.next() {
676                            Some(b'#') => state = State::Fragment,
677                            Some(b'%') => state = State::Pct1(Part::Query),
678                            Some(c) if is_pchar(c) || matches!(c, b'/' | b'?') => (),
679                            c => break Ok((i, c)),
680                        },
681                        State::Fragment => match bytes.next() {
682                            Some(b'%') => state = State::Pct1(Part::Fragment),
683                            Some(c) if is_pchar(c) || matches!(c, b'/' | b'?' | b'#') => (),
684                            c => break Ok((i, c)),
685                        },
686                        State::Pct1(q) => match bytes.next() {
687                            Some(c) if c.is_ascii_hexdigit() => state = State::Pct2(q),
688                            c => break Err(Unexpected(i, c)),
689                        },
690                        State::Pct2(q) => match bytes.next() {
691                            Some(c) if c.is_ascii_hexdigit() => state = q.state(),
692                            c => break Err(Unexpected(i, c)),
693                        },
694                    }
695
696                    i += 1
697                }
698            }
699        }
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    #[test]
708    fn parse_did_url_accept() {
709        let vectors: [&[u8]; 4] = [
710            b"did:method:foo",
711            b"did:a:b",
712            b"did:a:b#fragment",
713            b"did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#key"
714        ];
715
716        for input in vectors {
717            DIDURL::new(input).unwrap();
718        }
719    }
720
721    #[test]
722    fn parse_did_url_reject() {
723        let vectors: [&[u8]; 3] = [b"http:a:b", b"did::b", b"did:a:"];
724
725        for input in vectors {
726            assert!(DIDURL::new(input).is_err())
727        }
728    }
729}