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}