atproto utils for zig zat.dev
atproto sdk zig
at main 4.6 kB view raw
1//! TID - Timestamp Identifier 2//! 3//! tids encode a timestamp and clock id in a base32-sortable format. 4//! format: 13 characters using alphabet "234567abcdefghijklmnopqrstuvwxyz" 5//! - first char must be 2-7 (high bit 0x40 must be 0) 6//! - remaining chars encode 53-bit timestamp + 10-bit clock id 7//! 8//! the encoding is designed to be lexicographically sortable by time. 9//! see: https://atproto.com/specs/record-key#record-key-type-tid 10 11const std = @import("std"); 12 13pub const Tid = struct { 14 raw: [13]u8, 15 16 const alphabet = "234567abcdefghijklmnopqrstuvwxyz"; 17 18 /// parse a tid string. returns null if invalid. 19 pub fn parse(s: []const u8) ?Tid { 20 if (s.len != 13) return null; 21 22 // first char high bit (0x40) must be 0, meaning only '2'-'7' allowed 23 if (s[0] & 0x40 != 0) return null; 24 25 var result: Tid = undefined; 26 for (s, 0..) |c, i| { 27 if (charToValue(c) == null) return null; 28 result.raw[i] = c; 29 } 30 return result; 31 } 32 33 /// timestamp in microseconds since unix epoch 34 pub fn timestamp(self: Tid) u64 { 35 var ts: u64 = 0; 36 for (self.raw[0..11]) |c| { 37 const val = charToValue(c) orelse unreachable; 38 ts = (ts << 5) | val; 39 } 40 return ts; 41 } 42 43 /// clock identifier (lower 10 bits) 44 pub fn clockId(self: Tid) u10 { 45 var id: u10 = 0; 46 for (self.raw[11..13]) |c| { 47 const val: u10 = @intCast(charToValue(c) orelse unreachable); 48 id = (id << 5) | val; 49 } 50 return id; 51 } 52 53 /// generate tid from timestamp and clock id 54 pub fn fromTimestamp(ts: u64, clock_id: u10) Tid { 55 var result: Tid = undefined; 56 57 // encode timestamp (53 bits -> 11 chars) 58 var t = ts; 59 var i: usize = 11; 60 while (i > 0) { 61 i -= 1; 62 result.raw[i] = alphabet[@intCast(t & 0x1f)]; 63 t >>= 5; 64 } 65 66 // encode clock id (10 bits -> 2 chars) 67 var c: u10 = clock_id; 68 i = 13; 69 while (i > 11) { 70 i -= 1; 71 result.raw[i] = alphabet[@intCast(c & 0x1f)]; 72 c >>= 5; 73 } 74 75 return result; 76 } 77 78 /// get the raw string representation 79 pub fn str(self: *const Tid) []const u8 { 80 return &self.raw; 81 } 82 83 fn charToValue(c: u8) ?u5 { 84 return switch (c) { 85 '2'...'7' => @intCast(c - '2'), 86 'a'...'z' => @intCast(c - 'a' + 6), 87 else => null, 88 }; 89 } 90}; 91 92test "parse valid tid" { 93 // generate a valid tid and parse it back 94 const generated = Tid.fromTimestamp(1704067200000000, 42); 95 const tid = Tid.parse(generated.str()) orelse return error.InvalidTid; 96 try std.testing.expectEqual(@as(u64, 1704067200000000), tid.timestamp()); 97 try std.testing.expectEqual(@as(u10, 42), tid.clockId()); 98} 99 100test "reject invalid tid" { 101 // wrong length 102 try std.testing.expect(Tid.parse("abc") == null); 103 try std.testing.expect(Tid.parse("") == null); 104 105 // invalid chars 106 try std.testing.expect(Tid.parse("0000000000000") == null); 107 try std.testing.expect(Tid.parse("1111111111111") == null); 108 109 // first char must be 2-7 (high bit 0x40 must be 0) 110 try std.testing.expect(Tid.parse("a222222222222") == null); 111 try std.testing.expect(Tid.parse("z222222222222") == null); 112} 113 114test "roundtrip" { 115 const ts: u64 = 1704067200000000; // 2024-01-01 00:00:00 UTC in microseconds 116 const clock: u10 = 42; 117 118 const tid = Tid.fromTimestamp(ts, clock); 119 try std.testing.expectEqual(ts, tid.timestamp()); 120 try std.testing.expectEqual(clock, tid.clockId()); 121} 122 123test "valid first chars" { 124 // first char must be 2-7 only 125 try std.testing.expect(Tid.parse("2222222222222") != null); 126 try std.testing.expect(Tid.parse("3222222222222") != null); 127 try std.testing.expect(Tid.parse("4222222222222") != null); 128 try std.testing.expect(Tid.parse("5222222222222") != null); 129 try std.testing.expect(Tid.parse("6222222222222") != null); 130 try std.testing.expect(Tid.parse("7222222222222") != null); 131} 132 133test "all valid chars in non-first position" { 134 // chars 2-7 and a-z are valid after first position 135 try std.testing.expect(Tid.parse("2aaaaaaaaaaaa") != null); 136 try std.testing.expect(Tid.parse("2zzzzzzzzzzzz") != null); 137 try std.testing.expect(Tid.parse("2234567234567") != null); 138} 139 140test "uppercase rejected" { 141 try std.testing.expect(Tid.parse("2AAAAAAAAAAAA") == null); 142 try std.testing.expect(Tid.parse("2AAAAAAAAaaaa") == null); 143}