ssi_dids_core/
did.rs

1use core::fmt;
2use std::{borrow::Borrow, ops::Deref, str::FromStr};
3
4mod url;
5
6use iref::{Iri, IriBuf, Uri, UriBuf};
7use serde::{Deserialize, Serialize};
8pub use url::*;
9
10/// Error raised when a conversion to a DID fails.
11#[derive(Debug, thiserror::Error)]
12#[error("invalid DID `{0}`: {1}")]
13pub struct InvalidDID<T>(pub T, pub Unexpected);
14
15impl<T> InvalidDID<T> {
16    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> InvalidDID<U> {
17        InvalidDID(f(self.0), self.1)
18    }
19}
20
21#[macro_export]
22macro_rules! did {
23    ($did:literal) => {
24        $crate::DID::new($did).unwrap()
25    };
26}
27
28/// DID.
29///
30/// This type is unsized and used to represent borrowed DIDs. Use `DIDBuf` for
31/// owned DIDs.
32#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
33#[repr(transparent)]
34pub struct DID([u8]);
35
36impl DID {
37    /// Converts the input `data` to a DID.
38    ///
39    /// Fails if the data is not a DID according to the
40    /// [DID Syntax](https://w3c.github.io/did-core/#did-syntax).
41    pub fn new<B: ?Sized + AsRef<[u8]>>(data: &B) -> Result<&Self, InvalidDID<&B>> {
42        let bytes = data.as_ref();
43        match Self::validate(bytes) {
44            Ok(()) => Ok(unsafe {
45                // SAFETY: DID is a transparent wrapper over `[u8]`,
46                //         and we just checked that `data` is a DID.
47                std::mem::transmute::<&[u8], &Self>(bytes)
48            }),
49            Err(e) => Err(InvalidDID(data, e)),
50        }
51    }
52
53    /// Converts the input `data` to a DID without validation.
54    ///
55    /// # Safety
56    ///
57    /// The input `data` must be a DID according to the
58    /// [DID Syntax](https://w3c.github.io/did-core/#did-syntax).
59    pub unsafe fn new_unchecked(data: &[u8]) -> &Self {
60        // SAFETY: DID is a transparent wrapper over `[u8]`,
61        //         but we didn't check if it is actually a DID.
62        std::mem::transmute(data)
63    }
64
65    pub fn as_iri(&self) -> &Iri {
66        unsafe {
67            // SAFETY: a DID is an IRI.
68            Iri::new_unchecked(self.as_str())
69        }
70    }
71
72    pub fn as_uri(&self) -> &Uri {
73        unsafe {
74            // SAFETY: a DID is an URI.
75            Uri::new_unchecked(&self.0)
76        }
77    }
78
79    /// Returns the DID as a string.
80    pub fn as_str(&self) -> &str {
81        unsafe {
82            // SAFETY: a DID is a valid ASCII string.
83            std::str::from_utf8_unchecked(&self.0)
84        }
85    }
86
87    /// Returns the DID as a byte string.
88    pub fn as_bytes(&self) -> &[u8] {
89        &self.0
90    }
91
92    /// Returns the offset of the `:` byte just after the method name.
93    fn method_name_separator_offset(&self) -> usize {
94        self.0[5..].iter().position(|b| *b == b':').unwrap() + 5 // +5 and not +4 because the method name cannot be empty.
95    }
96
97    /// Returns the bytes of the DID method name.
98    pub fn method_name_bytes(&self) -> &[u8] {
99        &self.0[4..self.method_name_separator_offset()]
100    }
101
102    /// Returns the DID method name.
103    pub fn method_name(&self) -> &str {
104        unsafe {
105            // SAFETY: the method name is a valid ASCII string.
106            std::str::from_utf8_unchecked(self.method_name_bytes())
107        }
108    }
109
110    /// Returns the bytes of the DID method specific identifier.
111    pub fn method_specific_id_bytes(&self) -> &[u8] {
112        &self.0[self.method_name_separator_offset() + 1..]
113    }
114
115    /// Returns the DID method specific identifier.
116    pub fn method_specific_id(&self) -> &str {
117        unsafe {
118            // SAFETY: the method specific id is a valid ASCII string.
119            std::str::from_utf8_unchecked(self.method_specific_id_bytes())
120        }
121    }
122}
123
124impl Deref for DID {
125    type Target = str;
126
127    fn deref(&self) -> &Self::Target {
128        self.as_str()
129    }
130}
131
132impl Borrow<Uri> for DID {
133    fn borrow(&self) -> &Uri {
134        self.as_uri()
135    }
136}
137
138impl Borrow<Iri> for DID {
139    fn borrow(&self) -> &Iri {
140        self.as_iri()
141    }
142}
143
144impl PartialEq<DIDBuf> for DID {
145    fn eq(&self, other: &DIDBuf) -> bool {
146        self == other.as_did()
147    }
148}
149
150impl PartialEq<DIDURL> for DID {
151    fn eq(&self, other: &DIDURL) -> bool {
152        other == self
153    }
154}
155
156impl PartialEq<DIDURLBuf> for DID {
157    fn eq(&self, other: &DIDURLBuf) -> bool {
158        other == self
159    }
160}
161
162impl ToOwned for DID {
163    type Owned = DIDBuf;
164
165    fn to_owned(&self) -> Self::Owned {
166        unsafe { DIDBuf::new_unchecked(self.as_bytes().to_vec()) }
167    }
168}
169
170impl fmt::Display for DID {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        self.as_str().fmt(f)
173    }
174}
175
176/// Owned DID.
177#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
178pub struct DIDBuf(Vec<u8>);
179
180impl DIDBuf {
181    pub fn new(data: Vec<u8>) -> Result<Self, InvalidDID<Vec<u8>>> {
182        match DID::validate(&data) {
183            Ok(()) => Ok(Self(data)),
184            Err(e) => Err(InvalidDID(data, e)),
185        }
186    }
187
188    pub fn from_string(data: String) -> Result<Self, InvalidDID<String>> {
189        Self::new(data.into_bytes()).map_err(|InvalidDID(bytes, e)| {
190            InvalidDID(unsafe { String::from_utf8_unchecked(bytes) }, e)
191        })
192    }
193
194    /// Creates a new DID buffer without validation.
195    ///
196    /// # Safety
197    ///
198    /// The input data must be a valid DID.
199    pub unsafe fn new_unchecked(data: Vec<u8>) -> Self {
200        Self(data)
201    }
202
203    pub fn as_did(&self) -> &DID {
204        unsafe {
205            // SAFETY: we validated the data in `Self::new`.
206            DID::new_unchecked(&self.0)
207        }
208    }
209
210    pub fn as_did_url(&self) -> &DIDURL {
211        unsafe {
212            // SAFETY: we validated the data in `Self::new`.
213            DIDURL::new_unchecked(&self.0)
214        }
215    }
216
217    pub fn into_iri(self) -> IriBuf {
218        unsafe { IriBuf::new_unchecked(String::from_utf8_unchecked(self.0)) }
219    }
220
221    pub fn into_uri(self) -> UriBuf {
222        unsafe { UriBuf::new_unchecked(self.0) }
223    }
224
225    pub fn into_string(self) -> String {
226        unsafe { String::from_utf8_unchecked(self.0) }
227    }
228}
229
230impl TryFrom<String> for DIDBuf {
231    type Error = InvalidDID<String>;
232
233    fn try_from(value: String) -> Result<Self, Self::Error> {
234        DIDBuf::new(value.into_bytes()).map_err(|e| {
235            e.map(|bytes| unsafe {
236                // SAFETY: `bytes` comes from the `value` string, which is UTF-8
237                //         encoded by definition.
238                String::from_utf8_unchecked(bytes)
239            })
240        })
241    }
242}
243
244impl FromStr for DIDBuf {
245    type Err = InvalidDID<String>;
246
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        s.to_owned().try_into()
249    }
250}
251
252impl From<DIDBuf> for UriBuf {
253    fn from(value: DIDBuf) -> Self {
254        value.into_uri()
255    }
256}
257
258impl From<DIDBuf> for IriBuf {
259    fn from(value: DIDBuf) -> Self {
260        value.into_iri()
261    }
262}
263
264impl Deref for DIDBuf {
265    type Target = DID;
266
267    fn deref(&self) -> &Self::Target {
268        self.as_did()
269    }
270}
271
272impl Borrow<DID> for DIDBuf {
273    fn borrow(&self) -> &DID {
274        self.as_did()
275    }
276}
277
278impl Borrow<DIDURL> for DIDBuf {
279    fn borrow(&self) -> &DIDURL {
280        self.as_did_url()
281    }
282}
283
284impl Borrow<Uri> for DIDBuf {
285    fn borrow(&self) -> &Uri {
286        self.as_uri()
287    }
288}
289
290impl Borrow<Iri> for DIDBuf {
291    fn borrow(&self) -> &Iri {
292        self.as_iri()
293    }
294}
295
296impl fmt::Display for DIDBuf {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        self.as_str().fmt(f)
299    }
300}
301
302impl fmt::Debug for DIDBuf {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        self.as_str().fmt(f)
305    }
306}
307
308impl PartialEq<str> for DIDBuf {
309    fn eq(&self, other: &str) -> bool {
310        self.as_str() == other
311    }
312}
313
314impl<'a> PartialEq<&'a str> for DIDBuf {
315    fn eq(&self, other: &&'a str) -> bool {
316        self.as_str() == *other
317    }
318}
319
320impl PartialEq<DID> for DIDBuf {
321    fn eq(&self, other: &DID) -> bool {
322        self.as_did() == other
323    }
324}
325
326impl<'a> PartialEq<&'a DID> for DIDBuf {
327    fn eq(&self, other: &&'a DID) -> bool {
328        self.as_did() == *other
329    }
330}
331
332impl PartialEq<DIDURL> for DIDBuf {
333    fn eq(&self, other: &DIDURL) -> bool {
334        self.as_did() == other
335    }
336}
337
338impl<'a> PartialEq<&'a DIDURL> for DIDBuf {
339    fn eq(&self, other: &&'a DIDURL) -> bool {
340        self.as_did() == *other
341    }
342}
343
344impl PartialEq<DIDURLBuf> for DIDBuf {
345    fn eq(&self, other: &DIDURLBuf) -> bool {
346        self.as_did() == other
347    }
348}
349
350impl Serialize for DIDBuf {
351    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
352    where
353        S: serde::Serializer,
354    {
355        self.as_str().serialize(serializer)
356    }
357}
358
359impl<'de> Deserialize<'de> for DIDBuf {
360    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
361    where
362        D: serde::Deserializer<'de>,
363    {
364        struct Visitor;
365
366        impl serde::de::Visitor<'_> for Visitor {
367            type Value = DIDBuf;
368
369            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
370                write!(f, "a DID")
371            }
372
373            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
374            where
375                E: serde::de::Error,
376            {
377                v.try_into().map_err(|e| E::custom(e))
378            }
379
380            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
381            where
382                E: serde::de::Error,
383            {
384                self.visit_string(v.to_string())
385            }
386        }
387
388        deserializer.deserialize_string(Visitor)
389    }
390}
391
392#[derive(Debug, thiserror::Error)]
393pub struct Unexpected(pub usize, pub Option<u8>);
394
395impl fmt::Display for Unexpected {
396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397        match self.1 {
398            Some(b) => write!(f, "unexpected byte {b} at offset {0:#04x}", self.0),
399            None => write!(f, "unexpected end at offset {0:#04x}", self.0),
400        }
401    }
402}
403
404impl DID {
405    /// Validates a DID string.
406    fn validate(data: &[u8]) -> Result<(), Unexpected> {
407        let mut bytes = data.iter().copied();
408        match Self::validate_from(0, &mut bytes)? {
409            (_, None) => Ok(()),
410            (i, Some(c)) => Err(Unexpected(i, Some(c))),
411        }
412    }
413
414    /// Validates a DID string.
415    fn validate_from(
416        mut i: usize,
417        bytes: &mut impl Iterator<Item = u8>,
418    ) -> Result<(usize, Option<u8>), Unexpected> {
419        enum State {
420            Scheme1,         // d
421            Scheme2,         // i
422            Scheme3,         // d
423            SchemeSeparator, // :
424            MethodNameStart,
425            MethodName,
426            MethodSpecificIdStartOrSeparator,
427            MethodSpecificIdPct1,
428            MethodSpecificIdPct2,
429            MethodSpecificId,
430        }
431
432        let mut state = State::Scheme1;
433        fn is_method_char(b: u8) -> bool {
434            matches!(b, 0x61..=0x7a) || b.is_ascii_digit()
435        }
436
437        fn is_id_char(b: u8) -> bool {
438            b.is_ascii_alphanumeric() || matches!(b, b'.' | b'-' | b'_')
439        }
440
441        loop {
442            match state {
443                State::Scheme1 => match bytes.next() {
444                    Some(b'd') => state = State::Scheme2,
445                    c => break Err(Unexpected(i, c)),
446                },
447                State::Scheme2 => match bytes.next() {
448                    Some(b'i') => state = State::Scheme3,
449                    c => break Err(Unexpected(i, c)),
450                },
451                State::Scheme3 => match bytes.next() {
452                    Some(b'd') => state = State::SchemeSeparator,
453                    c => break Err(Unexpected(i, c)),
454                },
455                State::SchemeSeparator => match bytes.next() {
456                    Some(b':') => state = State::MethodNameStart,
457                    c => break Err(Unexpected(i, c)),
458                },
459                State::MethodNameStart => match bytes.next() {
460                    Some(c) if is_method_char(c) => state = State::MethodName,
461                    c => break Err(Unexpected(i, c)),
462                },
463                State::MethodName => match bytes.next() {
464                    Some(b':') => state = State::MethodSpecificIdStartOrSeparator,
465                    Some(c) if is_method_char(c) => (),
466                    c => break Err(Unexpected(i, c)),
467                },
468                State::MethodSpecificIdStartOrSeparator => match bytes.next() {
469                    Some(b':') => (),
470                    Some(b'%') => state = State::MethodSpecificIdPct1,
471                    Some(c) if is_id_char(c) => state = State::MethodSpecificId,
472                    c => break Err(Unexpected(i, c)),
473                },
474                State::MethodSpecificIdPct1 => match bytes.next() {
475                    Some(c) if c.is_ascii_hexdigit() => state = State::MethodSpecificIdPct2,
476                    c => break Err(Unexpected(i, c)),
477                },
478                State::MethodSpecificIdPct2 => match bytes.next() {
479                    Some(c) if c.is_ascii_hexdigit() => state = State::MethodSpecificId,
480                    c => break Err(Unexpected(i, c)),
481                },
482                State::MethodSpecificId => match bytes.next() {
483                    Some(b':') => state = State::MethodSpecificIdStartOrSeparator,
484                    Some(b'%') => state = State::MethodSpecificIdPct1,
485                    Some(c) if is_id_char(c) => (),
486                    c => break Ok((i, c)),
487                },
488            }
489
490            i += 1
491        }
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn parse_did_accept() {
501        let vectors: [&[u8]; 4] = [
502            b"did:method:foo",
503            b"did:a:b",
504            b"did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9",
505            b"did:web:example.com%3A443:u:bob"
506        ];
507
508        for input in vectors {
509            DID::new(input).unwrap();
510        }
511    }
512
513    #[test]
514    fn parse_did_reject() {
515        let vectors: [&[u8]; 3] = [b"http:a:b", b"did::b", b"did:a:"];
516
517        for input in vectors {
518            assert!(DID::new(input).is_err())
519        }
520    }
521}