atproto utils for zig zat.dev
atproto sdk zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144//! TID - Timestamp Identifier//!//! tids encode a timestamp and clock id in a base32-sortable format.//! format: 13 characters using alphabet "234567abcdefghijklmnopqrstuvwxyz"//! - first char must be 2-7 (high bit 0x40 must be 0)//! - remaining chars encode 53-bit timestamp + 10-bit clock id//!//! the encoding is designed to be lexicographically sortable by time.//! see: https://atproto.com/specs/record-key#record-key-type-tid
const std = @import("std");
pub const Tid = struct { raw: [13]u8,
const alphabet = "234567abcdefghijklmnopqrstuvwxyz";
/// parse a tid string. returns null if invalid. pub fn parse(s: []const u8) ?Tid { if (s.len != 13) return null;
// first char high bit (0x40) must be 0, meaning only '2'-'7' allowed if (s[0] & 0x40 != 0) return null;
var result: Tid = undefined; for (s, 0..) |c, i| { if (charToValue(c) == null) return null; result.raw[i] = c; } return result; }
/// timestamp in microseconds since unix epoch pub fn timestamp(self: Tid) u64 { var ts: u64 = 0; for (self.raw[0..11]) |c| { const val = charToValue(c) orelse unreachable; ts = (ts << 5) | val; } return ts; }
/// clock identifier (lower 10 bits) pub fn clockId(self: Tid) u10 { var id: u10 = 0; for (self.raw[11..13]) |c| { const val: u10 = @intCast(charToValue(c) orelse unreachable); id = (id << 5) | val; } return id; }
/// generate tid from timestamp and clock id pub fn fromTimestamp(ts: u64, clock_id: u10) Tid { var result: Tid = undefined;
// encode timestamp (53 bits -> 11 chars) var t = ts; var i: usize = 11; while (i > 0) { i -= 1; result.raw[i] = alphabet[@intCast(t & 0x1f)]; t >>= 5; }
// encode clock id (10 bits -> 2 chars) var c: u10 = clock_id; i = 13; while (i > 11) { i -= 1; result.raw[i] = alphabet[@intCast(c & 0x1f)]; c >>= 5; }
return result; }
/// get the raw string representation pub fn str(self: *const Tid) []const u8 { return &self.raw; }
fn charToValue(c: u8) ?u5 { return switch (c) { '2'...'7' => @intCast(c - '2'), 'a'...'z' => @intCast(c - 'a' + 6), else => null, }; }};
test "parse valid tid" { // generate a valid tid and parse it back const generated = Tid.fromTimestamp(1704067200000000, 42); const tid = Tid.parse(generated.str()) orelse return error.InvalidTid; try std.testing.expectEqual(@as(u64, 1704067200000000), tid.timestamp()); try std.testing.expectEqual(@as(u10, 42), tid.clockId());}
test "reject invalid tid" { // wrong length try std.testing.expect(Tid.parse("abc") == null); try std.testing.expect(Tid.parse("") == null);
// invalid chars try std.testing.expect(Tid.parse("0000000000000") == null); try std.testing.expect(Tid.parse("1111111111111") == null);
// first char must be 2-7 (high bit 0x40 must be 0) try std.testing.expect(Tid.parse("a222222222222") == null); try std.testing.expect(Tid.parse("z222222222222") == null);}
test "roundtrip" { const ts: u64 = 1704067200000000; // 2024-01-01 00:00:00 UTC in microseconds const clock: u10 = 42;
const tid = Tid.fromTimestamp(ts, clock); try std.testing.expectEqual(ts, tid.timestamp()); try std.testing.expectEqual(clock, tid.clockId());}
test "valid first chars" { // first char must be 2-7 only try std.testing.expect(Tid.parse("2222222222222") != null); try std.testing.expect(Tid.parse("3222222222222") != null); try std.testing.expect(Tid.parse("4222222222222") != null); try std.testing.expect(Tid.parse("5222222222222") != null); try std.testing.expect(Tid.parse("6222222222222") != null); try std.testing.expect(Tid.parse("7222222222222") != null);}
test "all valid chars in non-first position" { // chars 2-7 and a-z are valid after first position try std.testing.expect(Tid.parse("2aaaaaaaaaaaa") != null); try std.testing.expect(Tid.parse("2zzzzzzzzzzzz") != null); try std.testing.expect(Tid.parse("2234567234567") != null);}
test "uppercase rejected" { try std.testing.expect(Tid.parse("2AAAAAAAAAAAA") == null); try std.testing.expect(Tid.parse("2AAAAAAAAaaaa") == null);}