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 184 lines 5.6 kB view raw
1//! Handle - AT Protocol Handle Identifier 2//! 3//! handles are domain-name based identifiers for accounts. 4//! format: <segment>.<segment>...<tld> 5//! 6//! validation rules: 7//! - max 253 characters 8//! - ASCII only (a-z, 0-9, hyphen) 9//! - 2+ segments separated by dots 10//! - each segment: 1-63 chars, no leading/trailing hyphens 11//! - final segment (TLD) cannot start with a digit 12//! - case-insensitive, normalize to lowercase 13//! 14//! see: https://atproto.com/specs/handle 15 16const std = @import("std"); 17 18pub const Handle = struct { 19 /// the handle string (borrowed, not owned) 20 raw: []const u8, 21 22 pub const max_length = 253; 23 24 /// parse a handle string. returns null if invalid. 25 pub fn parse(s: []const u8) ?Handle { 26 if (!isValid(s)) return null; 27 return .{ .raw = s }; 28 } 29 30 /// validate a handle string without allocating 31 pub fn isValid(s: []const u8) bool { 32 // length check 33 if (s.len == 0 or s.len > max_length) return false; 34 35 // must be ascii 36 for (s) |c| { 37 if (c > 127) return false; 38 } 39 40 var segment_count: usize = 0; 41 var segment_start: usize = 0; 42 var last_segment_start: usize = 0; 43 44 for (s, 0..) |c, i| { 45 if (c == '.') { 46 const segment = s[segment_start..i]; 47 if (!isValidSegment(segment)) return false; 48 segment_count += 1; 49 last_segment_start = i + 1; 50 segment_start = i + 1; 51 } 52 } 53 54 // validate final segment (TLD) 55 const tld = s[last_segment_start..]; 56 if (!isValidSegment(tld)) return false; 57 if (!isValidTld(tld)) return false; 58 segment_count += 1; 59 60 // must have at least 2 segments 61 return segment_count >= 2; 62 } 63 64 /// get the handle string 65 pub fn str(self: Handle) []const u8 { 66 return self.raw; 67 } 68 69 fn isValidSegment(seg: []const u8) bool { 70 // 1-63 characters 71 if (seg.len == 0 or seg.len > 63) return false; 72 73 // cannot start or end with hyphen 74 if (seg[0] == '-' or seg[seg.len - 1] == '-') return false; 75 76 // only alphanumeric and hyphen 77 for (seg) |c| { 78 const valid = (c >= 'a' and c <= 'z') or 79 (c >= 'A' and c <= 'Z') or 80 (c >= '0' and c <= '9') or 81 c == '-'; 82 if (!valid) return false; 83 } 84 85 return true; 86 } 87 88 fn isValidTld(tld: []const u8) bool { 89 if (tld.len == 0) return false; 90 // TLD cannot start with a digit 91 const first = tld[0]; 92 return (first >= 'a' and first <= 'z') or (first >= 'A' and first <= 'Z'); 93 } 94}; 95 96// === tests from atproto.com/specs/handle === 97 98test "valid: simple handle" { 99 try std.testing.expect(Handle.parse("jay.bsky.social") != null); 100 try std.testing.expect(Handle.parse("alice.example.com") != null); 101} 102 103test "valid: two segments" { 104 try std.testing.expect(Handle.parse("example.com") != null); 105 try std.testing.expect(Handle.parse("test.org") != null); 106} 107 108test "valid: many segments" { 109 try std.testing.expect(Handle.parse("a.b.c.d.e.f") != null); 110} 111 112test "valid: with hyphens" { 113 try std.testing.expect(Handle.parse("my-name.example.com") != null); 114 try std.testing.expect(Handle.parse("test.my-domain.org") != null); 115} 116 117test "valid: with numbers" { 118 try std.testing.expect(Handle.parse("user123.example.com") != null); 119 try std.testing.expect(Handle.parse("123user.example.com") != null); 120} 121 122test "valid: uppercase (allowed, normalize to lowercase)" { 123 try std.testing.expect(Handle.parse("LOUD.example.com") != null); 124 try std.testing.expect(Handle.parse("Jay.Bsky.Social") != null); 125} 126 127test "invalid: single segment" { 128 try std.testing.expect(Handle.parse("example") == null); 129 try std.testing.expect(Handle.parse("localhost") == null); 130} 131 132test "invalid: TLD starts with digit" { 133 try std.testing.expect(Handle.parse("john.0") == null); 134 try std.testing.expect(Handle.parse("test.123") == null); 135} 136 137test "invalid: segment starts with hyphen" { 138 try std.testing.expect(Handle.parse("-test.example.com") == null); 139 try std.testing.expect(Handle.parse("test.-example.com") == null); 140} 141 142test "invalid: segment ends with hyphen" { 143 try std.testing.expect(Handle.parse("test-.example.com") == null); 144 try std.testing.expect(Handle.parse("test.example-.com") == null); 145} 146 147test "invalid: empty segment" { 148 try std.testing.expect(Handle.parse(".example.com") == null); 149 try std.testing.expect(Handle.parse("test..com") == null); 150 try std.testing.expect(Handle.parse("test.example.") == null); 151} 152 153test "invalid: trailing dot" { 154 try std.testing.expect(Handle.parse("example.com.") == null); 155} 156 157test "invalid: invalid characters" { 158 try std.testing.expect(Handle.parse("test_name.example.com") == null); 159 try std.testing.expect(Handle.parse("test@name.example.com") == null); 160 try std.testing.expect(Handle.parse("test name.example.com") == null); 161} 162 163test "invalid: non-ascii" { 164 try std.testing.expect(Handle.parse("tëst.example.com") == null); 165} 166 167test "invalid: too long" { 168 // create a handle longer than 253 chars 169 var buf: [300]u8 = undefined; 170 @memset(&buf, 'a'); 171 buf[100] = '.'; 172 buf[200] = '.'; 173 @memcpy(buf[201..204], "com"); 174 try std.testing.expect(Handle.parse(buf[0..254]) == null); 175} 176 177test "invalid: segment too long" { 178 // segment > 63 chars 179 var buf: [100]u8 = undefined; 180 @memset(&buf, 'a'); 181 buf[64] = '.'; 182 @memcpy(buf[65..68], "com"); 183 try std.testing.expect(Handle.parse(buf[0..68]) == null); 184}