atproto utils for zig zat.dev
atproto sdk zig
at main 3.5 kB view raw
1//! Record Key (rkey) 2//! 3//! record keys identify individual records within a collection. 4//! 5//! validation rules: 6//! - 1-512 characters 7//! - allowed chars: A-Z, a-z, 0-9, period, hyphen, underscore, colon, tilde 8//! - cannot be "." or ".." 9//! 10//! note: TIDs are a common rkey format but not the only valid one. 11//! see tid.zig for TID-specific parsing. 12//! 13//! see: https://atproto.com/specs/record-key 14 15const std = @import("std"); 16 17pub const Rkey = struct { 18 /// the rkey string (borrowed, not owned) 19 raw: []const u8, 20 21 pub const min_length = 1; 22 pub const max_length = 512; 23 24 /// parse a record key string. returns null if invalid. 25 pub fn parse(s: []const u8) ?Rkey { 26 if (!isValid(s)) return null; 27 return .{ .raw = s }; 28 } 29 30 /// validate a record key string 31 pub fn isValid(s: []const u8) bool { 32 // length check 33 if (s.len < min_length or s.len > max_length) return false; 34 35 // cannot be "." or ".." 36 if (std.mem.eql(u8, s, ".") or std.mem.eql(u8, s, "..")) return false; 37 38 // check all characters are valid 39 for (s) |c| { 40 const valid = switch (c) { 41 'A'...'Z', 'a'...'z', '0'...'9' => true, 42 '.', '-', '_', ':', '~' => true, 43 else => false, 44 }; 45 if (!valid) return false; 46 } 47 48 return true; 49 } 50 51 /// get the rkey string 52 pub fn str(self: Rkey) []const u8 { 53 return self.raw; 54 } 55}; 56 57// === tests from atproto.com/specs/record-key === 58 59test "valid: simple rkey" { 60 try std.testing.expect(Rkey.parse("abc123") != null); 61 try std.testing.expect(Rkey.parse("self") != null); 62} 63 64test "valid: tid format" { 65 try std.testing.expect(Rkey.parse("3jxtb5w2hkt2m") != null); 66} 67 68test "valid: with allowed special chars" { 69 try std.testing.expect(Rkey.parse("abc.def") != null); 70 try std.testing.expect(Rkey.parse("abc-def") != null); 71 try std.testing.expect(Rkey.parse("abc_def") != null); 72 try std.testing.expect(Rkey.parse("abc:def") != null); 73 try std.testing.expect(Rkey.parse("abc~def") != null); 74} 75 76test "valid: mixed case" { 77 try std.testing.expect(Rkey.parse("AbC123") != null); 78 try std.testing.expect(Rkey.parse("ABC") != null); 79} 80 81test "valid: single character" { 82 try std.testing.expect(Rkey.parse("a") != null); 83 try std.testing.expect(Rkey.parse("1") != null); 84} 85 86test "valid: max length" { 87 var buf: [512]u8 = undefined; 88 @memset(&buf, 'a'); 89 try std.testing.expect(Rkey.parse(&buf) != null); 90} 91 92test "invalid: empty" { 93 try std.testing.expect(Rkey.parse("") == null); 94} 95 96test "invalid: dot" { 97 try std.testing.expect(Rkey.parse(".") == null); 98} 99 100test "invalid: double dot" { 101 try std.testing.expect(Rkey.parse("..") == null); 102} 103 104test "invalid: too long" { 105 var buf: [513]u8 = undefined; 106 @memset(&buf, 'a'); 107 try std.testing.expect(Rkey.parse(&buf) == null); 108} 109 110test "invalid: forbidden characters" { 111 try std.testing.expect(Rkey.parse("abc/def") == null); 112 try std.testing.expect(Rkey.parse("abc?def") == null); 113 try std.testing.expect(Rkey.parse("abc#def") == null); 114 try std.testing.expect(Rkey.parse("abc@def") == null); 115 try std.testing.expect(Rkey.parse("abc def") == null); 116 try std.testing.expect(Rkey.parse("abc\ndef") == null); 117} 118 119test "invalid: non-ascii" { 120 var buf = "abcdef".*; 121 buf[2] = 200; // non-ascii byte 122 try std.testing.expect(Rkey.parse(&buf) == null); 123}