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 skipValue and peekType: zero-alloc CBOR navigation

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

jcalabro f1e62b57 4ccd0226

+231
+97
src/internal/repo/cbor.zig
··· 756 756 return .{ .val = payload[1..], .end = bytes_result.end }; 757 757 } 758 758 759 + // --------------------------------------------------------------------------- 760 + // Streaming helpers — skip / peek without full decode 761 + // --------------------------------------------------------------------------- 762 + 763 + /// Skip one CBOR value at `pos` without decoding it. Returns the position 764 + /// after the skipped value. Iterative (not recursive) using a small stack 765 + /// for nested containers. Zero allocation. 766 + pub fn skipValue(data: []const u8, pos: usize) DecodeError!usize { 767 + const max_stack = 32; 768 + var stack: [max_stack]u64 = undefined; 769 + var depth: usize = 0; 770 + var cur = pos; 771 + 772 + while (true) { 773 + const arg = try readArg(data, cur); 774 + cur = arg.end; 775 + 776 + switch (arg.major) { 777 + 0, 1 => { 778 + // integers: header only, nothing to skip after readArg 779 + }, 780 + 2, 3 => { 781 + // byte string / text string: skip `val` bytes of payload 782 + if (cur + arg.val > data.len) return error.UnexpectedEof; 783 + cur += @intCast(arg.val); 784 + }, 785 + 4 => { 786 + // array: push element count 787 + if (arg.val > 0) { 788 + if (depth >= max_stack) return error.MaxDepthExceeded; 789 + stack[depth] = arg.val; 790 + depth += 1; 791 + continue; // don't decrement — we haven't consumed an element yet 792 + } 793 + }, 794 + 5 => { 795 + // map: push key+value count (2 per entry) 796 + if (arg.val > 0) { 797 + if (depth >= max_stack) return error.MaxDepthExceeded; 798 + stack[depth] = arg.val * 2; 799 + depth += 1; 800 + continue; 801 + } 802 + }, 803 + 6 => { 804 + // tag: the tagged value follows immediately — loop to read it 805 + // don't push anything, don't decrement 806 + continue; 807 + }, 808 + 7 => { 809 + // simple/float: header only 810 + }, 811 + } 812 + 813 + // After consuming a value, unwind the stack 814 + while (depth > 0) { 815 + stack[depth - 1] -= 1; 816 + if (stack[depth - 1] > 0) break; 817 + depth -= 1; 818 + } 819 + 820 + if (depth == 0) return cur; 821 + } 822 + } 823 + 824 + /// Peek at the "$type" field in a DAG-CBOR map without full decode. 825 + /// Returns the type string (zero-copy slice) or null if not found. 826 + pub fn peekType(data: []const u8) DecodeError!?[]const u8 { 827 + return peekTypeAt(data, 0); 828 + } 829 + 830 + /// Peek at the "$type" field starting from a given position. 831 + pub fn peekTypeAt(data: []const u8, pos: usize) DecodeError!?[]const u8 { 832 + const map_header = try readArg(data, pos); 833 + if (map_header.major != 5) return null; 834 + 835 + var cur = map_header.end; 836 + const count = map_header.val; 837 + 838 + for (0..@as(usize, @intCast(count))) |_| { 839 + // Read key — DAG-CBOR keys are always text strings 840 + const key = readText(data, cur) catch return null; 841 + cur = key.end; 842 + 843 + if (std.mem.eql(u8, key.val, "$type")) { 844 + // Read the value as text 845 + const val = readText(data, cur) catch return null; 846 + return val.val; 847 + } 848 + 849 + // Skip the value 850 + cur = try skipValue(data, cur); 851 + } 852 + 853 + return null; 854 + } 855 + 759 856 // === tests === 760 857 761 858 test "decode unsigned integers" {
+134
src/internal/repo/cbor_read_test.zig
··· 366 366 const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2 (bytes), not a tag 367 367 try std.testing.expectError(error.WrongType, readCidLink(&data, 0)); 368 368 } 369 + 370 + // =========================================================================== 371 + // skipValue 372 + // =========================================================================== 373 + 374 + const skipValue = cbor.skipValue; 375 + const peekType = cbor.peekType; 376 + const peekTypeAt = cbor.peekTypeAt; 377 + const encodeAlloc = cbor.encodeAlloc; 378 + const Value = cbor.Value; 379 + 380 + test "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 + 387 + test "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 + 394 + test "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 + 411 + test "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 + 421 + test "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 + 430 + test "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 + 450 + test "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 + 465 + test "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 + 483 + test "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 + 497 + test "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 + }