atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

at dfd015c4e12850beb5b43c8b301c91bc8ef0f877 197 lines 6.3 kB view raw
1//! DID - Decentralized Identifier 2//! 3//! dids are persistent, long-term account identifiers based on W3C standard. 4//! format: did:<method>:<identifier> 5//! 6//! validation rules: 7//! - max 2048 characters 8//! - method must be lowercase letters only 9//! - identifier allows: a-zA-Z0-9._:%- 10//! - cannot end with : or % 11//! - cannot contain: / ? # [ ] @ 12//! 13//! see: https://atproto.com/specs/did 14 15const std = @import("std"); 16 17pub const Did = struct { 18 /// the full did string (borrowed, not owned) 19 raw: []const u8, 20 21 /// offset where method starts (after "did:") 22 method_start: usize, 23 24 /// offset where method ends / identifier starts 25 id_start: usize, 26 27 pub const max_length = 2048; 28 29 pub const Method = enum { 30 plc, 31 web, 32 other, 33 }; 34 35 /// parse a did string. returns null if invalid. 36 pub fn parse(s: []const u8) ?Did { 37 // length check 38 if (s.len == 0 or s.len > max_length) return null; 39 40 // must start with "did:" 41 if (!std.mem.startsWith(u8, s, "did:")) return null; 42 43 // find method end (next colon) 44 const after_did = s[4..]; 45 const method_end = std.mem.indexOfScalar(u8, after_did, ':') orelse return null; 46 if (method_end == 0) return null; // empty method 47 48 // method must be lowercase letters only 49 const method_str = after_did[0..method_end]; 50 for (method_str) |c| { 51 if (c < 'a' or c > 'z') return null; 52 } 53 54 // identifier must not be empty 55 const id_offset = 4 + method_end + 1; 56 if (id_offset >= s.len) return null; 57 58 const id_part = s[id_offset..]; 59 60 // cannot end with : or % 61 const last = id_part[id_part.len - 1]; 62 if (last == ':' or last == '%') return null; 63 64 // validate identifier characters 65 if (!isValidIdentifier(id_part)) return null; 66 67 return .{ 68 .raw = s, 69 .method_start = 4, 70 .id_start = id_offset, 71 }; 72 } 73 74 /// the method portion (e.g., "plc", "web") 75 pub fn methodStr(self: Did) []const u8 { 76 return self.raw[self.method_start .. self.id_start - 1]; 77 } 78 79 /// the method as an enum (plc, web, or other) 80 pub fn method(self: Did) Method { 81 const m = self.methodStr(); 82 if (std.mem.eql(u8, m, "plc")) return .plc; 83 if (std.mem.eql(u8, m, "web")) return .web; 84 return .other; 85 } 86 87 /// the method-specific identifier 88 pub fn identifier(self: Did) []const u8 { 89 return self.raw[self.id_start..]; 90 } 91 92 /// check if this is a plc did 93 pub fn isPlc(self: Did) bool { 94 return self.method() == .plc; 95 } 96 97 /// check if this is a web did 98 pub fn isWeb(self: Did) bool { 99 return self.method() == .web; 100 } 101 102 /// get the full did string 103 pub fn str(self: Did) []const u8 { 104 return self.raw; 105 } 106 107 fn isValidIdentifier(id: []const u8) bool { 108 for (id) |c| { 109 const valid = switch (c) { 110 'a'...'z', 'A'...'Z', '0'...'9' => true, 111 '.', '_', ':', '-', '%' => true, 112 // explicitly reject invalid chars 113 '/', '?', '#', '[', ']', '@' => false, 114 else => false, 115 }; 116 if (!valid) return false; 117 } 118 return true; 119 } 120}; 121 122// === tests from atproto.com/specs/did === 123 124test "valid: did:plc example" { 125 const did = Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur") orelse return error.InvalidDid; 126 try std.testing.expect(did.isPlc()); 127 try std.testing.expectEqualStrings("plc", did.methodStr()); 128 try std.testing.expectEqualStrings("z72i7hdynmk6r22z27h6tvur", did.identifier()); 129} 130 131test "valid: did:web example" { 132 const did = Did.parse("did:web:blueskyweb.xyz") orelse return error.InvalidDid; 133 try std.testing.expect(did.isWeb()); 134 try std.testing.expectEqualStrings("web", did.methodStr()); 135 try std.testing.expectEqualStrings("blueskyweb.xyz", did.identifier()); 136} 137 138test "valid: did:web with port" { 139 const did = Did.parse("did:web:localhost%3A8080") orelse return error.InvalidDid; 140 try std.testing.expect(did.isWeb()); 141 try std.testing.expectEqualStrings("localhost%3A8080", did.identifier()); 142} 143 144test "valid: other method" { 145 const did = Did.parse("did:example:123456") orelse return error.InvalidDid; 146 try std.testing.expect(did.method() == .other); 147 try std.testing.expectEqualStrings("example", did.methodStr()); 148} 149 150test "valid: identifier with allowed special chars" { 151 try std.testing.expect(Did.parse("did:plc:abc.def") != null); 152 try std.testing.expect(Did.parse("did:plc:abc_def") != null); 153 try std.testing.expect(Did.parse("did:plc:abc:def") != null); 154 try std.testing.expect(Did.parse("did:plc:abc-def") != null); 155 try std.testing.expect(Did.parse("did:plc:abc%20def") != null); 156} 157 158test "invalid: missing prefix" { 159 try std.testing.expect(Did.parse("plc:xyz") == null); 160 try std.testing.expect(Did.parse("xyz") == null); 161} 162 163test "invalid: uppercase method" { 164 try std.testing.expect(Did.parse("did:PLC:xyz") == null); 165 try std.testing.expect(Did.parse("did:METHOD:val") == null); 166} 167 168test "invalid: empty method" { 169 try std.testing.expect(Did.parse("did::xyz") == null); 170} 171 172test "invalid: empty identifier" { 173 try std.testing.expect(Did.parse("did:plc:") == null); 174 try std.testing.expect(Did.parse("did:web:") == null); 175} 176 177test "invalid: ends with colon or percent" { 178 try std.testing.expect(Did.parse("did:plc:abc:") == null); 179 try std.testing.expect(Did.parse("did:plc:abc%") == null); 180} 181 182test "invalid: contains forbidden chars" { 183 try std.testing.expect(Did.parse("did:plc:abc/def") == null); 184 try std.testing.expect(Did.parse("did:plc:abc?def") == null); 185 try std.testing.expect(Did.parse("did:plc:abc#def") == null); 186 try std.testing.expect(Did.parse("did:plc:abc[def") == null); 187 try std.testing.expect(Did.parse("did:plc:abc]def") == null); 188 try std.testing.expect(Did.parse("did:plc:abc@def") == null); 189} 190 191test "invalid: too long" { 192 // create a did longer than 2048 chars 193 var buf: [2100]u8 = undefined; 194 @memset(&buf, 'a'); 195 @memcpy(buf[0..8], "did:plc:"); 196 try std.testing.expect(Did.parse(&buf) == null); 197}