atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

add low-level type-specific readers: readText, readUint, readCidLink, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

jcalabro 4ccd0226 6e0d4068

+308
+109
src/internal/repo/cbor.zig
··· 647 647 return .{ .major = major, .val = val, .end = cur }; 648 648 } 649 649 650 + // --------------------------------------------------------------------------- 651 + // Type-specific readers — zero-copy, no allocator needed 652 + // --------------------------------------------------------------------------- 653 + 654 + pub const SliceResult = struct { val: []const u8, end: usize }; 655 + pub const U64Result = struct { val: u64, end: usize }; 656 + pub const I64Result = struct { val: i64, end: usize }; 657 + pub const BoolResult = struct { val: bool, end: usize }; 658 + 659 + /// Read a CBOR text string (major type 3) at `pos`. 660 + /// Validates UTF-8. Returns a zero-copy slice into `data`. 661 + pub fn readText(data: []const u8, pos: usize) DecodeError!SliceResult { 662 + const arg = try readArg(data, pos); 663 + if (arg.major != 3) return error.WrongType; 664 + const len = arg.val; 665 + if (arg.end + len > data.len) return error.UnexpectedEof; 666 + const text = data[arg.end..][0..len]; 667 + if (!std.unicode.utf8ValidateSlice(text)) return error.InvalidUtf8; 668 + return .{ .val = text, .end = arg.end + len }; 669 + } 670 + 671 + /// Read a CBOR byte string (major type 2) at `pos`. 672 + /// Returns a zero-copy slice into `data`. 673 + pub fn readBytes(data: []const u8, pos: usize) DecodeError!SliceResult { 674 + const arg = try readArg(data, pos); 675 + if (arg.major != 2) return error.WrongType; 676 + const len = arg.val; 677 + if (arg.end + len > data.len) return error.UnexpectedEof; 678 + return .{ .val = data[arg.end..][0..len], .end = arg.end + len }; 679 + } 680 + 681 + /// Read a CBOR unsigned integer (major type 0) at `pos`. 682 + pub fn readUint(data: []const u8, pos: usize) DecodeError!U64Result { 683 + const arg = try readArg(data, pos); 684 + if (arg.major != 0) return error.WrongType; 685 + return .{ .val = arg.val, .end = arg.end }; 686 + } 687 + 688 + /// Read a CBOR integer (major type 0 or 1) at `pos`. 689 + /// Major 0 = positive, major 1 = negative (-1 - val). 690 + /// Returns error.Overflow if a positive value exceeds maxInt(i64). 691 + pub fn readInt(data: []const u8, pos: usize) DecodeError!I64Result { 692 + const arg = try readArg(data, pos); 693 + switch (arg.major) { 694 + 0 => { 695 + if (arg.val > @as(u64, @intCast(std.math.maxInt(i64)))) return error.Overflow; 696 + return .{ .val = @intCast(arg.val), .end = arg.end }; 697 + }, 698 + 1 => { 699 + // CBOR negative: -1 - val 700 + // val can be 0..2^64-1, result is -1..-2^64 701 + // i64 can hold down to -2^63, so max raw val is 2^63 - 1 702 + if (arg.val > @as(u64, @intCast(std.math.maxInt(i64)))) return error.Overflow; 703 + return .{ .val = -1 - @as(i64, @intCast(arg.val)), .end = arg.end }; 704 + }, 705 + else => return error.WrongType, 706 + } 707 + } 708 + 709 + /// Read a CBOR boolean at `pos`. 710 + /// 0xf4 = false, 0xf5 = true. 711 + pub fn readBool(data: []const u8, pos: usize) DecodeError!BoolResult { 712 + if (pos >= data.len) return error.UnexpectedEof; 713 + return switch (data[pos]) { 714 + 0xf4 => .{ .val = false, .end = pos + 1 }, 715 + 0xf5 => .{ .val = true, .end = pos + 1 }, 716 + else => error.WrongType, 717 + }; 718 + } 719 + 720 + /// Read a CBOR null at `pos`. 721 + /// 0xf6 = null. Returns position after the null byte. 722 + pub fn readNull(data: []const u8, pos: usize) DecodeError!usize { 723 + if (pos >= data.len) return error.UnexpectedEof; 724 + if (data[pos] != 0xf6) return error.WrongType; 725 + return pos + 1; 726 + } 727 + 728 + /// Read a CBOR map header (major type 5) at `pos`. 729 + /// Returns the entry count. 730 + pub fn readMapHeader(data: []const u8, pos: usize) DecodeError!U64Result { 731 + const arg = try readArg(data, pos); 732 + if (arg.major != 5) return error.WrongType; 733 + return .{ .val = arg.val, .end = arg.end }; 734 + } 735 + 736 + /// Read a CBOR array header (major type 4) at `pos`. 737 + /// Returns the element count. 738 + pub fn readArrayHeader(data: []const u8, pos: usize) DecodeError!U64Result { 739 + const arg = try readArg(data, pos); 740 + if (arg.major != 4) return error.WrongType; 741 + return .{ .val = arg.val, .end = arg.end }; 742 + } 743 + 744 + /// Read a DAG-CBOR CID link at `pos`. 745 + /// Expects tag(42) followed by a byte string with a 0x00 identity multibase prefix. 746 + /// Returns the raw CID bytes (after the 0x00 prefix) as a zero-copy slice. 747 + pub fn readCidLink(data: []const u8, pos: usize) DecodeError!SliceResult { 748 + // Read the tag header — must be tag(42) 749 + const tag_arg = try readArg(data, pos); 750 + if (tag_arg.major != 6 or tag_arg.val != 42) return error.WrongType; 751 + // Read the inner byte string 752 + const bytes_result = try readBytes(data, tag_arg.end); 753 + const payload = bytes_result.val; 754 + // Must have at least the 0x00 prefix 755 + if (payload.len == 0 or payload[0] != 0x00) return error.InvalidCid; 756 + return .{ .val = payload[1..], .end = bytes_result.end }; 757 + } 758 + 650 759 // === tests === 651 760 652 761 test "decode unsigned integers" {
+199
src/internal/repo/cbor_read_test.zig
··· 2 2 const cbor = @import("cbor.zig"); 3 3 const readArg = cbor.readArg; 4 4 const Arg = cbor.Arg; 5 + const readText = cbor.readText; 6 + const readBytes = cbor.readBytes; 7 + const readUint = cbor.readUint; 8 + const readInt = cbor.readInt; 9 + const readBool = cbor.readBool; 10 + const readNull = cbor.readNull; 11 + const readMapHeader = cbor.readMapHeader; 12 + const readArrayHeader = cbor.readArrayHeader; 13 + const readCidLink = cbor.readCidLink; 5 14 6 15 // --------------------------------------------------------------------------- 7 16 // Inline values 0-23 (major type 0 = unsigned) ··· 167 176 const data = [_]u8{0x00}; 168 177 try std.testing.expectError(error.UnexpectedEof, readArg(&data, 1)); 169 178 } 179 + 180 + // =========================================================================== 181 + // readText 182 + // =========================================================================== 183 + 184 + test "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 + 192 + test "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 + 199 + test "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 + 204 + test "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 + 214 + test "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 + 224 + test "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 + 235 + test "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 + 243 + test "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 + 252 + test "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 + 260 + test "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 + 269 + test "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 + 278 + test "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 + 285 + test "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 + 292 + test "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 + 301 + test "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 + 307 + test "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 + 316 + test "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 + 323 + test "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 + 332 + test "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 + 339 + test "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 + 348 + test "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 + 365 + test "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 + }