atproto utils for zig
zat.dev
atproto
sdk
zig
1const std = @import("std");
2const cbor = @import("cbor.zig");
3const readArg = cbor.readArg;
4const Arg = cbor.Arg;
5const readText = cbor.readText;
6const readBytes = cbor.readBytes;
7const readUint = cbor.readUint;
8const readInt = cbor.readInt;
9const readBool = cbor.readBool;
10const readNull = cbor.readNull;
11const readMapHeader = cbor.readMapHeader;
12const readArrayHeader = cbor.readArrayHeader;
13const readCidLink = cbor.readCidLink;
14
15// ---------------------------------------------------------------------------
16// Inline values 0-23 (major type 0 = unsigned)
17// ---------------------------------------------------------------------------
18
19test "readArg: inline value 0" {
20 const data = [_]u8{0x00}; // major 0, additional 0
21 const arg = try readArg(&data, 0);
22 try std.testing.expectEqual(@as(u3, 0), arg.major);
23 try std.testing.expectEqual(@as(u64, 0), arg.val);
24 try std.testing.expectEqual(@as(usize, 1), arg.end);
25}
26
27test "readArg: inline value 1" {
28 const data = [_]u8{0x01};
29 const arg = try readArg(&data, 0);
30 try std.testing.expectEqual(@as(u3, 0), arg.major);
31 try std.testing.expectEqual(@as(u64, 1), arg.val);
32 try std.testing.expectEqual(@as(usize, 1), arg.end);
33}
34
35test "readArg: inline value 23" {
36 const data = [_]u8{0x17}; // major 0, additional 23
37 const arg = try readArg(&data, 0);
38 try std.testing.expectEqual(@as(u3, 0), arg.major);
39 try std.testing.expectEqual(@as(u64, 23), arg.val);
40 try std.testing.expectEqual(@as(usize, 1), arg.end);
41}
42
43// ---------------------------------------------------------------------------
44// 1-byte value (additional info = 24)
45// ---------------------------------------------------------------------------
46
47test "readArg: 1-byte value 24" {
48 const data = [_]u8{ 0x18, 24 }; // major 0, additional 24, payload 24
49 const arg = try readArg(&data, 0);
50 try std.testing.expectEqual(@as(u3, 0), arg.major);
51 try std.testing.expectEqual(@as(u64, 24), arg.val);
52 try std.testing.expectEqual(@as(usize, 2), arg.end);
53}
54
55test "readArg: 1-byte value 255" {
56 const data = [_]u8{ 0x18, 0xff }; // major 0, additional 24, payload 255
57 const arg = try readArg(&data, 0);
58 try std.testing.expectEqual(@as(u3, 0), arg.major);
59 try std.testing.expectEqual(@as(u64, 255), arg.val);
60 try std.testing.expectEqual(@as(usize, 2), arg.end);
61}
62
63// ---------------------------------------------------------------------------
64// 2-byte value (additional info = 25)
65// ---------------------------------------------------------------------------
66
67test "readArg: 2-byte value 256" {
68 const data = [_]u8{ 0x19, 0x01, 0x00 }; // major 0, additional 25, payload 256 big-endian
69 const arg = try readArg(&data, 0);
70 try std.testing.expectEqual(@as(u3, 0), arg.major);
71 try std.testing.expectEqual(@as(u64, 256), arg.val);
72 try std.testing.expectEqual(@as(usize, 3), arg.end);
73}
74
75// ---------------------------------------------------------------------------
76// 4-byte value (additional info = 26)
77// ---------------------------------------------------------------------------
78
79test "readArg: 4-byte value 65536" {
80 const data = [_]u8{ 0x1a, 0x00, 0x01, 0x00, 0x00 }; // major 0, additional 26, payload 65536
81 const arg = try readArg(&data, 0);
82 try std.testing.expectEqual(@as(u3, 0), arg.major);
83 try std.testing.expectEqual(@as(u64, 65536), arg.val);
84 try std.testing.expectEqual(@as(usize, 5), arg.end);
85}
86
87// ---------------------------------------------------------------------------
88// 8-byte value (additional info = 27)
89// ---------------------------------------------------------------------------
90
91test "readArg: 8-byte value 0x100000000" {
92 // major 0, additional 27, payload 0x00_00_00_01_00_00_00_00
93 const data = [_]u8{ 0x1b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 };
94 const arg = try readArg(&data, 0);
95 try std.testing.expectEqual(@as(u3, 0), arg.major);
96 try std.testing.expectEqual(@as(u64, 0x100000000), arg.val);
97 try std.testing.expectEqual(@as(usize, 9), arg.end);
98}
99
100// ---------------------------------------------------------------------------
101// Reject non-minimal encodings
102// ---------------------------------------------------------------------------
103
104test "readArg: reject non-minimal 0 encoded as 1-byte" {
105 // Value 0 encoded with additional=24 payload=0x00 (should be inline 0)
106 const data = [_]u8{ 0x18, 0x00 };
107 try std.testing.expectError(error.NonMinimalEncoding, readArg(&data, 0));
108}
109
110test "readArg: reject non-minimal 255 encoded as 2-byte" {
111 // Value 255 encoded with additional=25 payload=0x00ff (should be 1-byte)
112 const data = [_]u8{ 0x19, 0x00, 0xff };
113 try std.testing.expectError(error.NonMinimalEncoding, readArg(&data, 0));
114}
115
116// ---------------------------------------------------------------------------
117// Reject truncated data
118// ---------------------------------------------------------------------------
119
120test "readArg: reject truncated 2-byte" {
121 // additional=25 needs 2 payload bytes, but only 1 provided
122 const data = [_]u8{ 0x19, 0x01 };
123 try std.testing.expectError(error.UnexpectedEof, readArg(&data, 0));
124}
125
126// ---------------------------------------------------------------------------
127// Reject reserved additional info (28-30)
128// ---------------------------------------------------------------------------
129
130test "readArg: reject reserved additional info 28" {
131 const data = [_]u8{0x1c}; // major 0, additional 28
132 try std.testing.expectError(error.ReservedAdditionalInfo, readArg(&data, 0));
133}
134
135// ---------------------------------------------------------------------------
136// Reject indefinite length (additional info = 31)
137// ---------------------------------------------------------------------------
138
139test "readArg: reject indefinite length 31" {
140 const data = [_]u8{0x5f}; // major 2 (byte string), additional 31
141 try std.testing.expectError(error.IndefiniteLength, readArg(&data, 0));
142}
143
144// ---------------------------------------------------------------------------
145// Non-zero start position
146// ---------------------------------------------------------------------------
147
148test "readArg: non-zero start position" {
149 // prefix byte 0xAA, then a valid CBOR unsigned 24 at position 1
150 const data = [_]u8{ 0xaa, 0x18, 24 };
151 const arg = try readArg(&data, 1);
152 try std.testing.expectEqual(@as(u3, 0), arg.major);
153 try std.testing.expectEqual(@as(u64, 24), arg.val);
154 try std.testing.expectEqual(@as(usize, 3), arg.end);
155}
156
157test "readArg: non-zero start position with different major type" {
158 // At position 2: 0x63 = major 3 (text string), additional 3 (inline length 3)
159 const data = [_]u8{ 0x00, 0x00, 0x63, 0x66, 0x6f, 0x6f };
160 const arg = try readArg(&data, 2);
161 try std.testing.expectEqual(@as(u3, 3), arg.major);
162 try std.testing.expectEqual(@as(u64, 3), arg.val);
163 try std.testing.expectEqual(@as(usize, 3), arg.end);
164}
165
166// ---------------------------------------------------------------------------
167// EOF at start position
168// ---------------------------------------------------------------------------
169
170test "readArg: empty data returns UnexpectedEof" {
171 const data = [_]u8{};
172 try std.testing.expectError(error.UnexpectedEof, readArg(&data, 0));
173}
174
175test "readArg: pos beyond data returns UnexpectedEof" {
176 const data = [_]u8{0x00};
177 try std.testing.expectError(error.UnexpectedEof, readArg(&data, 1));
178}
179
180// ===========================================================================
181// readText
182// ===========================================================================
183
184test "readText: short string 'hello'" {
185 // 0x65 = major 3 (text), length 5
186 const data = [_]u8{ 0x65, 'h', 'e', 'l', 'l', 'o' };
187 const result = try readText(&data, 0);
188 try std.testing.expectEqualStrings("hello", result.val);
189 try std.testing.expectEqual(@as(usize, 6), result.end);
190}
191
192test "readText: empty string" {
193 const data = [_]u8{0x60}; // major 3, length 0
194 const result = try readText(&data, 0);
195 try std.testing.expectEqual(@as(usize, 0), result.val.len);
196 try std.testing.expectEqual(@as(usize, 1), result.end);
197}
198
199test "readText: reject non-text major" {
200 const data = [_]u8{ 0x45, 'h', 'e', 'l', 'l', 'o' }; // major 2 (bytes), length 5
201 try std.testing.expectError(error.WrongType, readText(&data, 0));
202}
203
204test "readText: reject invalid UTF-8" {
205 // 0x62 = major 3, length 2; 0xff 0xfe is invalid UTF-8
206 const data = [_]u8{ 0x62, 0xff, 0xfe };
207 try std.testing.expectError(error.InvalidUtf8, readText(&data, 0));
208}
209
210// ===========================================================================
211// readBytes
212// ===========================================================================
213
214test "readBytes: 3-byte string" {
215 const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2, length 3
216 const result = try readBytes(&data, 0);
217 try std.testing.expectEqual(@as(usize, 3), result.val.len);
218 try std.testing.expectEqual(@as(u8, 0x01), result.val[0]);
219 try std.testing.expectEqual(@as(u8, 0x02), result.val[1]);
220 try std.testing.expectEqual(@as(u8, 0x03), result.val[2]);
221 try std.testing.expectEqual(@as(usize, 4), result.end);
222}
223
224test "readBytes: empty bytes" {
225 const data = [_]u8{0x40}; // major 2, length 0
226 const result = try readBytes(&data, 0);
227 try std.testing.expectEqual(@as(usize, 0), result.val.len);
228 try std.testing.expectEqual(@as(usize, 1), result.end);
229}
230
231// ===========================================================================
232// readUint
233// ===========================================================================
234
235test "readUint: value 1000" {
236 // 0x19 = major 0, additional 25 (2-byte), 0x03e8 = 1000
237 const data = [_]u8{ 0x19, 0x03, 0xe8 };
238 const result = try readUint(&data, 0);
239 try std.testing.expectEqual(@as(u64, 1000), result.val);
240 try std.testing.expectEqual(@as(usize, 3), result.end);
241}
242
243test "readUint: reject negative" {
244 const data = [_]u8{0x20}; // major 1, value 0 => -1
245 try std.testing.expectError(error.WrongType, readUint(&data, 0));
246}
247
248// ===========================================================================
249// readInt
250// ===========================================================================
251
252test "readInt: positive 42" {
253 // 0x18 = major 0, additional 24 (1-byte), 42
254 const data = [_]u8{ 0x18, 42 };
255 const result = try readInt(&data, 0);
256 try std.testing.expectEqual(@as(i64, 42), result.val);
257 try std.testing.expectEqual(@as(usize, 2), result.end);
258}
259
260test "readInt: negative -10" {
261 // major 1, value 9 => -1 - 9 = -10
262 // 0x29 = 0b001_01001 = major 1, additional 9
263 const data = [_]u8{0x29};
264 const result = try readInt(&data, 0);
265 try std.testing.expectEqual(@as(i64, -10), result.val);
266 try std.testing.expectEqual(@as(usize, 1), result.end);
267}
268
269test "readInt: reject non-integer" {
270 const data = [_]u8{0x60}; // major 3 (text), length 0
271 try std.testing.expectError(error.WrongType, readInt(&data, 0));
272}
273
274// ===========================================================================
275// readBool
276// ===========================================================================
277
278test "readBool: true" {
279 const data = [_]u8{0xf5};
280 const result = try readBool(&data, 0);
281 try std.testing.expectEqual(true, result.val);
282 try std.testing.expectEqual(@as(usize, 1), result.end);
283}
284
285test "readBool: false" {
286 const data = [_]u8{0xf4};
287 const result = try readBool(&data, 0);
288 try std.testing.expectEqual(false, result.val);
289 try std.testing.expectEqual(@as(usize, 1), result.end);
290}
291
292test "readBool: reject non-bool" {
293 const data = [_]u8{0xf6}; // null
294 try std.testing.expectError(error.WrongType, readBool(&data, 0));
295}
296
297// ===========================================================================
298// readNull
299// ===========================================================================
300
301test "readNull: null" {
302 const data = [_]u8{0xf6};
303 const result = try readNull(&data, 0);
304 try std.testing.expectEqual(@as(usize, 1), result);
305}
306
307test "readNull: reject non-null" {
308 const data = [_]u8{0xf5}; // true
309 try std.testing.expectError(error.WrongType, readNull(&data, 0));
310}
311
312// ===========================================================================
313// readMapHeader
314// ===========================================================================
315
316test "readMapHeader: count 2" {
317 const data = [_]u8{0xa2}; // major 5, length 2
318 const result = try readMapHeader(&data, 0);
319 try std.testing.expectEqual(@as(u64, 2), result.val);
320 try std.testing.expectEqual(@as(usize, 1), result.end);
321}
322
323test "readMapHeader: reject non-map" {
324 const data = [_]u8{0x82}; // major 4 (array), length 2
325 try std.testing.expectError(error.WrongType, readMapHeader(&data, 0));
326}
327
328// ===========================================================================
329// readArrayHeader
330// ===========================================================================
331
332test "readArrayHeader: count 3" {
333 const data = [_]u8{0x83}; // major 4, length 3
334 const result = try readArrayHeader(&data, 0);
335 try std.testing.expectEqual(@as(u64, 3), result.val);
336 try std.testing.expectEqual(@as(usize, 1), result.end);
337}
338
339test "readArrayHeader: reject non-array" {
340 const data = [_]u8{0xa3}; // major 5 (map), length 3
341 try std.testing.expectError(error.WrongType, readArrayHeader(&data, 0));
342}
343
344// ===========================================================================
345// readCidLink
346// ===========================================================================
347
348test "readCidLink: valid CID (tag(42) + bytes with 0x00 prefix + 36-byte CIDv1)" {
349 // tag(42): 0xd8 0x2a
350 // bytes(37): 0x58 0x25 (37 = 1 prefix + 36 CID)
351 // 0x00 prefix
352 // 36-byte CID: 0x01 0x71 0x12 0x20 ++ [0xaa] ** 32
353 const cid_raw = [_]u8{ 0x01, 0x71, 0x12, 0x20 } ++ [_]u8{0xaa} ** 32;
354 const data = [_]u8{ 0xd8, 0x2a, 0x58, 0x25, 0x00 } ++ cid_raw;
355 const result = try readCidLink(&data, 0);
356 try std.testing.expectEqual(@as(usize, 36), result.val.len);
357 try std.testing.expectEqual(@as(u8, 0x01), result.val[0]);
358 try std.testing.expectEqual(@as(u8, 0x71), result.val[1]);
359 try std.testing.expectEqual(@as(u8, 0x12), result.val[2]);
360 try std.testing.expectEqual(@as(u8, 0x20), result.val[3]);
361 try std.testing.expectEqual(@as(u8, 0xaa), result.val[4]);
362 try std.testing.expectEqual(@as(usize, 4 + 37), result.end); // 4 header bytes (2 tag + 2 bytes hdr) + 37 payload bytes
363}
364
365test "readCidLink: reject non-tag" {
366 const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2 (bytes), not a tag
367 try std.testing.expectError(error.WrongType, readCidLink(&data, 0));
368}
369
370// ===========================================================================
371// skipValue
372// ===========================================================================
373
374const skipValue = cbor.skipValue;
375const peekType = cbor.peekType;
376const peekTypeAt = cbor.peekTypeAt;
377const encodeAlloc = cbor.encodeAlloc;
378const Value = cbor.Value;
379
380test "skipValue: unsigned integer (1 byte)" {
381 // 0x05 = major 0, value 5
382 const data = [_]u8{0x05};
383 const end = try skipValue(&data, 0);
384 try std.testing.expectEqual(@as(usize, 1), end);
385}
386
387test "skipValue: text string (header + payload)" {
388 // 0x65 = major 3, length 5 + "hello"
389 const data = [_]u8{ 0x65, 'h', 'e', 'l', 'l', 'o' };
390 const end = try skipValue(&data, 0);
391 try std.testing.expectEqual(@as(usize, 6), end);
392}
393
394test "skipValue: nested map {\"a\": [1, 2]}" {
395 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
396 defer arena.deinit();
397 const alloc = arena.allocator();
398
399 // Build {"a": [1, 2]} using the encoder
400 const val = Value{ .map = &.{
401 .{ .key = "a", .value = .{ .array = &.{
402 .{ .unsigned = 1 },
403 .{ .unsigned = 2 },
404 } } },
405 } };
406 const encoded = try encodeAlloc(alloc, val);
407 const end = try skipValue(encoded, 0);
408 try std.testing.expectEqual(encoded.len, end);
409}
410
411test "skipValue: CID link (tag 42 + byte string)" {
412 // tag(42): 0xd8 0x2a
413 // bytes(37): 0x58 0x25 (37 = 1 prefix + 36 CID)
414 // 0x00 prefix + 36-byte CID
415 const cid_raw = [_]u8{ 0x01, 0x71, 0x12, 0x20 } ++ [_]u8{0xaa} ** 32;
416 const data = [_]u8{ 0xd8, 0x2a, 0x58, 0x25, 0x00 } ++ cid_raw;
417 const end = try skipValue(&data, 0);
418 try std.testing.expectEqual(data.len, end);
419}
420
421test "skipValue: first of two concatenated values" {
422 // Two values: unsigned 5 (0x05) followed by unsigned 10 (0x0a)
423 const data = [_]u8{ 0x05, 0x0a };
424 const end = try skipValue(&data, 0);
425 try std.testing.expectEqual(@as(usize, 1), end);
426 // The second value starts at position 1
427 try std.testing.expectEqual(@as(u8, 0x0a), data[end]);
428}
429
430test "skipValue: complex record (encoded map)" {
431 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
432 defer arena.deinit();
433 const alloc = arena.allocator();
434
435 // Encode a realistic map with multiple types
436 const val = Value{ .map = &.{
437 .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } },
438 .{ .key = "createdAt", .value = .{ .text = "2024-01-01T00:00:00Z" } },
439 .{ .key = "text", .value = .{ .text = "hello world" } },
440 } };
441 const encoded = try encodeAlloc(alloc, val);
442 const end = try skipValue(encoded, 0);
443 try std.testing.expectEqual(encoded.len, end);
444}
445
446// ===========================================================================
447// peekType
448// ===========================================================================
449
450test "peekType: find $type when present" {
451 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
452 defer arena.deinit();
453 const alloc = arena.allocator();
454
455 const val = Value{ .map = &.{
456 .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } },
457 .{ .key = "text", .value = .{ .text = "hello" } },
458 } };
459 const encoded = try encodeAlloc(alloc, val);
460 const result = try peekType(encoded);
461 try std.testing.expect(result != null);
462 try std.testing.expectEqualStrings("app.bsky.feed.post", result.?);
463}
464
465test "peekType: find $type when not first key (DAG-CBOR sort order)" {
466 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
467 defer arena.deinit();
468 const alloc = arena.allocator();
469
470 // DAG-CBOR sorts by length then lex. Keys "ab" (2 bytes) sorts before
471 // "$type" (5 bytes), so "$type" won't be first.
472 const val = Value{ .map = &.{
473 .{ .key = "ab", .value = .{ .unsigned = 42 } },
474 .{ .key = "$type", .value = .{ .text = "app.bsky.graph.follow" } },
475 .{ .key = "zzzzzz", .value = .{ .boolean = true } },
476 } };
477 const encoded = try encodeAlloc(alloc, val);
478 const result = try peekType(encoded);
479 try std.testing.expect(result != null);
480 try std.testing.expectEqualStrings("app.bsky.graph.follow", result.?);
481}
482
483test "peekType: return null when no $type field" {
484 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
485 defer arena.deinit();
486 const alloc = arena.allocator();
487
488 const val = Value{ .map = &.{
489 .{ .key = "text", .value = .{ .text = "hello" } },
490 .{ .key = "count", .value = .{ .unsigned = 5 } },
491 } };
492 const encoded = try encodeAlloc(alloc, val);
493 const result = try peekType(encoded);
494 try std.testing.expect(result == null);
495}
496
497test "peekType: return null for non-map input" {
498 // An unsigned integer, not a map
499 const data = [_]u8{0x05};
500 const result = try peekType(&data);
501 try std.testing.expect(result == null);
502}