Skip to main content

identity_kit/did/
method.rs

1//! `DidMethod`: a DID method name with an optional namespace, such as `did:demo` or
2//! `did:demo:dev`.
3
4use core::fmt;
5use core::str::FromStr;
6
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::DidError;
11use super::syntax::{
12    DEFAULT_MAX_LEN, Did, check_len, is_idchar, is_method_char, is_pct_encoded_at,
13};
14
15/// A DID method, named with an optional namespace: `did:demo` or `did:demo:dev`.
16///
17/// Parties that declare or negotiate support for DID methods (SIOPv2's
18/// `subject_syntax_types_supported`, a wallet's configuration, a resolver's registry) exchange
19/// method identifiers rather than DIDs. A `DidMethod` is such an identifier: the method name of
20/// [DID Core §3.1] (`method-name = 1*method-char`, lowercase ASCII letters and digits only),
21/// optionally followed by one `idchar` segment that names a namespace within the method, as
22/// `dev` in `did:demo:dev` or `zXYZ` in `did:ebsi:zXYZ`. SIOPv2 lists methods without a
23/// namespace; wallets and issuers that distinguish networks of one method exchange the namespaced
24/// form; both parse.
25///
26/// A `DidMethod` is validated when it is constructed and keeps the input unchanged. It displays,
27/// serializes and deserializes as `did:<name>` or `did:<name>:<namespace>`, where
28/// deserialization validates. Equality is field-wise; the ordering is by name, then namespace,
29/// with the bare method before its namespaced forms.
30///
31/// [`DidMethod::matches`] tells whether a [`Did`] belongs to the method: its method name is
32/// equal and, when a namespace is set, the first colon-separated segment of its method-specific
33/// id is that namespace. [`Did::method`] keeps returning the name as a `&str`;
34/// `DidMethod::from(&did)` is the method of a DID, without a namespace.
35///
36/// # Errors
37///
38/// A method name that is empty or has a character outside `a-z` and `0-9` is
39/// [`DidError::InvalidMethodName`], as it is for a [`Did`]. A namespace that is empty, has a
40/// second segment (`did:demo:dev:x`) or a character that is not an `idchar` is
41/// [`DidError::InvalidNamespace`]; an incomplete percent-encoding in it is
42/// [`DidError::InvalidPercentEncoding`]. Parsing a string also reports
43/// [`DidError::MissingScheme`] and [`DidError::TooLong`] as [`Did::parse`] does. Positions are
44/// byte offsets into the input that was given: the string for the parsing forms, the name for
45/// [`DidMethod::new`], the namespace for [`DidMethod::with_namespace`].
46///
47/// # Examples
48///
49/// ```
50/// use identity_kit::did::{Did, DidError, DidMethod};
51///
52/// let demo: DidMethod = "did:demo".parse()?;
53/// let namespaced = DidMethod::new("demo")?.with_namespace("dev")?;
54/// assert_eq!(demo.name(), "demo");
55/// assert_eq!(demo.namespace(), None);
56/// assert_eq!(namespaced.namespace(), Some("dev"));
57/// assert_eq!(namespaced.to_string(), "did:demo:dev");
58///
59/// let did = Did::parse("did:demo:dev:0xabc")?;
60/// assert!(demo.matches(&did));
61/// assert!(namespaced.matches(&did));
62/// assert!(!DidMethod::parse("did:demo:alt")?.matches(&did));
63/// assert_eq!(DidMethod::from(&did), demo);
64///
65/// // The method name is lowercase; the namespace is one non-empty segment.
66/// assert_eq!(
67///     DidMethod::parse("did:Example"),
68///     Err(DidError::InvalidMethodName { position: 4 })
69/// );
70/// assert_eq!(
71///     DidMethod::parse("did:demo:"),
72///     Err(DidError::InvalidNamespace { position: 9 })
73/// );
74/// assert_eq!(
75///     DidMethod::parse("did:demo:dev:x"),
76///     Err(DidError::InvalidNamespace { position: 12 })
77/// );
78/// # Ok::<(), DidError>(())
79/// ```
80///
81/// [DID Core §3.1]: https://www.w3.org/TR/did-core/#did-syntax
82#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
83pub struct DidMethod {
84    /// The method name: `demo` in `did:demo:dev`.
85    name: String,
86    /// The namespace within the method: `dev` in `did:demo:dev`.
87    namespace: Option<String>,
88}
89
90impl DidMethod {
91    /// A method named `name`, without a namespace.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`DidError::InvalidMethodName`] when `name` is empty or has a character outside
96    /// `a-z` and `0-9`, with the byte offset into `name`.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use identity_kit::did::{DidError, DidMethod};
102    ///
103    /// assert_eq!(DidMethod::new("web")?.to_string(), "did:web");
104    /// assert_eq!(
105    ///     DidMethod::new("ex_ample"),
106    ///     Err(DidError::InvalidMethodName { position: 2 })
107    /// );
108    /// # Ok::<(), DidError>(())
109    /// ```
110    pub fn new(name: impl Into<String>) -> Result<Self, DidError> {
111        let name = name.into();
112        validate_name(&name, 0)?;
113        Ok(Self {
114            name,
115            namespace: None,
116        })
117    }
118
119    /// This method with its namespace set to `namespace`.
120    ///
121    /// # Errors
122    ///
123    /// Returns [`DidError::InvalidNamespace`] when `namespace` is empty or has a character that
124    /// is not an `idchar` (a `:` among them), or [`DidError::InvalidPercentEncoding`] when a `%`
125    /// is not followed by two hexadecimal digits, with the byte offset into `namespace`.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use identity_kit::did::{DidError, DidMethod};
131    ///
132    /// let ebsi = DidMethod::new("ebsi")?;
133    /// assert_eq!(ebsi.clone().with_namespace("zXYZ")?.to_string(), "did:ebsi:zXYZ");
134    /// assert_eq!(
135    ///     ebsi.with_namespace("a:b"),
136    ///     Err(DidError::InvalidNamespace { position: 1 })
137    /// );
138    /// # Ok::<(), DidError>(())
139    /// ```
140    pub fn with_namespace(self, namespace: impl Into<String>) -> Result<Self, DidError> {
141        let namespace = namespace.into();
142        validate_namespace(&namespace, 0)?;
143        Ok(Self {
144            name: self.name,
145            namespace: Some(namespace),
146        })
147    }
148
149    /// Parses `did:<name>` or `did:<name>:<namespace>`, rejecting input longer than
150    /// [`DEFAULT_MAX_LEN`] bytes.
151    ///
152    /// # Errors
153    ///
154    /// Returns a [`DidError`] describing the first violation, as listed for [`DidMethod`].
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use identity_kit::did::{DidError, DidMethod};
160    ///
161    /// let method = DidMethod::parse("did:ebsi:zXYZ")?;
162    /// assert_eq!(method.name(), "ebsi");
163    /// assert_eq!(method.namespace(), Some("zXYZ"));
164    ///
165    /// assert_eq!(DidMethod::parse("ebsi"), Err(DidError::MissingScheme));
166    /// # Ok::<(), DidError>(())
167    /// ```
168    pub fn parse(s: &str) -> Result<Self, DidError> {
169        check_len(s, DEFAULT_MAX_LEN)?;
170        let Some(rest) = s.strip_prefix(SCHEME) else {
171            return Err(DidError::MissingScheme);
172        };
173        let (name, namespace) = match rest.split_once(':') {
174            Some((name, namespace)) => (name, Some(namespace)),
175            None => (rest, None),
176        };
177        validate_name(name, SCHEME.len())?;
178        if let Some(namespace) = namespace {
179            validate_namespace(namespace, SCHEME.len() + name.len() + 1)?;
180        }
181        Ok(Self {
182            name: name.to_owned(),
183            namespace: namespace.map(str::to_owned),
184        })
185    }
186
187    /// The method name: `demo` in `did:demo:dev`.
188    pub fn name(&self) -> &str {
189        &self.name
190    }
191
192    /// The namespace within the method: `dev` in `did:demo:dev`, `None` in `did:demo`.
193    pub fn namespace(&self) -> Option<&str> {
194        self.namespace.as_deref()
195    }
196
197    /// Whether `did` belongs to this method: its method name is [`name`](Self::name) and, when a
198    /// namespace is set, the first colon-separated segment of its method-specific id is that
199    /// namespace.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// use identity_kit::did::{Did, DidMethod};
205    ///
206    /// let did = Did::parse("did:demo:dev:0xabc")?;
207    /// assert!(DidMethod::parse("did:demo")?.matches(&did));
208    /// assert!(DidMethod::parse("did:demo:dev")?.matches(&did));
209    /// assert!(!DidMethod::parse("did:demo:alt")?.matches(&did));
210    /// assert!(!DidMethod::parse("did:web")?.matches(&did));
211    /// # Ok::<(), identity_kit::did::DidError>(())
212    /// ```
213    pub fn matches(&self, did: &Did) -> bool {
214        if did.method() != self.name {
215            return false;
216        }
217        match &self.namespace {
218            None => true,
219            Some(namespace) => {
220                let id = did.method_specific_id();
221                let first = id.split_once(':').map_or(id, |(first, _)| first);
222                first == namespace
223            }
224        }
225    }
226}
227
228/// The method of a DID, without a namespace: `did:demo` for `did:demo:dev:0xabc`.
229impl From<&Did> for DidMethod {
230    fn from(did: &Did) -> Self {
231        Self {
232            name: did.method().to_owned(),
233            namespace: None,
234        }
235    }
236}
237
238impl fmt::Display for DidMethod {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        f.write_str(SCHEME)?;
241        f.write_str(&self.name)?;
242        if let Some(namespace) = &self.namespace {
243            f.write_str(":")?;
244            f.write_str(namespace)?;
245        }
246        Ok(())
247    }
248}
249
250impl FromStr for DidMethod {
251    type Err = DidError;
252
253    fn from_str(s: &str) -> Result<Self, Self::Err> {
254        Self::parse(s)
255    }
256}
257
258impl TryFrom<&str> for DidMethod {
259    type Error = DidError;
260
261    fn try_from(s: &str) -> Result<Self, Self::Error> {
262        Self::parse(s)
263    }
264}
265
266impl TryFrom<String> for DidMethod {
267    type Error = DidError;
268
269    fn try_from(s: String) -> Result<Self, Self::Error> {
270        Self::parse(&s)
271    }
272}
273
274impl Serialize for DidMethod {
275    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
276        serializer.collect_str(self)
277    }
278}
279
280impl<'de> Deserialize<'de> for DidMethod {
281    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
282        let s = String::deserialize(deserializer)?;
283        Self::parse(&s).map_err(D::Error::custom)
284    }
285}
286
287const SCHEME: &str = "did:";
288
289/// Checks `name` against `method-name = 1*method-char`; positions are reported from `offset`.
290fn validate_name(name: &str, offset: usize) -> Result<(), DidError> {
291    if name.is_empty() {
292        return Err(DidError::InvalidMethodName { position: offset });
293    }
294    match name.bytes().position(|c| !is_method_char(c)) {
295        None => Ok(()),
296        Some(i) => Err(DidError::InvalidMethodName {
297            position: offset + i,
298        }),
299    }
300}
301
302/// Checks `namespace` against `1*idchar`; positions are reported from `offset`.
303///
304/// The scanner only advances over ASCII bytes, so every position it reports is a `char` boundary.
305fn validate_namespace(namespace: &str, offset: usize) -> Result<(), DidError> {
306    let bytes = namespace.as_bytes();
307    if bytes.is_empty() {
308        return Err(DidError::InvalidNamespace { position: offset });
309    }
310    let mut i = 0;
311    while i < bytes.len() {
312        match bytes[i] {
313            b'%' if is_pct_encoded_at(bytes, i) => i += 3,
314            b'%' => {
315                return Err(DidError::InvalidPercentEncoding {
316                    position: offset + i,
317                });
318            }
319            c if is_idchar(c) => i += 1,
320            _ => {
321                return Err(DidError::InvalidNamespace {
322                    position: offset + i,
323                });
324            }
325        }
326    }
327    Ok(())
328}