authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-09-06 10:23:41+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-10-09 12:08:03-07:00
logf661ab6c36c0366331451986a29ee14ac05fd8eb
treea7ea8365f91c0dff9026558bbacd4e034f7fe72b
parent9ea4d9aa3bc184a1be1199f3b519796d417c7765

std.Io.Reader: fix delimiter bugs

Fix `takeDelimiter` and `takeDelimiterExclusive` tossing too many bytes (#25132) Also add/improve test coverage for all delimiter and sentinel methods, update usages of `takeDelimiterExclusive` to not rely on the fixed bug, tweak a handful of doc comments, and slightly simplify some logic. I have not fixed #24950 in this commit because I am a little less certain about the appropriate solution there. Resolves: #25132 Co-authored-by: Andrew Kelley <andrew@ziglang.org>

3 files changed, 70 insertions(+), 15 deletions(-)

lib/std/Io/Reader.zig+66-9
...@@ -449,7 +449,6 @@ pub fn readVecAll(r: *Reader, data: [][]u8) Error!void {...@@ -449,7 +449,6 @@ pub fn readVecAll(r: *Reader, data: [][]u8) Error!void {
449/// is returned instead.449/// is returned instead.
450///450///
451/// See also:451/// See also:
452/// * `peek`
453/// * `toss`452/// * `toss`
454pub fn peek(r: *Reader, n: usize) Error![]u8 {453pub fn peek(r: *Reader, n: usize) Error![]u8 {
455 try r.fill(n);454 try r.fill(n);
...@@ -700,7 +699,7 @@ pub const DelimiterError = error{...@@ -700,7 +699,7 @@ pub const DelimiterError = error{
700};699};
701700
702/// Returns a slice of the next bytes of buffered data from the stream until701/// Returns a slice of the next bytes of buffered data from the stream until
703/// `sentinel` is found, advancing the seek position.702/// `sentinel` is found, advancing the seek position past the sentinel.
704///703///
705/// Returned slice has a sentinel.704/// Returned slice has a sentinel.
706///705///
...@@ -733,7 +732,7 @@ pub fn peekSentinel(r: *Reader, comptime sentinel: u8) DelimiterError![:sentinel...@@ -733,7 +732,7 @@ pub fn peekSentinel(r: *Reader, comptime sentinel: u8) DelimiterError![:sentinel
733}732}
734733
735/// Returns a slice of the next bytes of buffered data from the stream until734/// Returns a slice of the next bytes of buffered data from the stream until
736/// `delimiter` is found, advancing the seek position.735/// `delimiter` is found, advancing the seek position past the delimiter.
737///736///
738/// Returned slice includes the delimiter as the last byte.737/// Returned slice includes the delimiter as the last byte.
739///738///
...@@ -786,7 +785,8 @@ pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {...@@ -786,7 +785,8 @@ pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {
786}785}
787786
788/// Returns a slice of the next bytes of buffered data from the stream until787/// Returns a slice of the next bytes of buffered data from the stream until
789/// `delimiter` is found, advancing the seek position.788/// `delimiter` is found, advancing the seek position up to (but not past)
789/// the delimiter.
790///790///
791/// Returned slice excludes the delimiter. End-of-stream is treated equivalent791/// Returned slice excludes the delimiter. End-of-stream is treated equivalent
792/// to a delimiter, unless it would result in a length 0 return value, in which792/// to a delimiter, unless it would result in a length 0 return value, in which
...@@ -800,20 +800,44 @@ pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {...@@ -800,20 +800,44 @@ pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {
800/// Invalidates previously returned values from `peek`.800/// Invalidates previously returned values from `peek`.
801///801///
802/// See also:802/// See also:
803/// * `takeDelimiter`
803/// * `takeDelimiterInclusive`804/// * `takeDelimiterInclusive`
804/// * `peekDelimiterExclusive`805/// * `peekDelimiterExclusive`
805pub fn takeDelimiterExclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {806pub fn takeDelimiterExclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 {
806 const result = r.peekDelimiterInclusive(delimiter) catch |err| switch (err) {807 const result = try r.peekDelimiterExclusive(delimiter);
808 r.toss(result.len);
809 return result;
810}
811
812/// Returns a slice of the next bytes of buffered data from the stream until
813/// `delimiter` is found, advancing the seek position past the delimiter.
814///
815/// Returned slice excludes the delimiter. End-of-stream is treated equivalent
816/// to a delimiter, unless it would result in a length 0 return value, in which
817/// case `null` is returned instead.
818///
819/// If the delimiter is not found within a number of bytes matching the
820/// capacity of this `Reader`, `error.StreamTooLong` is returned. In
821/// such case, the stream state is unmodified as if this function was never
822/// called.
823///
824/// Invalidates previously returned values from `peek`.
825///
826/// See also:
827/// * `takeDelimiterInclusive`
828/// * `takeDelimiterExclusive`
829pub fn takeDelimiter(r: *Reader, delimiter: u8) error{ ReadFailed, StreamTooLong }!?[]u8 {
830 const inclusive = r.peekDelimiterInclusive(delimiter) catch |err| switch (err) {
807 error.EndOfStream => {831 error.EndOfStream => {
808 const remaining = r.buffer[r.seek..r.end];832 const remaining = r.buffer[r.seek..r.end];
809 if (remaining.len == 0) return error.EndOfStream;833 if (remaining.len == 0) return null;
810 r.toss(remaining.len);834 r.toss(remaining.len);
811 return remaining;835 return remaining;
812 },836 },
813 else => |e| return e,837 else => |e| return e,
814 };838 };
815 r.toss(result.len);839 r.toss(inclusive.len);
816 return result[0 .. result.len - 1];840 return inclusive[0 .. inclusive.len - 1];
817}841}
818842
819/// Returns a slice of the next bytes of buffered data from the stream until843/// Returns a slice of the next bytes of buffered data from the stream until
...@@ -1336,6 +1360,9 @@ test peekSentinel {...@@ -1336,6 +1360,9 @@ test peekSentinel {
1336 var r: Reader = .fixed("ab\nc");1360 var r: Reader = .fixed("ab\nc");
1337 try testing.expectEqualStrings("ab", try r.peekSentinel('\n'));1361 try testing.expectEqualStrings("ab", try r.peekSentinel('\n'));
1338 try testing.expectEqualStrings("ab", try r.peekSentinel('\n'));1362 try testing.expectEqualStrings("ab", try r.peekSentinel('\n'));
1363 r.toss(3);
1364 try testing.expectError(error.EndOfStream, r.peekSentinel('\n'));
1365 try testing.expectEqualStrings("c", try r.peek(1));
1339}1366}
13401367
1341test takeDelimiterInclusive {1368test takeDelimiterInclusive {
...@@ -1350,22 +1377,52 @@ test peekDelimiterInclusive {...@@ -1350,22 +1377,52 @@ test peekDelimiterInclusive {
1350 try testing.expectEqualStrings("ab\n", try r.peekDelimiterInclusive('\n'));1377 try testing.expectEqualStrings("ab\n", try r.peekDelimiterInclusive('\n'));
1351 r.toss(3);1378 r.toss(3);
1352 try testing.expectError(error.EndOfStream, r.peekDelimiterInclusive('\n'));1379 try testing.expectError(error.EndOfStream, r.peekDelimiterInclusive('\n'));
1380 try testing.expectEqualStrings("c", try r.peek(1));
1353}1381}
13541382
1355test takeDelimiterExclusive {1383test takeDelimiterExclusive {
1356 var r: Reader = .fixed("ab\nc");1384 var r: Reader = .fixed("ab\nc");
1385
1357 try testing.expectEqualStrings("ab", try r.takeDelimiterExclusive('\n'));1386 try testing.expectEqualStrings("ab", try r.takeDelimiterExclusive('\n'));
1387 try testing.expectEqualStrings("", try r.takeDelimiterExclusive('\n'));
1388 try testing.expectEqualStrings("", try r.takeDelimiterExclusive('\n'));
1389 try testing.expectEqualStrings("\n", try r.take(1));
1390
1358 try testing.expectEqualStrings("c", try r.takeDelimiterExclusive('\n'));1391 try testing.expectEqualStrings("c", try r.takeDelimiterExclusive('\n'));
1359 try testing.expectError(error.EndOfStream, r.takeDelimiterExclusive('\n'));1392 try testing.expectError(error.EndOfStream, r.takeDelimiterExclusive('\n'));
1360}1393}
13611394
1362test peekDelimiterExclusive {1395test peekDelimiterExclusive {
1363 var r: Reader = .fixed("ab\nc");1396 var r: Reader = .fixed("ab\nc");
1397
1364 try testing.expectEqualStrings("ab", try r.peekDelimiterExclusive('\n'));1398 try testing.expectEqualStrings("ab", try r.peekDelimiterExclusive('\n'));
1365 try testing.expectEqualStrings("ab", try r.peekDelimiterExclusive('\n'));1399 try testing.expectEqualStrings("ab", try r.peekDelimiterExclusive('\n'));
1366 r.toss(3);1400 r.toss(2);
1401 try testing.expectEqualStrings("", try r.peekDelimiterExclusive('\n'));
1402 try testing.expectEqualStrings("\n", try r.take(1));
1403
1367 try testing.expectEqualStrings("c", try r.peekDelimiterExclusive('\n'));1404 try testing.expectEqualStrings("c", try r.peekDelimiterExclusive('\n'));
1368 try testing.expectEqualStrings("c", try r.peekDelimiterExclusive('\n'));1405 try testing.expectEqualStrings("c", try r.peekDelimiterExclusive('\n'));
1406 r.toss(1);
1407 try testing.expectError(error.EndOfStream, r.peekDelimiterExclusive('\n'));
1408}
1409
1410test takeDelimiter {
1411 var r: Reader = .fixed("ab\nc\n\nd");
1412 try testing.expectEqualStrings("ab", (try r.takeDelimiter('\n')).?);
1413 try testing.expectEqualStrings("c", (try r.takeDelimiter('\n')).?);
1414 try testing.expectEqualStrings("", (try r.takeDelimiter('\n')).?);
1415 try testing.expectEqualStrings("d", (try r.takeDelimiter('\n')).?);
1416 try testing.expectEqual(null, try r.takeDelimiter('\n'));
1417 try testing.expectEqual(null, try r.takeDelimiter('\n'));
1418
1419 r = .fixed("ab\nc\n\nd\n"); // one trailing newline does not affect behavior
1420 try testing.expectEqualStrings("ab", (try r.takeDelimiter('\n')).?);
1421 try testing.expectEqualStrings("c", (try r.takeDelimiter('\n')).?);
1422 try testing.expectEqualStrings("", (try r.takeDelimiter('\n')).?);
1423 try testing.expectEqualStrings("d", (try r.takeDelimiter('\n')).?);
1424 try testing.expectEqual(null, try r.takeDelimiter('\n'));
1425 try testing.expectEqual(null, try r.takeDelimiter('\n'));
1369}1426}
13701427
1371test streamDelimiter {1428test streamDelimiter {
lib/std/net.zig+3-2
...@@ -1393,7 +1393,7 @@ fn parseHosts(...@@ -1393,7 +1393,7 @@ fn parseHosts(
1393 br: *Io.Reader,1393 br: *Io.Reader,
1394) error{ OutOfMemory, ReadFailed }!void {1394) error{ OutOfMemory, ReadFailed }!void {
1395 while (true) {1395 while (true) {
1396 const line = br.takeDelimiterExclusive('\n') catch |err| switch (err) {1396 const line = br.takeDelimiter('\n') catch |err| switch (err) {
1397 error.StreamTooLong => {1397 error.StreamTooLong => {
1398 // Skip lines that are too long.1398 // Skip lines that are too long.
1399 _ = br.discardDelimiterInclusive('\n') catch |e| switch (e) {1399 _ = br.discardDelimiterInclusive('\n') catch |e| switch (e) {
...@@ -1403,7 +1403,8 @@ fn parseHosts(...@@ -1403,7 +1403,8 @@ fn parseHosts(
1403 continue;1403 continue;
1404 },1404 },
1405 error.ReadFailed => return error.ReadFailed,1405 error.ReadFailed => return error.ReadFailed,
1406 error.EndOfStream => break,1406 } orelse {
1407 break; // end of stream
1407 };1408 };
1408 var split_it = mem.splitScalar(u8, line, '#');1409 var split_it = mem.splitScalar(u8, line, '#');
1409 const no_comment_line = split_it.first();1410 const no_comment_line = split_it.first();
lib/std/zig/system/linux.zig+1-4
...@@ -358,14 +358,11 @@ fn CpuinfoParser(comptime impl: anytype) type {...@@ -358,14 +358,11 @@ fn CpuinfoParser(comptime impl: anytype) type {
358 return struct {358 return struct {
359 fn parse(arch: Target.Cpu.Arch, reader: *std.Io.Reader) !?Target.Cpu {359 fn parse(arch: Target.Cpu.Arch, reader: *std.Io.Reader) !?Target.Cpu {
360 var obj: impl = .{};360 var obj: impl = .{};
361 while (reader.takeDelimiterExclusive('\n')) |line| {361 while (try reader.takeDelimiter('\n')) |line| {
362 const colon_pos = mem.indexOfScalar(u8, line, ':') orelse continue;362 const colon_pos = mem.indexOfScalar(u8, line, ':') orelse continue;
363 const key = mem.trimEnd(u8, line[0..colon_pos], " \t");363 const key = mem.trimEnd(u8, line[0..colon_pos], " \t");
364 const value = mem.trimStart(u8, line[colon_pos + 1 ..], " \t");364 const value = mem.trimStart(u8, line[colon_pos + 1 ..], " \t");
365 if (!try obj.line_hook(key, value)) break;365 if (!try obj.line_hook(key, value)) break;
366 } else |err| switch (err) {
367 error.EndOfStream => {},
368 else => |e| return e,
369 }366 }
370 return obj.finalize(arch);367 return obj.finalize(arch);
371 }368 }