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.

clean up CBOR module: remove dead Tag variant, add getCid, fix MST bounds check

Remove Value.Tag union variant and encoder arm — the decoder rejects all
non-42 tags so this was unreachable dead code. Add Value.getCid() helper
for consistency with getString/getInt/etc. Fix parseCid docstring that
incorrectly claimed validation. Guard MST prefix_len against exceeding
prev_key length to prevent panic on malformed data. Add 14 tests for Cid
method edge cases, readUvarint, getter null paths, and min-i64 round-trip.

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

jcalabro 8a4411c7 b9c0db12

+138 -11
+10 -11
src/internal/repo/cbor.zig
··· 34 34 text: []const u8, 35 35 array: []const Value, 36 36 map: []const MapEntry, 37 - tag: Tag, 38 37 boolean: bool, 39 38 null, 40 39 cid: Cid, ··· 42 41 pub const MapEntry = struct { 43 42 key: []const u8, // DAG-CBOR: keys are always text strings 44 43 value: Value, 45 - }; 46 - 47 - pub const Tag = struct { 48 - number: u64, 49 - content: *const Value, 50 44 }; 51 45 52 46 /// look up a key in a map value ··· 114 108 const v = self.get(key) orelse return null; 115 109 return switch (v) { 116 110 .array => |a| a, 111 + else => null, 112 + }; 113 + } 114 + 115 + /// get a CID from a map by key 116 + pub fn getCid(self: Value, key: []const u8) ?Cid { 117 + const v = self.get(key) orelse return null; 118 + return switch (v) { 119 + .cid => |c| c, 117 120 else => null, 118 121 }; 119 122 } ··· 409 412 } 410 413 411 414 /// wrap raw CID bytes (after removing the 0x00 multibase prefix) into a Cid. 412 - /// validates the structure is parseable but stores only the raw bytes. 415 + /// does not validate the CID structure — call version()/codec()/digest() to parse lazily. 413 416 pub fn parseCid(raw: []const u8) Cid { 414 417 return .{ .raw = raw }; 415 418 } ··· 505 508 try encode(allocator, writer, .{ .text = entry.key }); 506 509 try encode(allocator, writer, entry.value); 507 510 } 508 - }, 509 - .tag => |t| { 510 - try writeArgument(writer, 6, t.number); 511 - try encode(allocator, writer, t.content.*); 512 511 }, 513 512 .boolean => |b| try writer.writeByte(if (b) @as(u8, 0xf5) else @as(u8, 0xf4)), 514 513 .null => try writer.writeByte(0xf6),
+127
src/internal/repo/cbor_test.zig
··· 1051 1051 0x00, // just one byte 1052 1052 })); 1053 1053 } 1054 + 1055 + // === Cid method edge cases === 1056 + 1057 + test "Cid.version returns null for empty raw" { 1058 + const cid = Cid{ .raw = &.{} }; 1059 + try std.testing.expect(cid.version() == null); 1060 + try std.testing.expect(cid.codec() == null); 1061 + try std.testing.expect(cid.hashFn() == null); 1062 + try std.testing.expect(cid.digest() == null); 1063 + } 1064 + 1065 + test "Cid.version returns null for single byte" { 1066 + const cid = Cid{ .raw = &.{0x01} }; 1067 + try std.testing.expect(cid.version() == null); 1068 + } 1069 + 1070 + test "Cid.digest returns null for truncated CIDv0" { 1071 + // CIDv0 starts with 0x12 0x20 but needs 34 bytes total 1072 + const cid = Cid{ .raw = &.{ 0x12, 0x20, 0xaa, 0xbb } }; // only 4 bytes, need 34 1073 + try std.testing.expect(cid.version().? == 0); // version parses ok 1074 + try std.testing.expect(cid.digest() == null); // but digest is truncated 1075 + } 1076 + 1077 + test "Cid.digest returns correct slice for valid CIDv1" { 1078 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1079 + defer arena.deinit(); 1080 + 1081 + const cid = try Cid.forDagCbor(arena.allocator(), "test"); 1082 + const d = cid.digest().?; 1083 + try std.testing.expectEqual(@as(usize, 32), d.len); 1084 + // verify it's actually SHA-256 of "test" 1085 + const Sha256 = std.crypto.hash.sha2.Sha256; 1086 + var expected: [32]u8 = undefined; 1087 + Sha256.hash("test", &expected, .{}); 1088 + try std.testing.expectEqualSlices(u8, &expected, d); 1089 + } 1090 + 1091 + // === readUvarint edge cases === 1092 + 1093 + test "readUvarint returns null on empty input" { 1094 + var pos: usize = 0; 1095 + try std.testing.expect(cbor.readUvarint(&.{}, &pos) == null); 1096 + } 1097 + 1098 + test "readUvarint returns null on truncated continuation" { 1099 + // 0x80 = continuation bit set, needs more bytes 1100 + var pos: usize = 0; 1101 + try std.testing.expect(cbor.readUvarint(&.{0x80}, &pos) == null); 1102 + } 1103 + 1104 + test "readUvarint decodes max single byte (127)" { 1105 + var pos: usize = 0; 1106 + try std.testing.expectEqual(@as(u64, 127), cbor.readUvarint(&.{0x7f}, &pos).?); 1107 + try std.testing.expectEqual(@as(usize, 1), pos); 1108 + } 1109 + 1110 + test "readUvarint decodes multi-byte value" { 1111 + // 128 = 0x80 0x01 1112 + var pos: usize = 0; 1113 + try std.testing.expectEqual(@as(u64, 128), cbor.readUvarint(&.{ 0x80, 0x01 }, &pos).?); 1114 + try std.testing.expectEqual(@as(usize, 2), pos); 1115 + } 1116 + 1117 + // === Value getter edge cases === 1118 + 1119 + test "getUint returns null for negative value" { 1120 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1121 + defer arena.deinit(); 1122 + 1123 + const val: Value = .{ .map = &.{ 1124 + .{ .key = "n", .value = .{ .negative = -5 } }, 1125 + } }; 1126 + // -5 can't be represented as u64 1127 + try std.testing.expect(val.getUint("n") == null); 1128 + } 1129 + 1130 + test "getInt returns null for u64 > max i64" { 1131 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1132 + defer arena.deinit(); 1133 + 1134 + const val: Value = .{ .map = &.{ 1135 + .{ .key = "n", .value = .{ .unsigned = std.math.maxInt(u64) } }, 1136 + } }; 1137 + // max u64 can't be represented as i64 1138 + try std.testing.expect(val.getInt("n") == null); 1139 + } 1140 + 1141 + test "getCid returns CID for cid value" { 1142 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1143 + defer arena.deinit(); 1144 + const alloc = arena.allocator(); 1145 + 1146 + const cid = try Cid.forDagCbor(alloc, "data"); 1147 + const val: Value = .{ .map = &.{ 1148 + .{ .key = "link", .value = .{ .cid = cid } }, 1149 + } }; 1150 + 1151 + const got = val.getCid("link").?; 1152 + try std.testing.expectEqualSlices(u8, cid.raw, got.raw); 1153 + } 1154 + 1155 + test "getCid returns null for non-cid value" { 1156 + const val: Value = .{ .map = &.{ 1157 + .{ .key = "x", .value = .{ .unsigned = 42 } }, 1158 + } }; 1159 + try std.testing.expect(val.getCid("x") == null); 1160 + } 1161 + 1162 + test "get returns null for non-map value" { 1163 + const val: Value = .{ .unsigned = 42 }; 1164 + try std.testing.expect(val.get("anything") == null); 1165 + try std.testing.expect(val.getString("anything") == null); 1166 + try std.testing.expect(val.getInt("anything") == null); 1167 + } 1168 + 1169 + // === negative integer encode round-trip at min i64 === 1170 + 1171 + test "round-trip encode min i64" { 1172 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1173 + defer arena.deinit(); 1174 + const alloc = arena.allocator(); 1175 + 1176 + const min_i64: i64 = std.math.minInt(i64); 1177 + const encoded = try cbor.encodeAlloc(alloc, .{ .negative = min_i64 }); 1178 + const decoded = try cbor.decodeAll(alloc, encoded); 1179 + try std.testing.expectEqual(min_i64, decoded.negative); 1180 + }
+1
src/internal/repo/mst.zig
··· 493 493 var prev_key: []const u8 = ""; 494 494 for (data.entries) |entry_data| { 495 495 // reconstruct full key 496 + if (entry_data.prefix_len > prev_key.len) return error.InvalidMstNode; 496 497 const full_key = try allocator.alloc(u8, entry_data.prefix_len + entry_data.key_suffix.len); 497 498 if (entry_data.prefix_len > 0) { 498 499 @memcpy(full_key[0..entry_data.prefix_len], prev_key[0..entry_data.prefix_len]);