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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Encoding writes straight into the writer's buffer when there is room, and keeps the drain path out of line, which is about 13% cheaper on a typical struct and 37% on an array of integers
- Encoding a struct is about 25% cheaper again, by telling the compiler that writing into the buffer cannot change the writer itself

### Fixed
- Decoding numbers from a streaming reader whose buffer is smaller than the number (under 8 bytes) works, instead of failing with `error.ReaderBufferTooSmall`; that error is now only for strings that have to be buffered whole, such as struct field names

## [0.9.0] - 2026-09-05

### Changed
Expand Down
24 changes: 16 additions & 8 deletions src/msgpack.zig
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ pub const SliceDecodeError = error{
pub const DecodeError = SliceDecodeError || error{
/// The reader failed, and its own error says why.
ReadFailed,
/// A value had to be buffered whole, and the reader's buffer is smaller.
/// A string had to be buffered whole, such as a struct field name, and
/// the reader's buffer is smaller.
ReaderBufferTooSmall,
};

Expand Down Expand Up @@ -453,21 +454,28 @@ test "decode from a streaming reader whose buffer holds the largest value" {
}
}

test "decode from a streaming reader with too small a buffer errors, never panics" {
const Numeric = struct { id: u64, seq: i32, ratio: f64, ok: bool };
test "decode numbers from a streaming reader whose buffer is smaller than them" {
const Numeric = struct {
id: u64,
seq: i32,
ratio: f64,
ok: bool,

pub fn msgpackFormat() StructFormat {
return .{ .as_array = .{} };
}
};
const value = Numeric{ .id = 0xdead_beef_cafe_1234, .seq = -4242, .ratio = 3.14159, .ok = true };

var encoded: [64]u8 = undefined;
const bytes = try encodeToBuffer(value, &encoded);

// The u64 payload needs 8 buffered bytes; anything smaller cannot rebase.
// The u64 and f64 payloads are 8 bytes, more than any of these buffers.
for ([_]usize{ 1, 2, 4, 7 }) |buffer_len| {
var buffer: [8]u8 = undefined;
var trickle = TrickleReader.init(buffer[0..buffer_len], bytes);
try std.testing.expectError(
error.ReaderBufferTooSmall,
decodeLeaky(Numeric, std.testing.allocator, &trickle.reader),
);
const decoded = try decodeLeaky(Numeric, std.testing.allocator, &trickle.reader);
try std.testing.expectEqualDeep(value, decoded);
}
}

Expand Down
29 changes: 22 additions & 7 deletions src/utils.zig
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,30 @@ pub noinline fn writeAllSlow(writer: *std.Io.Writer, bytes: []const u8) std.Io.W
return writer.writeAll(bytes);
}

/// Reads a big-endian `T` from the reader. `Reader.takeInt` rebases, which
/// asserts the reader's buffer can hold `@sizeOf(T)`; report that as an error
/// rather than letting the assert fire.
/// Reads a big-endian `T` from the reader. When the bytes are already
/// buffered, which is always the case when decoding from a slice, this is one
/// compare and a load. `Reader.takeInt` would check the same thing, but it
/// rebases on a miss, which asserts the reader's buffer can hold `@sizeOf(T)`,
/// so it needed a check of the buffer's capacity in front of it as well.
pub inline fn takeInt(reader: *std.Io.Reader, comptime T: type) !T {
if (reader.buffer.len < @divExact(@bitSizeOf(T), 8)) {
@branchHint(.unlikely);
return error.ReaderBufferTooSmall;
const n = @divExact(@bitSizeOf(T), 8);
const buffered = reader.buffered();
if (buffered.len >= n) {
@branchHint(.likely);
reader.toss(n);
return std.mem.readInt(T, buffered[0..n], .big);
}
return reader.takeInt(T, .big);
return takeIntSlow(reader, T);
}

/// The fallback of `takeInt`, for when the bytes are not all buffered. Copying
/// them out with `readSliceAll` works across refills, so a reader whose buffer
/// is smaller than `T` still works.
noinline fn takeIntSlow(reader: *std.Io.Reader, comptime T: type) !T {
@branchHint(.cold);
var bytes: [@divExact(@bitSizeOf(T), 8)]u8 = undefined;
try reader.readSliceAll(&bytes);
return std.mem.readInt(T, &bytes, .big);
}

/// Copies `dest.len` bytes out of the reader. When they are already buffered,
Expand Down
Loading