authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-05-18 03:47:27-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-05-19 03:15:55+02:00
log71f23402bcbb8554d5663ac5aa870abd69fda10f
tree12134acbd186d2b0914c1006a087863518b6846f
parent5b647b792c680a32c44823a050672537424c95c1

Writer: Fix splatBytePreserve losing data in certain cases

In cases where the now removed `preserve < w.end` branch was taken, some of the data in the buffer would never make it to logical sink. For example, in one of the newly added test cases where a buffer of 10 is filled with 9 bytes, and then `splatBytePreserve` is called with a preserve of 5 and a splat of 2, the 5 preserved bytes in the buffer would get memmoved to the front of the buffer in the `preserve < w.end` branch, clobbering the first 4 bytes (without any chance of them making it to the logical sink). After this commit, `rebase` is called to allow the implementation to do the preservation while sending any relevant bytes to the logical sink in the process. Addresses part of https://github.com/ziglang/zig/issues/24767 Supersedes https://github.com/ziglang/zig/pull/24875

2 files changed, 112 insertions(+), 6 deletions(-)

lib/std/Io/Writer.zig+51-6
...@@ -802,12 +802,14 @@ pub fn splatBytePreserve(w: *Writer, preserve: usize, byte: u8, n: usize) Error!...@@ -802,12 +802,14 @@ pub fn splatBytePreserve(w: *Writer, preserve: usize, byte: u8, n: usize) Error!
802 return;802 return;
803 }803 }
804 }804 }
805 // All the next bytes received must be preserved.805 // Ensure the contract of `rebase` is upheld.
806 if (preserve < w.end) {806 assert(w.end + remaining > w.buffer.len);
807 @memmove(w.buffer[0..preserve], w.buffer[w.end - preserve ..][0..preserve]);807 // Offset the amount preserved by the amount we have left to splat
808 w.end = preserve;808 // since the remaining splat is always going to be part of that
809 }809 // preservation.
810 while (remaining > 0) remaining -= try w.splatByte(byte, remaining);810 try w.vtable.rebase(w, preserve -| remaining, remaining);
811 @memset(w.buffer[w.end..][0..remaining], byte);
812 w.end += remaining;
811}813}
812814
813/// Writes the same byte many times, allowing short writes.815/// Writes the same byte many times, allowing short writes.
...@@ -2887,3 +2889,46 @@ test "writableSlice with fixed writer" {...@@ -2887,3 +2889,46 @@ test "writableSlice with fixed writer" {
2887 try w.writeByte(1);2889 try w.writeByte(1);
2888 try std.testing.expectError(error.WriteFailed, w.writableSlice(2));2890 try std.testing.expectError(error.WriteFailed, w.writableSlice(2));
2889}2891}
2892
2893test splatBytePreserve {
2894 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 5, .splat_len = 5 });
2895 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 9, .preserve = 5, .splat_len = 2 });
2896 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 5, .splat_len = 6 });
2897 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 6, .splat_len = 6 });
2898 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 5, .splat_len = 10 });
2899 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 6, .splat_len = 10 });
2900 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 6, .splat_len = 11 });
2901 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 6, .splat_len = 80 });
2902 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 6, .splat_len = 85 });
2903 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 10, .splat_len = 6 });
2904 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 10, .splat_len = 11 });
2905 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 10, .splat_len = 80 });
2906 try testSplatBytePreserve(.{ .buf_len = 10, .fill_len = 5, .preserve = 10, .splat_len = 85 });
2907}
2908
2909fn testSplatBytePreserve(options: struct { buf_len: u4, fill_len: u4, preserve: u4, splat_len: u8 }) !void {
2910 assert(options.fill_len <= options.buf_len);
2911 assert(options.preserve <= options.buf_len);
2912
2913 const fill_buf = "abcdefghijklmno";
2914 const fill = fill_buf[0..options.fill_len];
2915 var expected_out_buf: [256]u8 = @splat('X');
2916 @memcpy(expected_out_buf[0..options.fill_len], fill);
2917 const expected_out = expected_out_buf[0 .. options.fill_len + options.splat_len];
2918 const expected_preserved = expected_out[expected_out.len -| options.preserve..];
2919
2920 var out_buf: [256]u8 = undefined;
2921 var fw: Writer = .fixed(&out_buf);
2922 var indirect_buffer: [16]u8 = undefined;
2923 var twi: std.testing.WriterIndirect = .init(&fw, indirect_buffer[0..options.buf_len]);
2924 const w = &twi.interface;
2925
2926 try w.writeAll(fill);
2927 try w.splatBytePreserve(options.preserve, 'X', options.splat_len);
2928
2929 try std.testing.expectEqualStrings(expected_preserved, w.buffer[w.end -| options.preserve..w.end]);
2930
2931 try w.flush();
2932
2933 try std.testing.expectEqualStrings(expected_out, fw.buffer[0..fw.end]);
2934}
lib/std/testing.zig+61
...@@ -1334,6 +1334,67 @@ pub const ReaderIndirect = struct {...@@ -1334,6 +1334,67 @@ pub const ReaderIndirect = struct {
1334 }1334 }
1335};1335};
13361336
1337/// A `Io.Writer` that writes its data to another `Io.Writer`, and only
1338/// writes new data to its own buffer during `drain`.
1339pub const WriterIndirect = struct {
1340 out: *Io.Writer,
1341 interface: Io.Writer,
1342
1343 pub fn init(out: *Io.Writer, buffer: []u8) WriterIndirect {
1344 return .{
1345 .out = out,
1346 .interface = .{
1347 .vtable = &.{
1348 .drain = drain,
1349 },
1350 .buffer = buffer,
1351 .end = 0,
1352 },
1353 };
1354 }
1355
1356 fn drain(w: *Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
1357 const w_indirect: *WriterIndirect = @alignCast(@fieldParentPtr("interface", w));
1358
1359 // Write all data in the buffer to `out`
1360 try w_indirect.out.writeAll(w.buffer[0..w.end]);
1361 w.end = 0;
1362
1363 // Refill buffer using data
1364 {
1365 const end_before_fill = w.end;
1366 for (data[0 .. data.len - 1]) |bytes| {
1367 const dest = w.buffer[w.end..];
1368 const len = @min(bytes.len, dest.len);
1369 @memcpy(dest[0..len], bytes[0..len]);
1370 w.end += len;
1371 }
1372 const pattern = data[data.len - 1];
1373 switch (pattern.len) {
1374 0 => {},
1375 1 => {
1376 const len = @min(w.buffer[w.end..].len, splat);
1377 @memset(w.buffer[w.end..][0..len], pattern[0]);
1378 w.end += len;
1379 },
1380 else => {
1381 const dest = w.buffer[w.end..];
1382 for (0..splat) |i| {
1383 const start_i = i * pattern.len;
1384 if (start_i >= dest.len) break;
1385 const remaining = dest[start_i..];
1386 const len = @min(pattern.len, remaining.len);
1387 @memcpy(remaining[0..len], pattern[0..len]);
1388 w.end += len;
1389 }
1390 },
1391 }
1392
1393 return w.end - end_before_fill;
1394 }
1395 }
1396};
1397
1337test {1398test {
1338 _ = &Smith;1399 _ = &Smith;
1339}1400}