diff --git a/README.md b/README.md index 57999f2..e7954df 100644 --- a/README.md +++ b/README.md @@ -395,6 +395,10 @@ std.debug.print("As float: {d}\n", .{ decoded_ts.timestamp.toFloat() }); ### Error Handling +Decoding rejects the reserved marker `0xc1` with `MsgPackError.TypeMarkerReading`, +including in array elements and map keys/values. Only `0xc0` encodes `nil`. +This restriction applies to type markers, not bytes inside data or length fields. + ```zig // Type conversion with error handling const int_payload = msgpack.Payload.intToPayload(-42); @@ -526,6 +530,7 @@ This library uses an **iterative parser** (not recursive) to provide strong secu **Memory Safety:** +- `Payload.free(allocator)` releases owned data without allocating or recursing, even when the allocator is exhausted. - All error paths include complete cleanup (`errdefer` + `cleanupParseStack`) - Zero memory leaks verified by GPA (General Purpose Allocator) in tests - Safe to parse untrusted data from network, files, or user input diff --git a/README_CN.md b/README_CN.md index 62ea028..5adf3b8 100644 --- a/README_CN.md +++ b/README_CN.md @@ -303,6 +303,10 @@ std.debug.print("浮点数形式: {d}\n", .{ decoded_ts.timestamp.toFloat() }); ### 错误处理 +解码时,保留标记 `0xc1` 会返回 `MsgPackError.TypeMarkerReading`,包括数组元素和 +map 的键、值位置。只有 `0xc0` 表示 `nil`。此限制仅针对类型标记,不影响数据内容 +或长度字段中的同值字节。 + ```zig // 类型转换与错误处理 const int_payload = msgpack.Payload.intToPayload(-42); @@ -434,6 +438,7 @@ msgpack.MsgPackError.ExtDataTooLarge // 扩展类型数据过大 **内存安全**: +- `Payload.free(allocator)` 无需分配新内存或递归即可释放其拥有的数据,即使分配器已耗尽也能完成清理。 - 所有错误路径包含完整清理(`errdefer` + `cleanupParseStack`) - 零内存泄漏(测试中由 GPA 验证) - 可安全解析来自网络、文件或用户输入的不可信数据 diff --git a/src/msgpack.zig b/src/msgpack.zig index 48979a1..b3c66d4 100644 --- a/src/msgpack.zig +++ b/src/msgpack.zig @@ -1270,125 +1270,96 @@ pub const Payload = union(enum) { }; } - /// free all memory for this payload and sub payloads - /// the allocator is payload's allocator - /// This is an iterative implementation that avoids stack overflow from deep nesting - /// Optimization: Uses stack-allocated buffer for shallow structures to avoid heap allocation during free - pub fn free(self: Payload, allocator: Allocator) void { - // Use stack-allocated buffer for shallow structures (up to 256 items) - // This avoids heap allocation during memory cleanup for most common cases - const STACK_BUFFER_SIZE = 256; - var stack_buffer: [STACK_BUFFER_SIZE]Payload = undefined; - var stack_len: usize = 0; - - // Fallback to heap if we exceed stack buffer - var heap_stack: ?std.ArrayList(Payload) = null; - defer if (heap_stack) |*hs| { - if (current_zig.minor == 14) { - hs.deinit(); - } else { - hs.deinit(allocator); - } - }; + /// Intrusive worklist stored in an owned container's first Payload slot. + /// The displaced child is copied out before that slot is reused. + const FreeNode = struct { + next: ?*FreeNode, + container: union(enum) { + arr: []Payload, + map: PayloadHashMap, + }, - // Helper to push to stack (tries stack first, falls back to heap) - const pushPayload = struct { - fn push( - buffer: []Payload, - len: *usize, - heap: *?std.ArrayList(Payload), - alloc: Allocator, - payload: Payload, - ) void { - if (heap.*) |*h| { - // Already using heap - if (current_zig.minor == 14) { - h.append(payload) catch {}; - } else { - h.append(alloc, payload) catch {}; - } - } else if (len.* < buffer.len) { - // Stack buffer has space - buffer[len.*] = payload; - len.* += 1; - } else { - // Stack buffer full, migrate to heap - var new_heap = if (current_zig.minor == 14) - std.ArrayList(Payload).init(alloc) - else - std.ArrayList(Payload).empty; - - // Copy existing items from stack buffer to heap - for (buffer[0..len.*]) |item| { - if (current_zig.minor == 14) { - new_heap.append(item) catch return; - } else { - new_heap.append(alloc, item) catch return; + fn enqueue(payload: Payload, pending: *?*FreeNode, allocator: Allocator) void { + var current = payload; + while (true) { + switch (current) { + .str => |s| { + allocator.free(s.value()); + return; + }, + .bin => |b| { + allocator.free(b.value()); + return; + }, + .ext => |e| { + allocator.free(e.data); + return; + }, + .arr => |arr| { + if (arr.len == 0) { + allocator.free(arr); + return; } - } - // Add new item - if (current_zig.minor == 14) { - new_heap.append(payload) catch return; - } else { - new_heap.append(alloc, payload) catch return; - } - heap.* = new_heap; - len.* = 0; // Clear stack buffer - } - } - }.push; - - // Helper to pop from stack - const popPayload = struct { - fn pop( - buffer: []Payload, - len: *usize, - heap: *?std.ArrayList(Payload), - ) ?Payload { - if (heap.*) |*h| { - if (h.items.len > 0) { - return h.pop(); - } - } - if (len.* > 0) { - len.* -= 1; - return buffer[len.*]; + const child = arr[0]; + const node: *FreeNode = @ptrCast(&arr[0]); + node.* = .{ .next = pending.*, .container = .{ .arr = arr } }; + pending.* = node; + current = child; + }, + .map => |map| { + var table = map.map; + var it = table.iterator(); + const first = it.next() orelse { + table.deinit(); + return; + }; + const child = first.key_ptr.*; + const node: *FreeNode = @ptrCast(first.key_ptr); + node.* = .{ .next = pending.*, .container = .{ .map = table } }; + pending.* = node; + current = child; + }, + else => return, } - return null; } - }.pop; - - // Start with self - pushPayload(&stack_buffer, &stack_len, &heap_stack, allocator, self); - - while (popPayload(&stack_buffer, &stack_len, &heap_stack)) |payload_item| { - switch (payload_item) { - .str => |s| allocator.free(s.value()), - .bin => |b| allocator.free(b.value()), - .ext => |e| allocator.free(e.data), + } + }; + /// Free all owned memory without allocating or recursing. + /// The allocator must be the one used for this payload and its children. + pub fn free(self: Payload, allocator: Allocator) void { + comptime { + std.debug.assert(@sizeOf(FreeNode) <= @sizeOf(Payload)); + std.debug.assert(@alignOf(FreeNode) <= @alignOf(Payload)); + } + + var pending: ?*FreeNode = null; + FreeNode.enqueue(self, &pending, allocator); + while (pending) |node| { + // Copy the frame before freeing the container that stores it. + const frame = node.*; + pending = frame.next; + switch (frame.container) { .arr => |arr| { - defer allocator.free(arr); - // Push children to stack in reverse order - var i = arr.len; - while (i > 0) { - i -= 1; - pushPayload(&stack_buffer, &stack_len, &heap_stack, allocator, arr[i]); + // Element zero now holds the frame, not a Payload. + for (arr[1..]) |item| { + FreeNode.enqueue(item, &pending, allocator); } + allocator.free(arr); }, - .map => |map| { - var map_copy = map; - defer map_copy.deinit(); - // Push both keys and values to stack for recursive freeing - var it = map_copy.map.iterator(); + var table = map; + var it = table.iterator(); + // Only the first key was displaced. Its value still needs cleanup. + // Iteration uses occupancy metadata, never hashes the overwritten key. + const first = it.next().?; + FreeNode.enqueue(first.value_ptr.*, &pending, allocator); while (it.next()) |entry| { - pushPayload(&stack_buffer, &stack_len, &heap_stack, allocator, entry.key_ptr.*); - pushPayload(&stack_buffer, &stack_len, &heap_stack, allocator, entry.value_ptr.*); + FreeNode.enqueue(entry.key_ptr.*, &pending, allocator); + FreeNode.enqueue(entry.value_ptr.*, &pending, allocator); } + table.deinit(); }, - - else => {}, // nil, bool, int, uint, float, timestamp - no memory to free } } } @@ -2205,6 +2176,8 @@ pub fn PackWithLimits( fn readTypeMarkerU8(self: Self) !u8 { const val = try self.readByte(); + // The reserved byte is invalid only as a marker, not inside data. + if (val == 0xc1) return MsgPackError.TypeMarkerReading; return val; } @@ -2221,7 +2194,7 @@ pub fn PackWithLimits( 0x90...0x9f => .FIXARRAY, 0xa0...0xbf => .FIXSTR, 0xc0 => .NIL, - 0xc1 => .NIL, // Reserved byte, treat as NIL + 0xc1 => undefined, // Rejected by readTypeMarkerU8 before lookup. 0xc2 => .FALSE, 0xc3 => .TRUE, 0xc4 => .BIN8, diff --git a/src/test.zig b/src/test.zig index 179bc17..0d6f747 100644 --- a/src/test.zig +++ b/src/test.zig @@ -51,24 +51,234 @@ test "PackerIO: truncated data error" { } } -test "PackerIO: invalid msgpack marker" { +test "MessagePack spec: reserved marker is rejected at every value position" { if (!has_new_io) return error.SkipZigTest; - // 0xc1 is a reserved/invalid marker byte in MessagePack - var buffer: [10]u8 = [_]u8{ 0xc1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - var writer = std.Io.Writer.fixed(&buffer); - var reader = std.Io.Reader.fixed(&buffer); + // https://github.com/msgpack/msgpack/blob/master/spec.md#overview + // 0xc1 is "never used", not an alternative encoding of nil (0xc0). + const inputs = [_][]const u8{ + &.{0xc1}, + &.{ 0x91, 0xc1 }, // Array element. + &.{ 0x81, 0xc1, 0x01 }, // Map key. + &.{ 0x81, 0x01, 0xc1 }, // Map value. + &.{ 0x92, 0xa1, 'x', 0xc1 }, // Previously allocated array element. + &.{ 0x82, 0xa1, 'k', 0xa1, 'v', 0xc1, 0x00 }, // Completed map entry. + &.{ 0x81, 0xa1, 'k', 0xc1 }, // Allocated key waiting for its value. + &.{ 0x92, 0x91, 0xa1, 'x', 0x81, 0xa1, 'k', 0x91, 0xc1 }, // Mixed nesting. + }; + for (inputs) |input| { + var output: [0]u8 = .{}; + var writer = std.Io.Writer.fixed(&output); + var reader = std.Io.Reader.fixed(input); + var packer = msgpack.PackerIO.init(&reader, &writer); + const result = packer.read(allocator); + // Free unexpected successes too, so pre-fix failures do not leak. + if (result) |payload| payload.free(allocator) else |_| {} + try std.testing.expectError(msgpack.MsgPackError.TypeMarkerReading, result); + } +} + +test "MessagePack spec: scalar wire vectors" { + if (!has_new_io) return error.SkipZigTest; + + // Fixed wire vectors, independent of this library's encoder. + // https://github.com/msgpack/msgpack/blob/master/spec.md#formats + const Vector = struct { bytes: []const u8, value: Payload }; + const vectors = [_]Vector{ + .{ .bytes = "\xc0", .value = .{ .nil = {} } }, + .{ .bytes = "\xc2", .value = .{ .bool = false } }, + .{ .bytes = "\xc3", .value = .{ .bool = true } }, + .{ .bytes = "\x00", .value = .{ .uint = 0 } }, + .{ .bytes = "\x7f", .value = .{ .uint = 127 } }, + .{ .bytes = "\xcc\x80", .value = .{ .uint = 128 } }, + .{ .bytes = "\xcc\xc1", .value = .{ .uint = 193 } }, + .{ .bytes = "\xcd\x01\x00", .value = .{ .uint = 256 } }, + .{ .bytes = "\xce\x00\x01\x00\x00", .value = .{ .uint = 65536 } }, + .{ .bytes = "\xcf\xff\xff\xff\xff\xff\xff\xff\xff", .value = .{ .uint = std.math.maxInt(u64) } }, + .{ .bytes = "\xff", .value = .{ .int = -1 } }, + .{ .bytes = "\xe0", .value = .{ .int = -32 } }, + .{ .bytes = "\xd0\xdf", .value = .{ .int = -33 } }, + .{ .bytes = "\xd1\xff\x7f", .value = .{ .int = -129 } }, + .{ .bytes = "\xd2\xff\xff\x7f\xff", .value = .{ .int = -32769 } }, + .{ .bytes = "\xd3\x80\x00\x00\x00\x00\x00\x00\x00", .value = .{ .int = std.math.minInt(i64) } }, + .{ .bytes = "\xca\xc1\x20\x00\x00", .value = .{ .float = -10.0 } }, + .{ .bytes = "\xcb\x3f\xf0\x00\x00\x00\x00\x00\x01", .value = .{ .float = 1.0000000000000002 } }, + }; + for (vectors) |vector| { + var output: [16]u8 = undefined; + var reader = std.Io.Reader.fixed(vector.bytes); + var writer = std.Io.Writer.fixed(&output); + var packer = msgpack.PackerIO.init(&reader, &writer); + const decoded = try packer.read(allocator); + defer decoded.free(allocator); + try std.testing.expectEqualDeep(vector.value, decoded); + // Check encoding against the spec bytes too, not just a round trip. + try packer.write(vector.value); + try std.testing.expectEqualSlices(u8, vector.bytes, writer.buffered()); + } +} + +test "MessagePack spec: non-minimal integer formats remain valid" { + if (!has_new_io) return error.SkipZigTest; + + // Minimal encoding is a serializer SHOULD, not a decoder restriction. + const inputs = [_][]const u8{ + "\xcc\x01", + "\xcd\x00\x01", + "\xce\x00\x00\x00\x01", + "\xcf\x00\x00\x00\x00\x00\x00\x00\x01", + "\xd0\x01", + "\xd1\x00\x01", + "\xd2\x00\x00\x00\x01", + "\xd3\x00\x00\x00\x00\x00\x00\x00\x01", + }; + for (inputs) |input| { + var output: [0]u8 = .{}; + var reader = std.Io.Reader.fixed(input); + var writer = std.Io.Writer.fixed(&output); + var packer = msgpack.PackerIO.init(&reader, &writer); + const decoded = try packer.read(allocator); + defer decoded.free(allocator); + try std.testing.expectEqual(@as(u64, 1), try decoded.getUint()); + } +} +test "MessagePack spec: reserved byte is allowed in raw data and extension type" { + if (!has_new_io) return error.SkipZigTest; + + // String invalid-byte handling is implementation-defined; this library + // preserves the original bytes. Binary/extension bodies are arbitrary bytes. + // https://github.com/msgpack/msgpack/blob/master/spec.md#limitation + const Vector = struct { bytes: []const u8, kind: enum { str, bin, ext } }; + const vectors = [_]Vector{ + .{ .bytes = "\xa1\xc1", .kind = .str }, + .{ .bytes = "\xd9\x01\xc1", .kind = .str }, + .{ .bytes = "\xda\x00\x01\xc1", .kind = .str }, + .{ .bytes = "\xdb\x00\x00\x00\x01\xc1", .kind = .str }, + .{ .bytes = "\xc4\x01\xc1", .kind = .bin }, + .{ .bytes = "\xc5\x00\x01\xc1", .kind = .bin }, + .{ .bytes = "\xc6\x00\x00\x00\x01\xc1", .kind = .bin }, + .{ .bytes = "\xd4\xc1\xc1", .kind = .ext }, + .{ .bytes = "\xc7\x01\xc1\xc1", .kind = .ext }, + .{ .bytes = "\xc8\x00\x01\xc1\xc1", .kind = .ext }, + .{ .bytes = "\xc9\x00\x00\x00\x01\xc1\xc1", .kind = .ext }, + }; + for (vectors) |vector| { + var output: [0]u8 = .{}; + var reader = std.Io.Reader.fixed(vector.bytes); + var writer = std.Io.Writer.fixed(&output); + var packer = msgpack.PackerIO.init(&reader, &writer); + const decoded = try packer.read(allocator); + defer decoded.free(allocator); + switch (vector.kind) { + .str => try std.testing.expectEqualSlices(u8, "\xc1", try decoded.asStr()), + .bin => try std.testing.expectEqualSlices(u8, "\xc1", try decoded.asBin()), + .ext => { + try expect(decoded == .ext); + try std.testing.expectEqual(@as(i8, -63), decoded.ext.type); + try std.testing.expectEqualSlices(u8, "\xc1", decoded.ext.data); + }, + } + } + + // 0xc1 may also occur in a length field, rather than in a body. + var input: [195]u8 = undefined; + @memcpy(input[0..2], "\xc4\xc1"); + @memset(input[2..], 0x42); + var output: [195]u8 = undefined; + var reader = std.Io.Reader.fixed(&input); + var writer = std.Io.Writer.fixed(&output); var packer = msgpack.PackerIO.init(&reader, &writer); + const decoded = try packer.read(allocator); + defer decoded.free(allocator); + try std.testing.expectEqualSlices(u8, input[2..], try decoded.asBin()); + try packer.write(decoded); + try std.testing.expectEqualSlices(u8, &input, writer.buffered()); +} - // Should handle invalid marker gracefully (no crash) - const result = packer.read(allocator); - if (result) |payload| { - payload.free(allocator); - // If it succeeds, that's fine (marker might be treated as NIL or other) - } else |_| { - // Expected - invalid marker should cause error +test "MessagePack spec: array and map wire formats accept nil" { + if (!has_new_io) return error.SkipZigTest; + + const arrays = [_][]const u8{ + "\x91\xc0", + "\xdc\x00\x01\xc0", + "\xdd\x00\x00\x00\x01\xc0", + }; + for (arrays) |input| { + var output: [0]u8 = .{}; + var reader = std.Io.Reader.fixed(input); + var writer = std.Io.Writer.fixed(&output); + var packer = msgpack.PackerIO.init(&reader, &writer); + const decoded = try packer.read(allocator); + defer decoded.free(allocator); + try std.testing.expectEqual(@as(usize, 1), try decoded.getArrLen()); + try expect((try decoded.getArrElement(0)) == .nil); } + const maps = [_][]const u8{ + "\x81\xc0\xc0", + "\xde\x00\x01\xc0\xc0", + "\xdf\x00\x00\x00\x01\xc0\xc0", + }; + for (maps) |input| { + var output: [0]u8 = .{}; + var reader = std.Io.Reader.fixed(input); + var writer = std.Io.Writer.fixed(&output); + var packer = msgpack.PackerIO.init(&reader, &writer); + const decoded = try packer.read(allocator); + defer decoded.free(allocator); + try expect(decoded == .map); + try std.testing.expectEqual(@as(usize, 1), decoded.map.count()); + var entries = decoded.map.map.iterator(); + const entry = entries.next().?; + try expect(entry.key_ptr.* == .nil); + try expect(entry.value_ptr.* == .nil); + } +} + +test "MessagePack spec: timestamp wire vectors" { + if (!has_new_io) return error.SkipZigTest; + + // https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type + const Vector = struct { bytes: []const u8, seconds: i64, nanoseconds: u32 }; + const vectors = [_]Vector{ + .{ .bytes = "\xd6\xff\xff\xff\xff\xff", .seconds = 4294967295, .nanoseconds = 0 }, + .{ .bytes = "\xd7\xff\xee\x6b\x27\xff\xff\xff\xff\xff", .seconds = 17179869183, .nanoseconds = 999999999 }, + .{ .bytes = "\xc7\x0c\xff\x3b\x9a\xc9\xff\xff\xff\xff\xff\xff\xff\xff\xff", .seconds = -1, .nanoseconds = 999999999 }, + }; + for (vectors) |vector| { + var output: [15]u8 = undefined; + var reader = std.Io.Reader.fixed(vector.bytes); + var writer = std.Io.Writer.fixed(&output); + var packer = msgpack.PackerIO.init(&reader, &writer); + const decoded = try packer.read(allocator); + defer decoded.free(allocator); + const expected = Payload.timestampToPayload(vector.seconds, vector.nanoseconds); + try std.testing.expectEqualDeep(expected, decoded); + try packer.write(expected); + try std.testing.expectEqualSlices(u8, vector.bytes, writer.buffered()); + } +} + +test "MessagePack spec: reserved marker cleanup under allocation failure" { + // Completed children, a pending map key, and an active nested container must + // all be reclaimed whether allocation fails first or 0xc1 is reached. + var input = [_]u8{ 0x92, 0x91, 0xa1, 'x', 0x82, 0xa1, 'a', 0xa1, 'b', 0xa1, 'k', 0x91, 0xc1 }; + const Scenario = struct { + fn run(alloc: std.mem.Allocator, bytes: []u8) !void { + var output: [0]u8 = .{}; + var write_buffer = fixedBufferStream(&output); + var read_buffer = fixedBufferStream(bytes); + var p = pack.init(&write_buffer, &read_buffer); + const payload = p.read(alloc) catch |err| { + if (err == error.OutOfMemory) return err; + try std.testing.expectEqual(error.TypeMarkerReading, err); + return; + }; + defer payload.free(alloc); + return error.TestUnexpectedResult; + } + }; + try std.testing.checkAllAllocationFailures(allocator, Scenario.run, .{&input}); } test "PackerIO: corrupted length field" { @@ -3667,21 +3877,99 @@ test "clonePayload map partial fail path frees partially cloned entries" { } test "iterative free: deeply nested payload" { - // Create a deeply nested structure in memory - var root = try Payload.arrPayload(1, allocator); + var failing = std.testing.FailingAllocator.init(allocator, .{}); + const alloc = failing.allocator(); + var root = Payload.nilToPayload(); + errdefer root.free(alloc); var current: *Payload = &root; - // Build 200-layer deep structure - var i: usize = 0; - while (i < 200) : (i += 1) { - const nested = try Payload.arrPayload(1, allocator); - try current.setArrElement(0, nested); - current = ¤t.arr[0]; + // Keep siblings alive at every level, alternating arrays and maps. + for (0..4096) |depth| { + if (depth % 2 == 0) { + current.* = try Payload.arrPayload(2, alloc); + current.arr[1] = try Payload.strToPayload("sibling", alloc); + current = ¤t.arr[0]; + } else { + current.* = Payload.mapPayload(alloc); + try current.mapPut("sibling", Payload.nilToPayload()); + current.map.getPtr(.{ .str = msgpack.Str.init("sibling") }).?.* = + try Payload.binToPayload("sibling", alloc); + try current.mapPut("child", Payload.nilToPayload()); + current = current.map.getPtr(.{ .str = msgpack.Str.init("child") }).?; + } } - try current.setArrElement(0, Payload.intToPayload(42)); + current.* = try Payload.extToPayload(1, "leaf", alloc); + + failing.fail_index = failing.alloc_index; + root.free(alloc); + root = Payload.nilToPayload(); + try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); +} - // Free should not cause stack overflow - root.free(allocator); +test "iterative free: wide array under OOM" { + var failing = std.testing.FailingAllocator.init(allocator, .{}); + const alloc = failing.allocator(); + var root = try Payload.arrPayload(257, alloc); + errdefer root.free(alloc); + for (root.arr, 0..) |*item, i| { + item.* = switch (i % 3) { + 0 => try Payload.strToPayload("s", alloc), + 1 => try Payload.binToPayload("b", alloc), + else => try Payload.extToPayload(1, "e", alloc), + }; + } + + failing.fail_index = failing.alloc_index; + root.free(alloc); + root = Payload.nilToPayload(); + try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); +} + +test "iterative free: wide map with container keys under OOM" { + var failing = std.testing.FailingAllocator.init(allocator, .{}); + const alloc = failing.allocator(); + var root = Payload.mapPayload(alloc); + errdefer root.free(alloc); + for (0..257) |i| { + var key_items = [_]Payload{ + .{ .uint = i }, + .{ .str = msgpack.Str.init("key") }, + }; + const key = Payload{ .arr = &key_items }; + try root.mapPutGeneric(key, Payload.nilToPayload()); + const value = root.map.getPtr(key).?; + value.* = try Payload.arrPayload(2, alloc); + value.arr[0] = try Payload.binToPayload("value", alloc); + value.arr[1] = try Payload.extToPayload(1, "extension", alloc); + } + + failing.fail_index = failing.alloc_index; + root.free(alloc); + root = Payload.nilToPayload(); + try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); +} + +test "iterative free: parser OOM cleans completed wide child" { + // The last string can fail after the wide child has been fully decoded. + var input: [4 + 257 * 2 + 2]u8 = undefined; + @memcpy(input[0..4], &[_]u8{ 0x92, 0xdc, 0x01, 0x01 }); + for (0..257) |i| { + input[4 + i * 2] = 0xa1; + input[5 + i * 2] = 'x'; + } + @memcpy(input[input.len - 2 ..], &[_]u8{ 0xa1, 'y' }); + + const Scenario = struct { + fn run(alloc: std.mem.Allocator, bytes: []u8) !void { + var output: [0]u8 = .{}; + var write_buffer = fixedBufferStream(&output); + var read_buffer = fixedBufferStream(bytes); + var p = pack.init(&write_buffer, &read_buffer); + const payload = try p.read(alloc); + defer payload.free(alloc); + } + }; + try std.testing.checkAllAllocationFailures(allocator, Scenario.run, .{&input}); } // ========== Large Data and Fuzz Tests ==========