From 64a868ff2780efa6e9670c2459f015c92ffd1b7e Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 26 Feb 2026 08:38:28 +0300 Subject: [PATCH] io: fix buffered writer flush and write loop (fixes #21975) --- vlib/io/buffered_writer.v | 62 ++++++++++++++-------- vlib/io/buffered_writer_test.v | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 22 deletions(-) diff --git a/vlib/io/buffered_writer.v b/vlib/io/buffered_writer.v index 4881c7d08..9570945d6 100644 --- a/vlib/io/buffered_writer.v +++ b/vlib/io/buffered_writer.v @@ -47,14 +47,19 @@ pub fn (mut b BufferedWriter) flush() ! { if b.buffered() == 0 { return } - - n := b.wr.write(b.buf[0..b.n])! - if n < b.n { - return error('Writer accepted less bytes than expected without returning any explicit error.') + mut written := 0 + for written < b.n { + n := b.wr.write(b.buf[written..b.n]) or { + b.shift_unwritten_to_front(written) + return err + } + if n <= 0 || n > b.n - written { + b.shift_unwritten_to_front(written) + return error('writer returned an invalid number of bytes while flushing') + } + written += n } - b.n = 0 - return } // available returns the amount of available space left in the buffer. @@ -62,26 +67,39 @@ pub fn (b BufferedWriter) available() int { return b.buf.len - b.n } +fn (mut b BufferedWriter) shift_unwritten_to_front(written int) { + if written <= 0 { + return + } + remaining := b.n - written + if remaining <= 0 { + b.n = 0 + return + } + copy(mut b.buf[..remaining], b.buf[written..b.n]) + b.n = remaining +} + // write writes `src` in the buffer, flushing it to the underlying writer as needed, and returns the // number of bytes written. pub fn (mut b BufferedWriter) write(src []u8) !int { - mut p := src.clone() - mut nn := 0 - for p.len > b.available() { - mut n := 0 - if b.buffered() == 0 { - n = b.wr.write(p)! - } else { - n = copy(mut b.buf[b.n..], p) - b.n += n + mut written := 0 + for written < src.len { + remaining := src.len - written + if b.buffered() == 0 && remaining > b.available() { + n := b.wr.write(src[written..])! + if n <= 0 || n > remaining { + return error('writer returned an invalid number of bytes while writing') + } + written += n + continue + } + n := copy(mut b.buf[b.n..], src[written..]) + b.n += n + written += n + if b.available() == 0 { b.flush()! } - nn += n - p = p[n..].clone() } - - n := copy(mut b.buf[b.n..], p) - b.n += n - nn += n - return nn + return written } diff --git a/vlib/io/buffered_writer_test.v b/vlib/io/buffered_writer_test.v index 3d7d1daa5..27b08881a 100644 --- a/vlib/io/buffered_writer_test.v +++ b/vlib/io/buffered_writer_test.v @@ -21,6 +21,50 @@ fn (mut aw ArrayWriter) write(buf []u8) !int { return res } +struct ChunkedArrayWriter { + chunk int +pub mut: + result []u8 +} + +fn (mut cw ChunkedArrayWriter) write(buf []u8) !int { + if buf.len == 0 { + return 0 + } + n := if buf.len < cw.chunk { buf.len } else { cw.chunk } + for i := 0; i < n; i++ { + cw.result << buf[i] + } + return n +} + +struct FailOnceChunkedWriter { + chunk int +pub mut: + result []u8 +mut: + writes int +} + +fn (mut fw FailOnceChunkedWriter) write(buf []u8) !int { + if fw.writes == 1 { + fw.writes++ + return error('forced write failure') + } + fw.writes++ + n := if buf.len < fw.chunk { buf.len } else { fw.chunk } + for i := 0; i < n; i++ { + fw.result << buf[i] + } + return n +} + +struct ZeroProgressWriter {} + +fn (mut w ZeroProgressWriter) write(buf []u8) !int { + return 0 +} + // write less than max bytes, returns number of bytes written // data is written in chunks. fn write_random_data(mut aw ArrayWriter, mut bw io.BufferedWriter, max int) !int { @@ -135,3 +179,54 @@ fn test_simple_flush() { assert bw.buffered() == 0 assert aw.result.len == 7 } + +fn test_flush_handles_partial_writes() { + mut cw := ChunkedArrayWriter{ + chunk: 2 + } + mut bw := io.new_buffered_writer(writer: cw, cap: 8)! + data := 'abcdefg'.bytes() + + written := bw.write(data)! + assert written == data.len + assert bw.buffered() == data.len + assert cw.result.len == 0 + + bw.flush()! + assert bw.buffered() == 0 + assert cw.result == data +} + +fn test_flush_preserves_unwritten_data_on_error() { + mut fw := FailOnceChunkedWriter{ + chunk: 3 + } + mut bw := io.new_buffered_writer(writer: fw, cap: 8)! + data := 'abcdefg'.bytes() + _ = bw.write(data)! + + if _ := bw.flush() { + assert false + } else { + assert err.msg() == 'forced write failure' + } + assert fw.result == 'abc'.bytes() + assert bw.buffered() == 4 + assert bw.buf[..bw.buffered()] == 'defg'.bytes() + + bw.flush()! + assert bw.buffered() == 0 + assert fw.result == data +} + +fn test_write_returns_error_for_zero_progress_writes() { + mut zw := ZeroProgressWriter{} + mut bw := io.new_buffered_writer(writer: zw, cap: 4)! + + if _ := bw.write('12345'.bytes()) { + assert false + } else { + assert err.msg() == 'writer returned an invalid number of bytes while writing' + } + assert bw.buffered() == 0 +} -- 2.39.5