diff --git a/vlib/io/buffered_writer.v b/vlib/io/buffered_writer.v new file mode 100644 index 0000000000..4881c7d08b --- /dev/null +++ b/vlib/io/buffered_writer.v @@ -0,0 +1,87 @@ +module io + +pub struct BufferedWriter { +mut: + n int + wr Writer +pub mut: + buf []u8 +} + +@[params] +pub struct BufferedWriterConfig { +pub: + writer Writer + cap int = 128 * 1024 +} + +// new_buffered_writer creates a new BufferedWriter with the specified BufferedWriterConfig. +// Returns an error when cap is 0 or negative. +pub fn new_buffered_writer(o BufferedWriterConfig) !&BufferedWriter { + if o.cap < 1 { + return error('`o.cap` must be a positive integer') + } + + return &BufferedWriter{ + buf: []u8{len: o.cap} + wr: o.writer + } +} + +// reset resets the buffer to its initial state. +pub fn (mut b BufferedWriter) reset() { + cap := b.buf.len + b.buf = []u8{len: cap} + b.n = 0 +} + +// buffered returns the number of bytes currently stored in the buffer. +pub fn (b BufferedWriter) buffered() int { + return b.n +} + +// flush writes the buffered data to the underlying writer and clears the buffer, ensures all data is +// written. +// Returns an error if the writer fails to write all buffered data. +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.') + } + + b.n = 0 + return +} + +// available returns the amount of available space left in the buffer. +pub fn (b BufferedWriter) available() int { + return b.buf.len - b.n +} + +// 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 + b.flush()! + } + nn += n + p = p[n..].clone() + } + + n := copy(mut b.buf[b.n..], p) + b.n += n + nn += n + return nn +} diff --git a/vlib/io/buffered_writer_test.v b/vlib/io/buffered_writer_test.v new file mode 100644 index 0000000000..78eca87c27 --- /dev/null +++ b/vlib/io/buffered_writer_test.v @@ -0,0 +1,133 @@ +import io +import rand + +struct ArrayWriter { +pub mut: + result []u8 +} + +// implement io.Writer +fn (mut aw ArrayWriter) write(buf []u8) !int { + len := buf.len + mut res := 0 + for i := 0; i < len; i++ { + aw.result << buf[i] + res++ + } + return res +} + +// 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 { + less_than_max := max - 255 // guarantee 1 full u8 less than max + mut total := 0 + for total < less_than_max { + r := rand.u8() + d := rand.bytes(r)! + w := bw.write(d)! + total += w + } + return total +} + +fn test_flush() { + mut aw := ArrayWriter{} + max := 65536 + mut bw := io.new_buffered_writer(writer: aw, cap: max)! + + // write less data than buffer capacity, the underlying writer should receive no data. + written := write_random_data(mut aw, mut bw, 65536)! + assert written == bw.buffered() + assert aw.result.len == 0 + + bw.flush()! + assert aw.result.len == written + assert bw.buffered() == 0 + assert bw.available() == max +} + +fn test_write() { + mut aw := ArrayWriter{} + max := 65536 + mut bw := io.new_buffered_writer(writer: aw, cap: max)! + + // write less data than buffer capacity, the underlying writer should receive no data. + written := write_random_data(mut aw, mut bw, 65536)! + + // now exceed buffer capacity by a little + little := rand.u8() + excess := bw.available() + little + excess_data := rand.bytes(excess)! + w := bw.write(excess_data)! + assert bw.buffered() == little + assert bw.available() == max - little + assert aw.result.len == max +} + +fn test_write_big() { + mut aw := ArrayWriter{} + max := 65536 + mut bw := io.new_buffered_writer(writer: aw, cap: max)! + + more_than_max := max + rand.u8() + big_source := rand.bytes(more_than_max)! + w := bw.write(big_source)! + assert w == more_than_max + assert bw.buffered() == 0 + assert bw.available() == max + assert aw.result.len == more_than_max +} + +// create_data returns an array with `n` elements plus `\n` as last character. +fn create_data(n int) []u8 { + mut res := []u8{} + for i := 0; i < n; i++ { + res << `X` + } + res << `\n` + return res +} + +fn test_simple_write() { + mut aw := ArrayWriter{} + mut bw := io.new_buffered_writer(writer: aw, cap: 10)! + mut data := create_data(6) + w1 := bw.write(data)! + + // add data to buffer, less than cap + // data is written to buffer, not to underlying writer. + assert w1 == 7 // 6*x + \n + assert bw.buffered() == 7 + assert aw.result.len == 0 + + // add more data, exceed cap + // data is flushed to underlying writer when buffer is full, then more data is written to buffer. + w2 := bw.write(data)! + assert w2 == 7 + assert bw.buffered() == 4 + assert aw.result.len == 10 + + // exceed cap immediately + // all data is written without buffering + aw.result = []u8{} + data = create_data(33) + bw.reset() + w3 := bw.write(data)! + assert bw.buffered() == 0 + assert aw.result.len == 34 // 33*x + \n +} + +fn test_simple_flush() { + mut aw := ArrayWriter{} + mut bw := io.new_buffered_writer(writer: aw, cap: 10)! + data := create_data(6) + w := bw.write(data)! + + assert w == 7 // 6*x + \n + assert bw.buffered() == 7 + + bw.flush()! + assert bw.buffered() == 0 + assert aw.result.len == 7 +}