From b0c59bd77f842478bb6a284b325db648d9d34b1c Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:21 +0300 Subject: [PATCH] encoding.binary: add stream reader/writer (fixes #19037) --- vlib/encoding/binary/README.md | 53 +++++ vlib/encoding/binary/stream.v | 298 +++++++++++++++++++++++++++++ vlib/encoding/binary/stream_test.v | 132 +++++++++++++ 3 files changed, 483 insertions(+) create mode 100644 vlib/encoding/binary/stream.v create mode 100644 vlib/encoding/binary/stream_test.v diff --git a/vlib/encoding/binary/README.md b/vlib/encoding/binary/README.md index 732d9df26..2342c5187 100644 --- a/vlib/encoding/binary/README.md +++ b/vlib/encoding/binary/README.md @@ -87,3 +87,56 @@ fn main() { assert a == c } ``` + +For Go-style fixed-size stream I/O, you can use `read()`, `write()`, and `size()` with +`binary.little_endian` or `binary.big_endian`: + +```v +module main + +import encoding.binary +import io + +struct Header { + version u16 + flags u16 + length u32 +} + +struct Buffer { +mut: + data []u8 + pos int +} + +fn (mut b Buffer) read(mut out []u8) !int { + if b.pos >= b.data.len { + return io.Eof{} + } + n := if out.len < b.data.len - b.pos { out.len } else { b.data.len - b.pos } + copy(mut out[..n], b.data[b.pos..b.pos + n]) + b.pos += n + return n +} + +fn (mut b Buffer) write(src []u8) !int { + b.data << src + return src.len +} + +fn main() { + header := Header{ + version: 1 + flags: 2 + length: 32 + } + mut buf := Buffer{} + binary.write(mut buf, binary.big_endian, header)! + assert binary.size(header) == 8 + + buf.pos = 0 + mut decoded := Header{} + binary.read(mut buf, binary.big_endian, mut decoded)! + assert decoded == header +} +``` diff --git a/vlib/encoding/binary/stream.v b/vlib/encoding/binary/stream.v new file mode 100644 index 000000000..a7198d12c --- /dev/null +++ b/vlib/encoding/binary/stream.v @@ -0,0 +1,298 @@ +module binary + +import io + +// ByteOrder decodes and encodes fixed-size integers using a specific byte order. +pub interface ByteOrder { + u16(b []u8) u16 + u32(b []u8) u32 + u64(b []u8) u64 + put_u16(mut b []u8, value u16) + put_u32(mut b []u8, value u32) + put_u64(mut b []u8, value u64) +} + +// LittleEndian implements ByteOrder using little-endian encoding. +pub struct LittleEndian {} + +// BigEndian implements ByteOrder using big-endian encoding. +pub struct BigEndian {} + +pub const little_endian = LittleEndian{} +pub const big_endian = BigEndian{} + +// u16 decodes a 16-bit unsigned integer from b using little-endian byte order. +pub fn (_ LittleEndian) u16(b []u8) u16 { + return little_endian_u16(b) +} + +// u32 decodes a 32-bit unsigned integer from b using little-endian byte order. +pub fn (_ LittleEndian) u32(b []u8) u32 { + return little_endian_u32(b) +} + +// u64 decodes a 64-bit unsigned integer from b using little-endian byte order. +pub fn (_ LittleEndian) u64(b []u8) u64 { + return little_endian_u64(b) +} + +// put_u16 encodes value into b using little-endian byte order. +pub fn (_ LittleEndian) put_u16(mut b []u8, value u16) { + little_endian_put_u16(mut b, value) +} + +// put_u32 encodes value into b using little-endian byte order. +pub fn (_ LittleEndian) put_u32(mut b []u8, value u32) { + little_endian_put_u32(mut b, value) +} + +// put_u64 encodes value into b using little-endian byte order. +pub fn (_ LittleEndian) put_u64(mut b []u8, value u64) { + little_endian_put_u64(mut b, value) +} + +// u16 decodes a 16-bit unsigned integer from b using big-endian byte order. +pub fn (_ BigEndian) u16(b []u8) u16 { + return big_endian_u16(b) +} + +// u32 decodes a 32-bit unsigned integer from b using big-endian byte order. +pub fn (_ BigEndian) u32(b []u8) u32 { + return big_endian_u32(b) +} + +// u64 decodes a 64-bit unsigned integer from b using big-endian byte order. +pub fn (_ BigEndian) u64(b []u8) u64 { + return big_endian_u64(b) +} + +// put_u16 encodes value into b using big-endian byte order. +pub fn (_ BigEndian) put_u16(mut b []u8, value u16) { + big_endian_put_u16(mut b, value) +} + +// put_u32 encodes value into b using big-endian byte order. +pub fn (_ BigEndian) put_u32(mut b []u8, value u32) { + big_endian_put_u32(mut b, value) +} + +// put_u64 encodes value into b using big-endian byte order. +pub fn (_ BigEndian) put_u64(mut b []u8, value u64) { + big_endian_put_u64(mut b, value) +} + +// read decodes fixed-size values from reader using order. +pub fn read[T](mut reader io.Reader, order ByteOrder, mut data T) ! { + read_value(mut reader, order, mut data)! +} + +// write encodes fixed-size values to writer using order. +pub fn write[T](mut writer io.Writer, order ByteOrder, data T) ! { + write_value(mut writer, order, data)! +} + +// size returns the number of bytes needed to encode data, or `-1` for unsupported types. +pub fn size[T](data T) int { + return size_value(data) +} + +fn size_value[T](data T) int { + $if T is bool || T is u8 || T is i8 { + return 1 + } $else $if T is u16 || T is i16 { + return 2 + } $else $if T is u32 || T is i32 || T is f32 { + return 4 + } $else $if T is u64 || T is i64 || T is f64 { + return 8 + } $else $if T is $array { + mut total := 0 + for value in data { + value_size := size(value) + if value_size < 0 { + return -1 + } + total += value_size + } + return total + } $else $if T is $struct { + mut total := 0 + $for field in T.fields { + if field.name == '_' { + return -1 + } + field_size := size(data.$(field.name)) + if field_size < 0 { + return -1 + } + total += field_size + } + return total + } $else { + return -1 + } +} + +fn read_value[T](mut reader io.Reader, order ByteOrder, mut data T) ! { + $if T is bool { + mut buf := []u8{len: 1} + read_full(mut reader, mut buf)! + data = buf[0] != 0 + } $else $if T is u8 { + mut buf := []u8{len: 1} + read_full(mut reader, mut buf)! + data = buf[0] + } $else $if T is i8 { + mut buf := []u8{len: 1} + read_full(mut reader, mut buf)! + data = i8(buf[0]) + } $else $if T is u16 { + mut buf := []u8{len: 2} + read_full(mut reader, mut buf)! + data = order.u16(buf) + } $else $if T is i16 { + mut buf := []u8{len: 2} + read_full(mut reader, mut buf)! + data = i16(order.u16(buf)) + } $else $if T is u32 { + mut buf := []u8{len: 4} + read_full(mut reader, mut buf)! + data = order.u32(buf) + } $else $if T is i32 { + mut buf := []u8{len: 4} + read_full(mut reader, mut buf)! + data = i32(order.u32(buf)) + } $else $if T is u64 { + mut buf := []u8{len: 8} + read_full(mut reader, mut buf)! + data = order.u64(buf) + } $else $if T is i64 { + mut buf := []u8{len: 8} + read_full(mut reader, mut buf)! + data = i64(order.u64(buf)) + } $else $if T is f32 { + mut buf := []u8{len: 4} + read_full(mut reader, mut buf)! + data = unsafe { + U32_F32{ + u: order.u32(buf) + }.f + } + } $else $if T is f64 { + mut buf := []u8{len: 8} + read_full(mut reader, mut buf)! + data = unsafe { + U64_F64{ + u: order.u64(buf) + }.f + } + } $else $if T is $array { + $if T is []u8 { + read_full(mut reader, mut data)! + } $else { + for i in 0 .. data.len { + read_value(mut reader, order, mut data[i])! + } + } + } $else $if T is $struct { + $for field in T.fields { + if field.name == '_' { + return error('binary.read: structs with `_` fields are not supported') + } + read_value(mut reader, order, mut data.$(field.name))! + } + } $else { + return error('binary.read: unsupported type ${typeof(data).name}') + } +} + +fn write_value[T](mut writer io.Writer, order ByteOrder, data T) ! { + $if T is bool { + write_full(mut writer, [u8(data)])! + } $else $if T is u8 { + write_full(mut writer, [u8(data)])! + } $else $if T is i8 { + write_full(mut writer, [u8(data)])! + } $else $if T is u16 { + mut buf := []u8{len: 2} + order.put_u16(mut buf, data) + write_full(mut writer, buf)! + } $else $if T is i16 { + mut buf := []u8{len: 2} + order.put_u16(mut buf, u16(data)) + write_full(mut writer, buf)! + } $else $if T is u32 { + mut buf := []u8{len: 4} + order.put_u32(mut buf, data) + write_full(mut writer, buf)! + } $else $if T is i32 { + mut buf := []u8{len: 4} + order.put_u32(mut buf, u32(data)) + write_full(mut writer, buf)! + } $else $if T is u64 { + mut buf := []u8{len: 8} + order.put_u64(mut buf, data) + write_full(mut writer, buf)! + } $else $if T is i64 { + mut buf := []u8{len: 8} + order.put_u64(mut buf, u64(data)) + write_full(mut writer, buf)! + } $else $if T is f32 { + mut buf := []u8{len: 4} + bits := unsafe { + U32_F32{ + f: data + }.u + } + order.put_u32(mut buf, bits) + write_full(mut writer, buf)! + } $else $if T is f64 { + mut buf := []u8{len: 8} + bits := unsafe { + U64_F64{ + f: data + }.u + } + order.put_u64(mut buf, bits) + write_full(mut writer, buf)! + } $else $if T is $array { + $if T is []u8 { + write_full(mut writer, data)! + } $else { + for value in data { + write_value(mut writer, order, value)! + } + } + } $else $if T is $struct { + $for field in T.fields { + if field.name == '_' { + return error('binary.write: structs with `_` fields are not supported') + } + write_value(mut writer, order, data.$(field.name))! + } + } $else { + return error('binary.write: unsupported type ${typeof(data).name}') + } +} + +fn read_full(mut reader io.Reader, mut buf []u8) ! { + mut offset := 0 + for offset < buf.len { + n := reader.read(mut buf[offset..])! + if n <= 0 || n > buf.len - offset { + return error('binary.read: reader returned an invalid number of bytes') + } + offset += n + } +} + +fn write_full(mut writer io.Writer, buf []u8) ! { + mut offset := 0 + for offset < buf.len { + n := writer.write(buf[offset..])! + if n <= 0 || n > buf.len - offset { + return error('binary.write: writer returned an invalid number of bytes') + } + offset += n + } +} diff --git a/vlib/encoding/binary/stream_test.v b/vlib/encoding/binary/stream_test.v new file mode 100644 index 000000000..a58dcdc16 --- /dev/null +++ b/vlib/encoding/binary/stream_test.v @@ -0,0 +1,132 @@ +module binary + +import io + +struct ChunkedReader { + data []u8 + chunk int = 1 +mut: + offset int +} + +fn (mut r ChunkedReader) read(mut buf []u8) !int { + if r.offset >= r.data.len { + return io.Eof{} + } + mut n := if r.chunk > 0 && r.chunk < buf.len { r.chunk } else { buf.len } + remaining := r.data.len - r.offset + if remaining < n { + n = remaining + } + copy(mut buf[..n], r.data[r.offset..r.offset + n]) + r.offset += n + return n +} + +struct ChunkedWriter { + chunk int = 1 +mut: + data []u8 +} + +fn (mut w ChunkedWriter) write(buf []u8) !int { + if buf.len == 0 { + return 0 + } + n := if w.chunk > 0 && w.chunk < buf.len { w.chunk } else { buf.len } + w.data << buf[..n] + return n +} + +struct Packet { + flag bool + count u16 + values [2]u32 + sample f32 + trailer [3]u8 +} + +struct UnsupportedPacket { + name string +} + +fn test_stream_write_and_read_struct() { + packet := Packet{ + flag: true + count: 0x1234 + values: [u32(0x11223344), 0x55667788]! + sample: f32(12.5) + trailer: [u8(1), 2, 3]! + } + mut writer := ChunkedWriter{ + chunk: 3 + } + write(mut writer, big_endian, packet)! + assert size(packet) == writer.data.len + + mut reader := ChunkedReader{ + data: writer.data.clone() + chunk: 2 + } + mut decoded := Packet{} + read(mut reader, big_endian, mut decoded)! + assert decoded == packet +} + +fn test_stream_read_and_write_slice() { + values := [u16(0x0102), 0x0304, 0x0506] + mut writer := ChunkedWriter{ + chunk: 2 + } + write(mut writer, little_endian, values)! + assert size(values) == 6 + + mut reader := ChunkedReader{ + data: writer.data.clone() + chunk: 1 + } + mut decoded := []u16{len: values.len} + read(mut reader, little_endian, mut decoded)! + assert decoded == values +} + +fn test_stream_read_and_write_bytes() { + bytes := [u8(9), 8, 7, 6, 5] + mut writer := ChunkedWriter{ + chunk: 4 + } + write(mut writer, little_endian, bytes)! + + mut reader := ChunkedReader{ + data: writer.data.clone() + chunk: 2 + } + mut decoded := []u8{len: bytes.len} + read(mut reader, little_endian, mut decoded)! + assert decoded == bytes +} + +fn test_stream_short_read_returns_eof() { + mut reader := ChunkedReader{ + data: [u8(1), 2] + chunk: 1 + } + mut value := u32(0) + if _ := read(mut reader, little_endian, mut value) { + assert false + } else { + assert err is io.Eof + } +} + +fn test_stream_reports_unsupported_types() { + assert size('hello') == -1 + assert size(UnsupportedPacket{}) == -1 + + mut writer := ChunkedWriter{} + if _ := write(mut writer, little_endian, 'hello') { + assert false + } else { + assert err.msg() == 'binary.write: unsupported type string' + } +} -- 2.39.5