1package syntax
2
3import (
4 "errors"
5 "regexp"
6 "strings"
7)
8
9// Represents a syntaxtually valid DID identifier, as would pass Lexicon syntax validation.
10//
11// Always use [ParseDID] instead of wrapping strings directly, especially when working with input.
12//
13// Syntax specification: https://atproto.com/specs/did
14type DID string
15
16var didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
17
18func isASCIIAlphaNum(c rune) bool {
19 if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') {
20 return true
21 }
22 return false
23}
24
25func ParseDID(raw string) (DID, error) {
26 // fast-path for did:plc, avoiding regex
27 if len(raw) == 32 && strings.HasPrefix(raw, "did:plc:") {
28 // NOTE: this doesn't really check base32, just broader alphanumberic. might pass invalid PLC DIDs, but they still have overall valid DID syntax
29 isPlc := true
30 for _, c := range raw[8:32] {
31 if !isASCIIAlphaNum(c) {
32 isPlc = false
33 break
34 }
35 }
36 if isPlc {
37 return DID(raw), nil
38 }
39 }
40 if raw == "" {
41 return "", errors.New("expected DID, got empty string")
42 }
43 if len(raw) > 2*1024 {
44 return "", errors.New("DID is too long (2048 chars max)")
45 }
46 if !didRegex.MatchString(raw) {
47 return "", errors.New("DID syntax didn't validate via regex")
48 }
49 return DID(raw), nil
50}
51
52// The "method" part of the DID, between the 'did:' prefix and the final identifier segment, normalized to lower-case.
53func (d DID) Method() string {
54 // syntax guarantees that there are at least 3 parts of split
55 parts := strings.SplitN(string(d), ":", 3)
56 if len(parts) < 2 {
57 // this should be impossible; return empty to avoid out-of-bounds
58 return ""
59 }
60 return strings.ToLower(parts[1])
61}
62
63// The final "identifier" segment of the DID
64func (d DID) Identifier() string {
65 // syntax guarantees that there are at least 3 parts of split
66 parts := strings.SplitN(string(d), ":", 3)
67 if len(parts) < 3 {
68 // this should be impossible; return empty to avoid out-of-bounds
69 return ""
70 }
71 return parts[2]
72}
73
74func (d DID) AtIdentifier() AtIdentifier {
75 return AtIdentifier{Inner: d}
76}
77
78func (d DID) String() string {
79 return string(d)
80}
81
82func (d DID) MarshalText() ([]byte, error) {
83 return []byte(d.String()), nil
84}
85
86func (d *DID) UnmarshalText(text []byte) error {
87 did, err := ParseDID(string(text))
88 if err != nil {
89 return err
90 }
91 *d = did
92 return nil
93}