Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -434,6 +438,7 @@ msgpack.MsgPackError.ExtDataTooLarge // 扩展类型数据过大

**内存安全**:

- `Payload.free(allocator)` 无需分配新内存或递归即可释放其拥有的数据,即使分配器已耗尽也能完成清理。
- 所有错误路径包含完整清理(`errdefer` + `cleanupParseStack`)
- 零内存泄漏(测试中由 GPA 验证)
- 可安全解析来自网络、文件或用户输入的不可信数据
Expand Down
189 changes: 81 additions & 108 deletions src/msgpack.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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,
Expand Down
Loading
Loading