authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-10-03 01:18:53-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-10-09 12:01:51-07:00
logacd6ffdf69332d61b51f64f4707b472c147b7034
treef7a797f21a4d70962014bbf7b6669a9178e4a626
parentff16a7c3fa29c95edefc8496114d577b4037fe6f

Reader.peekDelimiterInclusive: Fix handling of `stream` implementations that return 0

Previously, the logic in peekDelimiterInclusive (when the delimiter was not found in the existing buffer) used the `n` returned from `r.vtable.stream` as the length of the slice to check, but it's valid for `vtable.stream` implementations to return 0 if they wrote to the buffer instead of `w`. In that scenario, the `indexOfScalarPos` would be given a 0-length slice so it would never be able to find the delimiter. This commit changes the logic to assume that `r.vtable.stream` can both: - return 0, and - modify seek/end (i.e. it's also valid for a `vtable.stream` implementation to rebase) Also introduces `std.testing.ReaderIndirect` which helps in being able to test against Reader implementations that return 0 from `stream`/`readVec` Fixes #25428

2 files changed, 97 insertions(+), 4 deletions(-)

lib/std/Io/Reader.zig+37-4
......@@ -763,13 +763,14 @@ pub fn takeDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {
763763pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {
764764 const buffer = r.buffer[0..r.end];
765765 const seek = r.seek;
766 if (std.mem.indexOfScalarPos(u8, buffer, seek, delimiter)) |end| {
766 if (std.mem.indexOfScalarPos(u8, buffer, seek, delimiter)) |delimiter_index| {
767767 @branchHint(.likely);
768 return buffer[seek .. end + 1];
768 return buffer[seek .. delimiter_index + 1];
769769 }
770770 // TODO take a parameter for max search length rather than relying on buffer capacity
771771 try rebase(r, r.buffer.len);
772772 while (r.buffer.len - r.end != 0) {
773 const existing_buffered_len = r.end - r.seek;
773774 const end_cap = r.buffer[r.end..];
774775 var writer: Writer = .fixed(end_cap);
775776 const n = r.vtable.stream(r, &writer, .limited(end_cap.len)) catch |err| switch (err) {
......@@ -777,8 +778,8 @@ pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {
777778 else => |e| return e,
778779 };
779780 r.end += n;
780 if (std.mem.indexOfScalarPos(u8, end_cap[0..n], 0, delimiter)) |end| {
781 return r.buffer[0 .. r.end - n + end + 1];
781 if (std.mem.indexOfScalarPos(u8, r.buffer[0..r.end], r.seek + existing_buffered_len, delimiter)) |delimiter_index| {
782 return r.buffer[r.seek .. delimiter_index + 1];
782783 }
783784 }
784785 return error.StreamTooLong;
......@@ -1534,6 +1535,18 @@ test "readSliceShort with smaller buffer than Reader" {
15341535 try testing.expectEqualStrings(str, &buf);
15351536}
15361537
1538test "readSliceShort with indirect reader" {
1539 var r: Reader = .fixed("HelloFren");
1540 var ri_buf: [3]u8 = undefined;
1541 var ri: std.testing.ReaderIndirect = .init(&r, &ri_buf);
1542 var buf: [5]u8 = undefined;
1543 try testing.expectEqual(5, try ri.interface.readSliceShort(&buf));
1544 try testing.expectEqualStrings("Hello", buf[0..5]);
1545 try testing.expectEqual(4, try ri.interface.readSliceShort(&buf));
1546 try testing.expectEqualStrings("Fren", buf[0..4]);
1547 try testing.expectEqual(0, try ri.interface.readSliceShort(&buf));
1548}
1549
15371550test readVec {
15381551 var r: Reader = .fixed(std.ascii.letters);
15391552 var flat_buffer: [52]u8 = undefined;
......@@ -1643,6 +1656,26 @@ test "takeDelimiterInclusive when it rebases" {
16431656 }
16441657}
16451658
1659test "takeDelimiterInclusive on an indirect reader when it rebases" {
1660 const written_line = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n";
1661 var buffer: [128]u8 = undefined;
1662 var tr: std.testing.Reader = .init(&buffer, &.{
1663 .{ .buffer = written_line[0..4] },
1664 .{ .buffer = written_line[4..] },
1665 .{ .buffer = written_line },
1666 .{ .buffer = written_line },
1667 .{ .buffer = written_line },
1668 .{ .buffer = written_line },
1669 .{ .buffer = written_line },
1670 });
1671 var indirect_buffer: [128]u8 = undefined;
1672 var tri: std.testing.ReaderIndirect = .init(&tr.interface, &indirect_buffer);
1673 const r = &tri.interface;
1674 for (0..6) |_| {
1675 try std.testing.expectEqualStrings(written_line, try r.takeDelimiterInclusive('\n'));
1676 }
1677}
1678
16461679test "takeStruct and peekStruct packed" {
16471680 var r: Reader = .fixed(&.{ 0b11110000, 0b00110011 });
16481681 const S = packed struct(u16) { a: u2, b: u6, c: u7, d: u1 };
lib/std/testing.zig+60
......@@ -1249,3 +1249,63 @@ pub const Reader = struct {
12491249 return n;
12501250 }
12511251};
1252
1253/// A `std.Io.Reader` that gets its data from another `std.Io.Reader`, and always
1254/// writes to its own buffer (and returns 0) during `stream` and `readVec`.
1255pub const ReaderIndirect = struct {
1256 in: *std.Io.Reader,
1257 interface: std.Io.Reader,
1258
1259 pub fn init(in: *std.Io.Reader, buffer: []u8) ReaderIndirect {
1260 return .{
1261 .in = in,
1262 .interface = .{
1263 .vtable = &.{
1264 .stream = stream,
1265 .readVec = readVec,
1266 },
1267 .buffer = buffer,
1268 .seek = 0,
1269 .end = 0,
1270 },
1271 };
1272 }
1273
1274 fn readVec(r: *std.Io.Reader, _: [][]u8) std.Io.Reader.Error!usize {
1275 try streamInner(r);
1276 return 0;
1277 }
1278
1279 fn stream(r: *std.Io.Reader, _: *std.Io.Writer, _: std.Io.Limit) std.Io.Reader.StreamError!usize {
1280 try streamInner(r);
1281 return 0;
1282 }
1283
1284 fn streamInner(r: *std.Io.Reader) std.Io.Reader.Error!void {
1285 const r_indirect: *ReaderIndirect = @alignCast(@fieldParentPtr("interface", r));
1286
1287 // If there's no room remaining in the buffer at all, make room.
1288 if (r.buffer.len == r.end) {
1289 try r.rebase(r.buffer.len);
1290 }
1291
1292 var writer: std.Io.Writer = .{
1293 .buffer = r.buffer,
1294 .end = r.end,
1295 .vtable = &.{
1296 .drain = std.Io.Writer.unreachableDrain,
1297 .rebase = std.Io.Writer.unreachableRebase,
1298 },
1299 };
1300 defer r.end = writer.end;
1301
1302 r_indirect.in.streamExact(&writer, r.buffer.len - r.end) catch |err| switch (err) {
1303 // Only forward EndOfStream if no new bytes were written to the buffer
1304 error.EndOfStream => |e| if (r.end == writer.end) {
1305 return e;
1306 },
1307 error.WriteFailed => unreachable,
1308 else => |e| return e,
1309 };
1310 }
1311};