diff --git a/src/internal/repo/cbor.zig b/src/internal/repo/cbor.zig index d50e845..b010803 100644 --- a/src/internal/repo/cbor.zig +++ b/src/internal/repo/cbor.zig @@ -647,6 +647,115 @@ pub fn readArg(data: []const u8, pos: usize) DecodeError!Arg { return .{ .major = major, .val = val, .end = cur }; } +// --------------------------------------------------------------------------- +// Type-specific readers — zero-copy, no allocator needed +// --------------------------------------------------------------------------- + +pub const SliceResult = struct { val: []const u8, end: usize }; +pub const U64Result = struct { val: u64, end: usize }; +pub const I64Result = struct { val: i64, end: usize }; +pub const BoolResult = struct { val: bool, end: usize }; + +/// Read a CBOR text string (major type 3) at `pos`. +/// Validates UTF-8. Returns a zero-copy slice into `data`. +pub fn readText(data: []const u8, pos: usize) DecodeError!SliceResult { + const arg = try readArg(data, pos); + if (arg.major != 3) return error.WrongType; + const len = arg.val; + if (arg.end + len > data.len) return error.UnexpectedEof; + const text = data[arg.end..][0..len]; + if (!std.unicode.utf8ValidateSlice(text)) return error.InvalidUtf8; + return .{ .val = text, .end = arg.end + len }; +} + +/// Read a CBOR byte string (major type 2) at `pos`. +/// Returns a zero-copy slice into `data`. +pub fn readBytes(data: []const u8, pos: usize) DecodeError!SliceResult { + const arg = try readArg(data, pos); + if (arg.major != 2) return error.WrongType; + const len = arg.val; + if (arg.end + len > data.len) return error.UnexpectedEof; + return .{ .val = data[arg.end..][0..len], .end = arg.end + len }; +} + +/// Read a CBOR unsigned integer (major type 0) at `pos`. +pub fn readUint(data: []const u8, pos: usize) DecodeError!U64Result { + const arg = try readArg(data, pos); + if (arg.major != 0) return error.WrongType; + return .{ .val = arg.val, .end = arg.end }; +} + +/// Read a CBOR integer (major type 0 or 1) at `pos`. +/// Major 0 = positive, major 1 = negative (-1 - val). +/// Returns error.Overflow if a positive value exceeds maxInt(i64). +pub fn readInt(data: []const u8, pos: usize) DecodeError!I64Result { + const arg = try readArg(data, pos); + switch (arg.major) { + 0 => { + if (arg.val > @as(u64, @intCast(std.math.maxInt(i64)))) return error.Overflow; + return .{ .val = @intCast(arg.val), .end = arg.end }; + }, + 1 => { + // CBOR negative: -1 - val + // val can be 0..2^64-1, result is -1..-2^64 + // i64 can hold down to -2^63, so max raw val is 2^63 - 1 + if (arg.val > @as(u64, @intCast(std.math.maxInt(i64)))) return error.Overflow; + return .{ .val = -1 - @as(i64, @intCast(arg.val)), .end = arg.end }; + }, + else => return error.WrongType, + } +} + +/// Read a CBOR boolean at `pos`. +/// 0xf4 = false, 0xf5 = true. +pub fn readBool(data: []const u8, pos: usize) DecodeError!BoolResult { + if (pos >= data.len) return error.UnexpectedEof; + return switch (data[pos]) { + 0xf4 => .{ .val = false, .end = pos + 1 }, + 0xf5 => .{ .val = true, .end = pos + 1 }, + else => error.WrongType, + }; +} + +/// Read a CBOR null at `pos`. +/// 0xf6 = null. Returns position after the null byte. +pub fn readNull(data: []const u8, pos: usize) DecodeError!usize { + if (pos >= data.len) return error.UnexpectedEof; + if (data[pos] != 0xf6) return error.WrongType; + return pos + 1; +} + +/// Read a CBOR map header (major type 5) at `pos`. +/// Returns the entry count. +pub fn readMapHeader(data: []const u8, pos: usize) DecodeError!U64Result { + const arg = try readArg(data, pos); + if (arg.major != 5) return error.WrongType; + return .{ .val = arg.val, .end = arg.end }; +} + +/// Read a CBOR array header (major type 4) at `pos`. +/// Returns the element count. +pub fn readArrayHeader(data: []const u8, pos: usize) DecodeError!U64Result { + const arg = try readArg(data, pos); + if (arg.major != 4) return error.WrongType; + return .{ .val = arg.val, .end = arg.end }; +} + +/// Read a DAG-CBOR CID link at `pos`. +/// Expects tag(42) followed by a byte string with a 0x00 identity multibase prefix. +/// Returns the raw CID bytes (after the 0x00 prefix) as a zero-copy slice. +pub fn readCidLink(data: []const u8, pos: usize) DecodeError!SliceResult { + // Read the tag header — must be tag(42) + const tag_arg = try readArg(data, pos); + if (tag_arg.major != 6 or tag_arg.val != 42) return error.WrongType; + // Read the inner byte string + const bytes_result = try readBytes(data, tag_arg.end); + const payload = bytes_result.val; + // Must have at least the 0x00 prefix + if (payload.len == 0 or payload[0] != 0x00) return error.InvalidCid; + return .{ .val = payload[1..], .end = bytes_result.end }; +} + // === tests === test "decode unsigned integers" { diff --git a/src/internal/repo/cbor_read_test.zig b/src/internal/repo/cbor_read_test.zig index 6e6639c..36d8ed3 100644 --- a/src/internal/repo/cbor_read_test.zig +++ b/src/internal/repo/cbor_read_test.zig @@ -2,6 +2,15 @@ const std = @import("std"); const cbor = @import("cbor.zig"); const readArg = cbor.readArg; const Arg = cbor.Arg; +const readText = cbor.readText; +const readBytes = cbor.readBytes; +const readUint = cbor.readUint; +const readInt = cbor.readInt; +const readBool = cbor.readBool; +const readNull = cbor.readNull; +const readMapHeader = cbor.readMapHeader; +const readArrayHeader = cbor.readArrayHeader; +const readCidLink = cbor.readCidLink; // --------------------------------------------------------------------------- // Inline values 0-23 (major type 0 = unsigned) @@ -167,3 +176,193 @@ test "readArg: pos beyond data returns UnexpectedEof" { const data = [_]u8{0x00}; try std.testing.expectError(error.UnexpectedEof, readArg(&data, 1)); } + +// =========================================================================== +// readText +// =========================================================================== + +test "readText: short string 'hello'" { + // 0x65 = major 3 (text), length 5 + const data = [_]u8{ 0x65, 'h', 'e', 'l', 'l', 'o' }; + const result = try readText(&data, 0); + try std.testing.expectEqualStrings("hello", result.val); + try std.testing.expectEqual(@as(usize, 6), result.end); +} + +test "readText: empty string" { + const data = [_]u8{0x60}; // major 3, length 0 + const result = try readText(&data, 0); + try std.testing.expectEqual(@as(usize, 0), result.val.len); + try std.testing.expectEqual(@as(usize, 1), result.end); +} + +test "readText: reject non-text major" { + const data = [_]u8{ 0x45, 'h', 'e', 'l', 'l', 'o' }; // major 2 (bytes), length 5 + try std.testing.expectError(error.WrongType, readText(&data, 0)); +} + +test "readText: reject invalid UTF-8" { + // 0x62 = major 3, length 2; 0xff 0xfe is invalid UTF-8 + const data = [_]u8{ 0x62, 0xff, 0xfe }; + try std.testing.expectError(error.InvalidUtf8, readText(&data, 0)); +} + +// =========================================================================== +// readBytes +// =========================================================================== + +test "readBytes: 3-byte string" { + const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2, length 3 + const result = try readBytes(&data, 0); + try std.testing.expectEqual(@as(usize, 3), result.val.len); + try std.testing.expectEqual(@as(u8, 0x01), result.val[0]); + try std.testing.expectEqual(@as(u8, 0x02), result.val[1]); + try std.testing.expectEqual(@as(u8, 0x03), result.val[2]); + try std.testing.expectEqual(@as(usize, 4), result.end); +} + +test "readBytes: empty bytes" { + const data = [_]u8{0x40}; // major 2, length 0 + const result = try readBytes(&data, 0); + try std.testing.expectEqual(@as(usize, 0), result.val.len); + try std.testing.expectEqual(@as(usize, 1), result.end); +} + +// =========================================================================== +// readUint +// =========================================================================== + +test "readUint: value 1000" { + // 0x19 = major 0, additional 25 (2-byte), 0x03e8 = 1000 + const data = [_]u8{ 0x19, 0x03, 0xe8 }; + const result = try readUint(&data, 0); + try std.testing.expectEqual(@as(u64, 1000), result.val); + try std.testing.expectEqual(@as(usize, 3), result.end); +} + +test "readUint: reject negative" { + const data = [_]u8{0x20}; // major 1, value 0 => -1 + try std.testing.expectError(error.WrongType, readUint(&data, 0)); +} + +// =========================================================================== +// readInt +// =========================================================================== + +test "readInt: positive 42" { + // 0x18 = major 0, additional 24 (1-byte), 42 + const data = [_]u8{ 0x18, 42 }; + const result = try readInt(&data, 0); + try std.testing.expectEqual(@as(i64, 42), result.val); + try std.testing.expectEqual(@as(usize, 2), result.end); +} + +test "readInt: negative -10" { + // major 1, value 9 => -1 - 9 = -10 + // 0x29 = 0b001_01001 = major 1, additional 9 + const data = [_]u8{0x29}; + const result = try readInt(&data, 0); + try std.testing.expectEqual(@as(i64, -10), result.val); + try std.testing.expectEqual(@as(usize, 1), result.end); +} + +test "readInt: reject non-integer" { + const data = [_]u8{0x60}; // major 3 (text), length 0 + try std.testing.expectError(error.WrongType, readInt(&data, 0)); +} + +// =========================================================================== +// readBool +// =========================================================================== + +test "readBool: true" { + const data = [_]u8{0xf5}; + const result = try readBool(&data, 0); + try std.testing.expectEqual(true, result.val); + try std.testing.expectEqual(@as(usize, 1), result.end); +} + +test "readBool: false" { + const data = [_]u8{0xf4}; + const result = try readBool(&data, 0); + try std.testing.expectEqual(false, result.val); + try std.testing.expectEqual(@as(usize, 1), result.end); +} + +test "readBool: reject non-bool" { + const data = [_]u8{0xf6}; // null + try std.testing.expectError(error.WrongType, readBool(&data, 0)); +} + +// =========================================================================== +// readNull +// =========================================================================== + +test "readNull: null" { + const data = [_]u8{0xf6}; + const result = try readNull(&data, 0); + try std.testing.expectEqual(@as(usize, 1), result); +} + +test "readNull: reject non-null" { + const data = [_]u8{0xf5}; // true + try std.testing.expectError(error.WrongType, readNull(&data, 0)); +} + +// =========================================================================== +// readMapHeader +// =========================================================================== + +test "readMapHeader: count 2" { + const data = [_]u8{0xa2}; // major 5, length 2 + const result = try readMapHeader(&data, 0); + try std.testing.expectEqual(@as(u64, 2), result.val); + try std.testing.expectEqual(@as(usize, 1), result.end); +} + +test "readMapHeader: reject non-map" { + const data = [_]u8{0x82}; // major 4 (array), length 2 + try std.testing.expectError(error.WrongType, readMapHeader(&data, 0)); +} + +// =========================================================================== +// readArrayHeader +// =========================================================================== + +test "readArrayHeader: count 3" { + const data = [_]u8{0x83}; // major 4, length 3 + const result = try readArrayHeader(&data, 0); + try std.testing.expectEqual(@as(u64, 3), result.val); + try std.testing.expectEqual(@as(usize, 1), result.end); +} + +test "readArrayHeader: reject non-array" { + const data = [_]u8{0xa3}; // major 5 (map), length 3 + try std.testing.expectError(error.WrongType, readArrayHeader(&data, 0)); +} + +// =========================================================================== +// readCidLink +// =========================================================================== + +test "readCidLink: valid CID (tag(42) + bytes with 0x00 prefix + 36-byte CIDv1)" { + // tag(42): 0xd8 0x2a + // bytes(37): 0x58 0x25 (37 = 1 prefix + 36 CID) + // 0x00 prefix + // 36-byte CID: 0x01 0x71 0x12 0x20 ++ [0xaa] ** 32 + const cid_raw = [_]u8{ 0x01, 0x71, 0x12, 0x20 } ++ [_]u8{0xaa} ** 32; + const data = [_]u8{ 0xd8, 0x2a, 0x58, 0x25, 0x00 } ++ cid_raw; + const result = try readCidLink(&data, 0); + try std.testing.expectEqual(@as(usize, 36), result.val.len); + try std.testing.expectEqual(@as(u8, 0x01), result.val[0]); + try std.testing.expectEqual(@as(u8, 0x71), result.val[1]); + try std.testing.expectEqual(@as(u8, 0x12), result.val[2]); + try std.testing.expectEqual(@as(u8, 0x20), result.val[3]); + try std.testing.expectEqual(@as(u8, 0xaa), result.val[4]); + try std.testing.expectEqual(@as(usize, 4 + 37), result.end); // 4 header bytes (2 tag + 2 bytes hdr) + 37 payload bytes +} + +test "readCidLink: reject non-tag" { + const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2 (bytes), not a tag + try std.testing.expectError(error.WrongType, readCidLink(&data, 0)); +}