fork of indigo with slightly nicer lexgen

atproto/syntax: initial DID and Handle format types

+37
atproto/syntax/did.go
··· 1 + package syntax 2 + 3 + import ( 4 + "fmt" 5 + "regexp" 6 + "strings" 7 + ) 8 + 9 + // Represents a syntaxtually valid DID identifier, as would pass Lexicon syntax validation. 10 + // 11 + // Syntax specification: https://atproto.com/specs/did 12 + type DID string 13 + 14 + func ParseDID(raw string) (DID, error) { 15 + if len(raw) > 2*1024 { 16 + return "", fmt.Errorf("DID is too long (2048 chars max)") 17 + } 18 + var didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`) 19 + if !didRegex.MatchString(raw) { 20 + return "", fmt.Errorf("DID syntax didn't validate via regex") 21 + } 22 + return DID(raw), nil 23 + } 24 + 25 + // The "method" part of the DID, between the 'did:' prefix and the final identifier segment, normalized to lower-case. 26 + func (d DID) Method() string { 27 + // syntax guarantees that there are at least 3 parts of split 28 + parts := strings.SplitN(string(d), ":", 3) 29 + return strings.ToLower(parts[1]) 30 + } 31 + 32 + // The final "identifier" segment of the DID 33 + func (d DID) Identifier() string { 34 + // syntax guarantees that there are at least 3 parts of split 35 + parts := strings.SplitN(string(d), ":", 3) 36 + return parts[2] 37 + }
+58
atproto/syntax/did_test.go
··· 1 + package syntax 2 + 3 + import ( 4 + "bufio" 5 + "fmt" 6 + "os" 7 + "testing" 8 + 9 + "github.com/stretchr/testify/assert" 10 + ) 11 + 12 + func TestDIDParts(t *testing.T) { 13 + assert := assert.New(t) 14 + d, err := ParseDID("did:example:123456789abcDEFghi") 15 + assert.NoError(err) 16 + assert.Equal("example", d.Method()) 17 + assert.Equal("123456789abcDEFghi", d.Identifier()) 18 + } 19 + 20 + func TestInteropDIDsValid(t *testing.T) { 21 + assert := assert.New(t) 22 + file, err := os.Open("testdata/did_syntax_valid.txt") 23 + assert.NoError(err) 24 + defer file.Close() 25 + scanner := bufio.NewScanner(file) 26 + for scanner.Scan() { 27 + line := scanner.Text() 28 + if len(line) == 0 || line[0] == '#' { 29 + continue 30 + } 31 + _, err := ParseDID(line) 32 + if err != nil { 33 + fmt.Println("GOOD: " + line) 34 + } 35 + assert.NoError(err) 36 + } 37 + assert.NoError(scanner.Err()) 38 + } 39 + 40 + func TestInteropDIDsInvalid(t *testing.T) { 41 + assert := assert.New(t) 42 + file, err := os.Open("testdata/did_syntax_invalid.txt") 43 + assert.NoError(err) 44 + defer file.Close() 45 + scanner := bufio.NewScanner(file) 46 + for scanner.Scan() { 47 + line := scanner.Text() 48 + if len(line) == 0 || line[0] == '#' { 49 + continue 50 + } 51 + _, err := ParseDID(line) 52 + if err == nil { 53 + fmt.Println("BAD: " + line) 54 + } 55 + assert.Error(err) 56 + } 57 + assert.NoError(scanner.Err()) 58 + }
+4
atproto/syntax/doc.go
··· 1 + // Package syntax provides types for identifiers and other string formats. 2 + // 3 + // These are primarily simple string alias types for parsing or verifying protocol-level syntax of identifiers, not routines for things like resolution or verification against application policies. 4 + package syntax
+51
atproto/syntax/handle.go
··· 1 + package syntax 2 + 3 + import ( 4 + "fmt" 5 + "regexp" 6 + "strings" 7 + ) 8 + 9 + // String type which represents a syntaxtually valid handle identifier, as would pass Lexicon syntax validation. 10 + // 11 + // Syntax specification: https://atproto.com/specs/handle 12 + type Handle string 13 + 14 + func ParseHandle(raw string) (Handle, error) { 15 + if len(raw) > 253 { 16 + return "", fmt.Errorf("Handle is too long (253 chars max)") 17 + } 18 + var handleRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) 19 + if !handleRegex.MatchString(raw) { 20 + return "", fmt.Errorf("Handle syntax didn't validate via regex") 21 + } 22 + return Handle(raw), nil 23 + } 24 + 25 + // Some top-level domains (TLDs) are disallowed for registration across the atproto ecosystem. The *syntax* is valid, but these should never be considered acceptable handles for account registration or linking. 26 + func (h *Handle) AllowedTLD() bool { 27 + switch h.TLD() { 28 + case "local", 29 + "arpa", 30 + "invalid", 31 + "localhost", 32 + "internal", 33 + "onion": 34 + return false 35 + } 36 + return true 37 + } 38 + 39 + func (h Handle) TLD() string { 40 + parts := strings.Split(string(h.Normalize()), ".") 41 + return parts[len(parts)-1] 42 + } 43 + 44 + // Is this the special "handle.invalid" handle? 45 + func (h Handle) IsInvalidHandle() bool { 46 + return h.Normalize() == "handle.invalid" 47 + } 48 + 49 + func (h Handle) Normalize() Handle { 50 + return Handle(strings.ToLower(string(h))) 51 + }
+62
atproto/syntax/handle_test.go
··· 1 + package syntax 2 + 3 + import ( 4 + "bufio" 5 + "fmt" 6 + "os" 7 + "testing" 8 + 9 + "github.com/stretchr/testify/assert" 10 + ) 11 + 12 + func TestInteropHandlesValid(t *testing.T) { 13 + assert := assert.New(t) 14 + file, err := os.Open("testdata/handle_syntax_valid.txt") 15 + assert.NoError(err) 16 + defer file.Close() 17 + scanner := bufio.NewScanner(file) 18 + for scanner.Scan() { 19 + line := scanner.Text() 20 + if len(line) == 0 || line[0] == '#' { 21 + continue 22 + } 23 + _, err := ParseHandle(line) 24 + if err != nil { 25 + fmt.Println("GOOD: " + line) 26 + } 27 + assert.NoError(err) 28 + } 29 + assert.NoError(scanner.Err()) 30 + } 31 + 32 + func TestInteropHandlesInvalid(t *testing.T) { 33 + assert := assert.New(t) 34 + file, err := os.Open("testdata/handle_syntax_invalid.txt") 35 + assert.NoError(err) 36 + defer file.Close() 37 + scanner := bufio.NewScanner(file) 38 + for scanner.Scan() { 39 + line := scanner.Text() 40 + if len(line) == 0 || line[0] == '#' { 41 + continue 42 + } 43 + _, err := ParseHandle(line) 44 + if err == nil { 45 + fmt.Println("BAD: " + line) 46 + } 47 + assert.Error(err) 48 + } 49 + assert.NoError(scanner.Err()) 50 + } 51 + 52 + func TestHandleNormalize(t *testing.T) { 53 + assert := assert.New(t) 54 + 55 + handle, err := ParseHandle("JoHn.TeST") 56 + assert.NoError(err) 57 + assert.Equal(string(handle.Normalize()), "john.test") 58 + assert.NoError(err) 59 + 60 + _, err = ParseHandle("JoH!n.TeST") 61 + assert.Error(err) 62 + }
+19
atproto/syntax/testdata/did_syntax_invalid.txt
··· 1 + did 2 + didmethodval 3 + method:did:val 4 + did:method: 5 + didmethod:val 6 + did:methodval) 7 + :did:method:val 8 + did.method.val 9 + did:method:val: 10 + did:method:val% 11 + DID:method:val 12 + did:METHOD:val 13 + did:m123:val 14 + did:method:val/two 15 + did:method:val?two 16 + did:method:val#two 17 + did:method:val% 18 + did:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 19 +
+26
atproto/syntax/testdata/did_syntax_valid.txt
··· 1 + did:method:val 2 + did:method:VAL 3 + did:method:val123 4 + did:method:123 5 + did:method:val-two 6 + did:method:val_two 7 + did:method:val.two 8 + did:method:val:two 9 + did:method:val%BB 10 + did:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 11 + did:m:v 12 + did:method::::val 13 + did:method:- 14 + did:method:-:_:.:%ab 15 + did:method:. 16 + did:method:_ 17 + did:method::. 18 + 19 + # allows some real DID values 20 + did:onion:2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid 21 + did:example:123456789abcdefghi 22 + did:plc:7iza6de2dwap2sbkpav7c6c6 23 + did:web:example.com 24 + did:web:localhost%3A1234 25 + did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N 26 + did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a
+57
atproto/syntax/testdata/handle_syntax_invalid.txt
··· 1 + # throws on invalid handles 2 + did:thing.test 3 + did:thing 4 + john-.test 5 + john.0 6 + john.- 7 + xn--bcher-.tld 8 + john..test 9 + jo_hn.test 10 + -john.test 11 + .john.test 12 + jo!hn.test 13 + jo%hn.test 14 + jo&hn.test 15 + jo@hn.test 16 + jo*hn.test 17 + jo|hn.test 18 + jo:hn.test 19 + jo/hn.test 20 + john💩.test 21 + bücher.test 22 + john .test 23 + john.test. 24 + john 25 + john. 26 + .john 27 + john.test. 28 + .john.test 29 + john.test 30 + john.test 31 + joh-.test 32 + john.-est 33 + john.tes- 34 + 35 + # TODO: short/long examples 36 + 37 + # throws on "dotless" TLD handles 38 + org 39 + ai 40 + gg 41 + io 42 + 43 + # correctly validates corner cases (modern vs. old RFCs) 44 + cn.8 45 + thing.0aa 46 + thing.0aa 47 + 48 + # does not allow IP addresses as handles 49 + 127.0.0.1 50 + 192.168.0.142 51 + fe80::7325:8a97:c100:94b 52 + 2600:3c03::f03c:9100:feb0:af1f 53 + 54 + # examples from stackoverflow 55 + -notvalid.at-all 56 + -thing.com 57 + www.masełkowski.pl.com
+85
atproto/syntax/testdata/handle_syntax_valid.txt
··· 1 + # allows valid handles 2 + A.ISI.EDU 3 + XX.LCS.MIT.EDU 4 + SRI-NIC.ARPA 5 + john.test 6 + jan.test 7 + a234567890123456789.test 8 + john2.test 9 + john-john.test 10 + john.bsky.app 11 + jo.hn 12 + a.co 13 + a.org 14 + joh.n 15 + j0.h0 16 + jaymome-johnber123456.test 17 + jay.mome-johnber123456.test 18 + john.test.bsky.app 19 + # TODO: short and long 20 + 21 + # NOTE: this probably isn't ever going to be a real domain, but my read of the RFC is that it would be possible 22 + john.t 23 + 24 + # allows .local and .arpa handles (proto-level) 25 + laptop.local 26 + laptop.arpa 27 + 28 + # allows punycode handles 29 + # 💩.test 30 + xn--ls8h.test 31 + # bücher.tld 32 + xn--bcher-kva.tld 33 + xn--3jk.com 34 + xn--w3d.com 35 + xn--vqb.com 36 + xn--ppd.com 37 + xn--cs9a.com 38 + xn--8r9a.com 39 + xn--cfd.com 40 + xn--5jk.com 41 + xn--2lb.com 42 + 43 + # allows onion (Tor) handles 44 + expyuzz4wqqyqhjn.onion 45 + friend.expyuzz4wqqyqhjn.onion 46 + g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 47 + friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 48 + friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 49 + 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 50 + friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 51 + 52 + # correctly validates corner cases (modern vs. old RFCs) 53 + 12345.test 54 + 8.cn 55 + 4chan.org 56 + 4chan.o-g 57 + blah.4chan.org 58 + thing.a01 59 + 120.0.0.1.com 60 + 0john.test 61 + 9sta--ck.com 62 + 99stack.com 63 + 0ohn.test 64 + john.t--t 65 + thing.0aa.thing 66 + 67 + # examples from stackoverflow 68 + stack.com 69 + sta-ck.com 70 + sta---ck.com 71 + sta--ck9.com 72 + stack99.com 73 + sta99ck.com 74 + google.com.uk 75 + google.co.in 76 + google.com 77 + maselkowski.pl 78 + m.maselkowski.pl 79 + xn--masekowski-d0b.pl 80 + xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s 81 + xn--stackoverflow.com 82 + stackoverflow.xn--com 83 + stackoverflow.co.uk 84 + xn--masekowski-d0b.pl 85 + xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s