From 08044ce5ef44e59fd3a6c02a922aff4f75717c09 Mon Sep 17 00:00:00 2001 From: Laurence Withers Date: Fri, 3 Jan 2020 13:42:38 +0000 Subject: [PATCH] Initial commit; import from github.com/lwithers/pkg --- LICENSE | 19 +++ README.md | 57 ++++++++ doc.go | 25 ++++ example_test.go | 64 +++++++++ go.mod | 3 + go.sum | 0 reader.go | 23 ++++ reader_test.go | 71 ++++++++++ val_read.go | 271 ++++++++++++++++++++++++++++++++++++++ val_read_test.go | 329 ++++++++++++++++++++++++++++++++++++++++++++++ val_write.go | 153 +++++++++++++++++++++ val_write_test.go | 260 ++++++++++++++++++++++++++++++++++++ writer.go | 36 +++++ writer_test.go | 78 +++++++++++ 14 files changed, 1389 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doc.go create mode 100644 example_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 reader.go create mode 100644 reader_test.go create mode 100644 val_read.go create mode 100644 val_read_test.go create mode 100644 val_write.go create mode 100644 val_write_test.go create mode 100644 writer.go create mode 100644 writer_test.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c33580 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Laurence Withers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc8a4e2 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# src.lwithers.me.uk/go/byteio + +[![GoDoc](https://godoc.org/src.lwithers.me.uk/go/byteio?status.svg)](https://godoc.org/src.lwithers.me.uk/go/byteio) + +This package provides two interfaces, `Reader` and `Writer`, which capture the +most common byte-oriented methods for I/O, namely `Read([]byte)`, `ReadByte()` +and `ReadRune()` (plus the equivalent writing methods). This is useful because +sometimes you simply want to perform byte-oriented I/O without knowing whether +you have an underlying `bytes.Buffer` or `bufio.Reader` etc. + +Furthermore, as a convenience, there are functions which automatically wrap +any `io.Reader` or `io.Writer` with a bufio equivalent if necessary. These +allow for simple byte-oriented I/O operations on arbitrary reader / writer +interfaces with minimal boilerplate. + +Example of a method using the `byteio.Reader` interface: + +``` +func ReadUint16LE(r byteio.Reader) (uint16, error) { + var n uint16 + if x, err := r.ReadByte(); err != nil { + return 0, err + } else { + n = uint16(x) + } + if x, err := r.ReadByte(); err != nil { + return 0, err + } else { + n |= uint16(x) << 8 + } + return n, nil +} +``` + +Example of something calling this function: + +``` + var r io.Reader + // … + br := byteio.NewReader(r) + n, err := ReadUint16LE(br) +``` + +Example of using the writer interface: + +``` + var w io.Writer + // … + bw := byteio.NewWriter(w) + defer byteio.FlushIfNecessary(bw) + err := WriteUint16LE(bw, 0x5432) +``` + +The `byteio.FlushIfNecessary` method determines whether its argument has a +`Flush()` method or not, and if it does, calls it. This allows the caller to +write non-conditional code, not having to check whether `bw` is a `bufio.Writer` +(which needs a flush) or a `bytes.Buffer` (which does not), etc. diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..db1dc12 --- /dev/null +++ b/doc.go @@ -0,0 +1,25 @@ +/* +Package byteio supports applications that perform byte-oriented I/O, reading and +writing only small chunks at a time. Its primary purpose is the Reader and +Writer interfaces which extend io.Reader and io.Writer with byte-, rune- and +string-oriented functions, along with an adapter function that turns any reader +or writer into the interface (possibly by wrapping it in a bufio.Reader or +bufio.Writer). + +Note that the Reader and Writer types from both the bufio and the bytes package +implement this package's Reader and Writer interfaces. + +When using the adapter function for the writer, since it is possible that the +returned type may be a new bufio.Writer, it is necessary to check for whether +Flush() must be called at operation completion time. This can be done +succinctly with: + + bout := byteio.NewWriter(out) + defer byteio.FlushIfNecessary(bout) + +The binary read/write functions were benchmarked (using bufio) to determine that, +for sizes up to and including 8 bytes, it was faster to call +ReadByte()/WriteByte() multiple times in succession than to call Read()/Write() +with a small buffer. +*/ +package byteio diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..a003f31 --- /dev/null +++ b/example_test.go @@ -0,0 +1,64 @@ +package byteio_test + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "os" + + "src.lwithers.me.uk/go/byteio" +) + +// SwapEndian32 swaps the endianness of a stream of uint32 integers. +// +// Note this function takes standard io.Reader and io.Writer interfaces, and +// uses byteio to (possibly) wrap these with bufio.Reader/Writer for efficient +// byte-oriented operation. This is transparent to the caller. +func SwapEndian32(in io.Reader, out io.Writer) error { + // optionally wrap input and output + bin, bout := byteio.NewReader(in), byteio.NewWriter(out) + + // since output may be buffered, make sure to flush it when leaving + // this function + defer byteio.FlushIfNecessary(bout) + + for { + // it doesn't matter whether we read BE or LE, as long as we + // write the opposite! + n, err := byteio.ReadUint32BE(bin) + switch err { + case nil: + case io.EOF: + return nil + default: + return err + } + + if err = byteio.WriteUint32LE(bout, n); err != nil { + return err + } + } +} + +func Example() { + // prepare input buffer + input := []uint32{0xDEADBEEF, 0x7FFFFFFF} + inbuf := bytes.NewBuffer(nil) + for _, n := range input { + byteio.WriteUint32BE(inbuf, n) + } + + // run our endianness swapper + outbuf := bytes.NewBuffer(nil) + if err := SwapEndian32(inbuf, outbuf); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // dump the output + fmt.Println(hex.Dump(outbuf.Bytes())) + + // Output: + // 00000000 ef be ad de ff ff ff 7f |........| +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..87eb01d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module src.lwithers.me.uk/go/byteio + +go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..dca29ba --- /dev/null +++ b/reader.go @@ -0,0 +1,23 @@ +package byteio + +import ( + "bufio" + "io" +) + +// Reader provides byte-oriented reading routines. It is satisfied by +// bufio.Reader, bytes.Reader and bytes.Buffer. +type Reader interface { + io.Reader + io.ByteReader + io.RuneReader +} + +// NewReader adapts any io.Reader into a byteio.Reader, possibly returning +// a new bufio.Reader. +func NewReader(in io.Reader) Reader { + if bin, ok := in.(Reader); ok { + return bin + } + return bufio.NewReader(in) +} diff --git a/reader_test.go b/reader_test.go new file mode 100644 index 0000000..2174013 --- /dev/null +++ b/reader_test.go @@ -0,0 +1,71 @@ +package byteio_test + +import ( + "bufio" + "bytes" + "os" + "testing" + + "src.lwithers.me.uk/go/byteio" +) + +// MockReader does not implement Reader, thus ensuring that NewReader must wrap +// it. The implementation returns an incrementing byte pattern. +type MockReader struct { + pos uint8 +} + +func (r *MockReader) Read(buf []byte) (n int, err error) { + for i := range buf { + buf[i] = r.pos + r.pos++ + } + return len(buf), nil +} + +// TestNewReaderBytesB checks that a bytes.Buffer is successfully transformed +// into a Reader. +func TestNewReaderBytesB(t *testing.T) { + orig := bytes.NewBuffer(nil) + bin := byteio.NewReader(orig) + if act, ok := bin.(*bytes.Buffer); !ok { + t.Errorf("Reader(%T) returned unexpected %T", orig, bin) + } else if act != orig { + t.Errorf("Reader(%p) returned unexpected %p", orig, act) + } +} + +// TestNewReaderBytesR checks that a bytes.Reader is successfully transformed +// into a Reader. +func TestNewReaderBytesR(t *testing.T) { + orig := bytes.NewReader(nil) + bin := byteio.NewReader(orig) + if act, ok := bin.(*bytes.Reader); !ok { + t.Errorf("Reader(%T) returned unexpected %T", orig, bin) + } else if act != orig { + t.Errorf("Reader(%p) returned unexpected %p", orig, act) + } +} + +// TestNewReaderBufio checks that a bufio.Reader is successfully transformed +// into a Reader. +func TestNewReaderBufio(t *testing.T) { + orig := bufio.NewReader(os.Stdin) + bin := byteio.NewReader(orig) + if act, ok := bin.(*bufio.Reader); !ok { + t.Errorf("Reader(%T) returned unexpected %T", orig, bin) + } else if act != orig { + t.Errorf("Reader(%p) returned unexpected %p", orig, act) + } +} + +// TestNewReader checks that an arbitrary io.Reader is successfully wrapped into +// a byteio.Reader. +func TestNewReader(t *testing.T) { + orig := new(MockReader) + bin := byteio.NewReader(orig) + if _, ok := bin.(*bufio.Reader); !ok { + t.Errorf("Reader(%T) did not wrap to bufio.Reader (got %T)", + orig, bin) + } +} diff --git a/val_read.go b/val_read.go new file mode 100644 index 0000000..1b9107c --- /dev/null +++ b/val_read.go @@ -0,0 +1,271 @@ +package byteio + +import ( + "io" + "math" +) + +// ReadUint16BE reads an unsigned uint16 in big-endian (network) byte order. +func ReadUint16BE(bin Reader) (uint16, error) { + var b0, b1 byte + var err error + if b0, err = bin.ReadByte(); err != nil { + // allow io.EOF to propagate normally before first read + return 0, err + } + if b1, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + return uint16(b0)<<8 | uint16(b1), nil +} + +// ReadInt16BE reads a signed int16 in big-endian (network) byte order. +func ReadInt16BE(bin Reader) (int16, error) { + n, err := ReadUint16BE(bin) + return int16(n), err +} + +// ReadUint32BE reads an unsigned uint32 in big-endian (network) byte order. +func ReadUint32BE(bin Reader) (uint32, error) { + var b0, b1, b2, b3 byte + var err error + if b0, err = bin.ReadByte(); err != nil { + return 0, err + } + if b1, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b2, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b3, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + return uint32(b0)<<24 | uint32(b1)<<16 | + uint32(b2)<<8 | uint32(b3), nil +} + +// ReadInt32BE reads a signed int32 in big-endian (network) byte order. +func ReadInt32BE(bin Reader) (int32, error) { + n, err := ReadUint32BE(bin) + return int32(n), err +} + +// ReadFloat32BE reads an IEEE-754 32-bit floating point number in big-endian +// (network) byte order. +func ReadFloat32BE(bin Reader) (float32, error) { + n, err := ReadUint32BE(bin) + return math.Float32frombits(n), err +} + +// ReadUint64BE reads an unsigned uint64 in big-endian (network) byte order. +func ReadUint64BE(bin Reader) (uint64, error) { + var b0, b1, b2, b3, b4, b5, b6, b7 byte + var err error + if b0, err = bin.ReadByte(); err != nil { + return 0, err + } + if b1, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b2, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b3, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b4, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b5, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b6, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b7, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + return uint64(b0)<<56 | uint64(b1)<<48 | + uint64(b2)<<40 | uint64(b3)<<32 | + uint64(b4)<<24 | uint64(b5)<<16 | + uint64(b6)<<8 | uint64(b7), nil +} + +// ReadInt64BE reads a signed int64 in big-endian (network) byte order. +func ReadInt64BE(bin Reader) (int64, error) { + n, err := ReadUint64BE(bin) + return int64(n), err +} + +// ReadFloat64BE reads an IEEE-754 64-bit floating point number in big-endian +// (network) byte order. +func ReadFloat64BE(bin Reader) (float64, error) { + n, err := ReadUint64BE(bin) + return math.Float64frombits(n), err +} + +// ReadUint16LE reads an unsigned uint16 in little-endian byte order. +func ReadUint16LE(bin Reader) (uint16, error) { + var b0, b1 byte + var err error + if b1, err = bin.ReadByte(); err != nil { + return 0, err + } + if b0, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + return uint16(b0)<<8 | uint16(b1), nil +} + +// ReadUint16LE reads a signed int16 in little-endian byte order. +func ReadInt16LE(bin Reader) (int16, error) { + n, err := ReadUint16LE(bin) + return int16(n), err +} + +// ReadUint32LE reads an unsigned uint32 in little-endian byte order. +func ReadUint32LE(bin Reader) (uint32, error) { + var b0, b1, b2, b3 byte + var err error + if b3, err = bin.ReadByte(); err != nil { + return 0, err + } + if b2, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b1, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b0, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + return uint32(b0)<<24 | uint32(b1)<<16 | + uint32(b2)<<8 | uint32(b3), nil +} + +// ReadUint32LE reads a signed int32 in little-endian byte order. +func ReadInt32LE(bin Reader) (int32, error) { + n, err := ReadUint32LE(bin) + return int32(n), err +} + +// ReadFloat32LE reads an IEEE-754 32-bit floating point number in big-endian +// (network) byte order. +func ReadFloat32LE(bin Reader) (float32, error) { + n, err := ReadUint32LE(bin) + return math.Float32frombits(n), err +} + +// ReadUint64LE reads an unsigned uint64 in little-endian byte order. +func ReadUint64LE(bin Reader) (uint64, error) { + var b0, b1, b2, b3, b4, b5, b6, b7 byte + var err error + if b7, err = bin.ReadByte(); err != nil { + return 0, err + } + if b6, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b5, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b4, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b3, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b2, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b1, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + if b0, err = bin.ReadByte(); err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + return uint64(b0)<<56 | uint64(b1)<<48 | + uint64(b2)<<40 | uint64(b3)<<32 | + uint64(b4)<<24 | uint64(b5)<<16 | + uint64(b6)<<8 | uint64(b7), nil +} + +// ReadInt64LE reads a signed int64 in little-endian byte order. +func ReadInt64LE(bin Reader) (int64, error) { + n, err := ReadUint64LE(bin) + return int64(n), err +} + +// ReadFloat64LE reads an IEEE-754 64-bit floating point number in big-endian +// (network) byte order. +func ReadFloat64LE(bin Reader) (float64, error) { + n, err := ReadUint64LE(bin) + return math.Float64frombits(n), err +} diff --git a/val_read_test.go b/val_read_test.go new file mode 100644 index 0000000..49c0e7a --- /dev/null +++ b/val_read_test.go @@ -0,0 +1,329 @@ +package byteio_test + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "math" + "testing" + + "src.lwithers.me.uk/go/byteio" +) + +// TestReadUint ensures correct operation of the ReadUint variants by verifying +// read data integrity against encoding/binary. +func TestReadUint(t *testing.T) { + checkErr := func(err error) { + if err != nil { + t.Fatalf("unexpected I/O error: %v", err) + } + } + + // prepare a buffer of test data + vals := []uint64{0, 1, 0x8000, 0xFFFF, 0x81020304050607} + buf := bytes.NewBuffer(nil) + for _, val := range vals { + checkErr(binary.Write(buf, binary.BigEndian, uint16(val))) + checkErr(binary.Write(buf, binary.BigEndian, uint32(val))) + checkErr(binary.Write(buf, binary.BigEndian, uint64(val))) + checkErr(binary.Write(buf, binary.LittleEndian, uint16(val))) + checkErr(binary.Write(buf, binary.LittleEndian, uint32(val))) + checkErr(binary.Write(buf, binary.LittleEndian, uint64(val))) + } + + // validate that we read back the expected values + for _, exp64 := range vals { + var ( + act16, exp16 uint16 = 0, uint16(exp64) + act32, exp32 uint32 = 0, uint32(exp64) + act64 uint64 + err error + ) + + act16, err = byteio.ReadUint16BE(buf) + checkErr(err) + if act16 != exp16 { + t.Errorf("ReadUint16BE: act %X ≠ exp %X", act16, exp16) + } + + act32, err = byteio.ReadUint32BE(buf) + checkErr(err) + if act32 != exp32 { + t.Errorf("ReadUint32BE: act %X ≠ exp %X", act32, exp32) + } + + act64, err = byteio.ReadUint64BE(buf) + checkErr(err) + if act64 != exp64 { + t.Errorf("ReadUint64BE: act %X ≠ exp %X", act64, exp64) + } + + act16, err = byteio.ReadUint16LE(buf) + checkErr(err) + if act16 != exp16 { + t.Errorf("ReadUint16LE: act %X ≠ exp %X", act16, exp16) + } + + act32, err = byteio.ReadUint32LE(buf) + checkErr(err) + if act32 != exp32 { + t.Errorf("ReadUint32LE: act %X ≠ exp %X", act32, exp32) + } + + act64, err = byteio.ReadUint64LE(buf) + checkErr(err) + if act64 != exp64 { + t.Errorf("ReadUint64LE: act %X ≠ exp %X", act64, exp64) + } + } +} + +// TestReadInt ensures correct operation of the ReadInt variants by comparing +// operation against encoding/binary. +func TestReadInt(t *testing.T) { + checkErr := func(err error) { + if err != nil { + t.Fatalf("read error: %v", err) + } + } + + // prepare a buffer of test data using encoding/binary + vals := []int64{-1, 0, 1, -0x8000, 0xFFFF, 0x01020304050607} + buf := bytes.NewBuffer(nil) + for _, val := range vals { + checkErr(binary.Write(buf, binary.BigEndian, int16(val))) + checkErr(binary.Write(buf, binary.BigEndian, int32(val))) + checkErr(binary.Write(buf, binary.BigEndian, int64(val))) + checkErr(binary.Write(buf, binary.LittleEndian, int16(val))) + checkErr(binary.Write(buf, binary.LittleEndian, int32(val))) + checkErr(binary.Write(buf, binary.LittleEndian, int64(val))) + } + + // validate that we read back the expected values + for _, exp64 := range vals { + var ( + v16, exp16 int16 = 0, int16(exp64) + v32, exp32 int32 = 0, int32(exp64) + v64 int64 + err error + ) + + v16, err = byteio.ReadInt16BE(buf) + checkErr(err) + if v16 != exp16 { + t.Errorf("ReadInt16BE: act %X ≠ exp %X", v16, exp16) + } + + v32, err = byteio.ReadInt32BE(buf) + checkErr(err) + if v32 != exp32 { + t.Errorf("ReadInt32BE: act %X ≠ exp %X", v32, exp32) + } + + v64, err = byteio.ReadInt64BE(buf) + checkErr(err) + if v64 != exp64 { + t.Errorf("ReadInt64BE: act %X ≠ exp %X", v64, exp64) + } + + v16, err = byteio.ReadInt16LE(buf) + checkErr(err) + if v16 != exp16 { + t.Errorf("ReadInt16LE: act %X ≠ exp %X", v16, exp16) + } + + v32, err = byteio.ReadInt32LE(buf) + checkErr(err) + if v32 != exp32 { + t.Errorf("ReadInt32LE: act %X ≠ exp %X", v32, exp32) + } + + v64, err = byteio.ReadInt64LE(buf) + checkErr(err) + if v64 != exp64 { + t.Errorf("ReadInt64LE: act %X ≠ exp %X", v64, exp64) + } + } +} + +// TestReadFloat validates operation of the byteio.ReadFloat variants against +// values written by encoding/binary. +func TestReadFloat(t *testing.T) { + vals := []float64{ + 0, 1, -1, + math.MaxFloat32, math.SmallestNonzeroFloat32, + math.Inf(1), math.Inf(-1), math.NaN(), + } + + buf := bytes.NewBuffer(nil) + + for _, val := range vals { + binary.Write(buf, binary.BigEndian, val) + binary.Write(buf, binary.BigEndian, float32(val)) + binary.Write(buf, binary.LittleEndian, val) + binary.Write(buf, binary.LittleEndian, float32(val)) + } + + for _, exp64 := range vals { + exp32 := float32(exp64) + + if act64, err := byteio.ReadFloat64BE(buf); err != nil { + t.Fatalf("read error: %v", err) + } else if act64 != exp64 && !math.IsNaN(act64) && !math.IsNaN(exp64) { + t.Errorf("act %f (0x%X) ≠ exp %f (0x%X)", + act64, math.Float64bits(act64), + exp64, math.Float64bits(exp64)) + } + if act32, err := byteio.ReadFloat32BE(buf); err != nil { + t.Fatalf("read error: %v", err) + } else if act32 != exp32 && !math.IsNaN(float64(act32)) && !math.IsNaN(exp64) { + t.Errorf("act %f (0x%X) ≠ exp %f (0x%X)", + act32, math.Float32bits(act32), + exp32, math.Float32bits(exp32)) + } + + if act64, err := byteio.ReadFloat64LE(buf); err != nil { + t.Fatalf("read error: %v", err) + } else if act64 != exp64 && !math.IsNaN(act64) && !math.IsNaN(exp64) { + t.Errorf("act %f (0x%X) ≠ exp %f (0x%X)", + act64, math.Float64bits(act64), + exp64, math.Float64bits(exp64)) + } + if act32, err := byteio.ReadFloat32LE(buf); err != nil { + t.Fatalf("read error: %v", err) + } else if act32 != exp32 && !math.IsNaN(float64(act32)) && !math.IsNaN(exp64) { + t.Errorf("act %f (0x%X) ≠ exp %f (0x%X)", + act32, math.Float32bits(act32), + exp32, math.Float32bits(exp32)) + } + } +} + +// TestReadIntEOF ensures that io.EOF is propagated correctly when a ReadInt +// variant is called at the end of a buffer (i.e. 0 bytes to read). It only +// tests the Uint variants since the Int and Float variants are wrappers over +// Uint. +func TestReadIntEOF(t *testing.T) { + bin := bytes.NewBuffer(nil) + check := func(fn string, f func() error) { + err := f() + switch err { + case nil: + t.Errorf("%s: did not return expected error", fn) + case io.EOF: + // OK + default: + t.Errorf("%s: returned unexpected error %v", fn, err) + } + } + + check("ReadUint16LE", func() error { _, err := byteio.ReadUint16LE(bin); return err }) + check("ReadUint16BE", func() error { _, err := byteio.ReadUint16BE(bin); return err }) + check("ReadUint32LE", func() error { _, err := byteio.ReadUint32LE(bin); return err }) + check("ReadUint32BE", func() error { _, err := byteio.ReadUint32BE(bin); return err }) + check("ReadUint64LE", func() error { _, err := byteio.ReadUint64LE(bin); return err }) + check("ReadUint64BE", func() error { _, err := byteio.ReadUint64BE(bin); return err }) + +} + +// TestReadIntShort ensures that io.ErrUnexpectedEOF is returned when a ReadInt +// variant is called without sufficient data, but not immediately at the end of +// the buffer (i.e. > 0 bytes to read, but not sufficient for data type). +func TestReadIntShort(t *testing.T) { + check := func(fn string, sz int, f func(bin byteio.Reader) error) { + for i := 1; i < sz; i++ { + bin := bytes.NewBuffer(make([]byte, i)) + err := f(bin) + switch err { + case nil: + t.Errorf("%s/%d: did not return expected error", + fn, i) + case io.ErrUnexpectedEOF: + // OK + case io.EOF: + t.Errorf("%s/%d: returned incorrect io.EOF", + fn, i) + default: + t.Errorf("%s/%d: returned unexpected error %v", + fn, i, err) + } + } + } + + check("ReadUint16LE", 2, func(bin byteio.Reader) error { _, err := byteio.ReadUint16LE(bin); return err }) + check("ReadUint16BE", 2, func(bin byteio.Reader) error { _, err := byteio.ReadUint16BE(bin); return err }) + check("ReadUint32LE", 4, func(bin byteio.Reader) error { _, err := byteio.ReadUint32LE(bin); return err }) + check("ReadUint32BE", 4, func(bin byteio.Reader) error { _, err := byteio.ReadUint32BE(bin); return err }) + check("ReadUint64LE", 8, func(bin byteio.Reader) error { _, err := byteio.ReadUint64LE(bin); return err }) + check("ReadUint64BE", 8, func(bin byteio.Reader) error { _, err := byteio.ReadUint64BE(bin); return err }) +} + +// ErrAbortReader is returned by AbortReader when the limit is reached. +var ErrAbortReader = errors.New("aborted read") + +// AbortReader will abort after a certain number of bytes have been read with a +// non-io.EOF error. This can be used to test error handling on partial reads. +// It does not implement byteio.Reader, to ensure that errors propagate +// correctly through the implicit bufio wrapper. +type AbortReader struct { + when, cur int +} + +func (ar *AbortReader) Read(buf []byte) (n int, err error) { + for i := range buf { + if ar.when == ar.cur { + return i, ErrAbortReader + } + buf[i] = 0x55 + ar.cur++ + } + return len(buf), nil +} + +// TestReadIntErr ensures that errors encountered during reads are propagated +// correctly. +func TestReadIntErr(t *testing.T) { + check := func(name string, size int, fn func(bin byteio.Reader) error) { + for i := 0; i < size; i++ { + err := fn(byteio.NewReader(&AbortReader{when: i})) + switch err { + case nil: + if i < size { + t.Errorf("%s: expected err after %d "+ + "bytes", name, i) + } + case ErrAbortReader: + if i == size { + t.Errorf("%s: unexpected err after %d "+ + "bytes", name, i) + } + default: + t.Errorf("%s: unexpected err %v", name, err) + } + } + } + + check("ReadUint16BE", 2, func(bin byteio.Reader) error { _, err := byteio.ReadUint16BE(bin); return err }) + check("ReadUint32BE", 4, func(bin byteio.Reader) error { _, err := byteio.ReadUint32BE(bin); return err }) + check("ReadUint64BE", 8, func(bin byteio.Reader) error { _, err := byteio.ReadUint64BE(bin); return err }) + check("ReadUint16LE", 2, func(bin byteio.Reader) error { _, err := byteio.ReadUint16LE(bin); return err }) + check("ReadUint32LE", 4, func(bin byteio.Reader) error { _, err := byteio.ReadUint32LE(bin); return err }) + check("ReadUint64LE", 8, func(bin byteio.Reader) error { _, err := byteio.ReadUint64LE(bin); return err }) +} + +// BenchmarkReadUint32BE is a simple benchmark for reading 32-bit integers. +func BenchmarkReadUint32BE(b *testing.B) { + bin := byteio.NewReader(new(MockReader)) + for i := 0; i < b.N; i++ { + _, _ = byteio.ReadUint32BE(bin) + } +} + +// BenchmarkReadUint32BE is a simple benchmark for reading 64-bit integers. +func BenchmarkReadUint64BE(b *testing.B) { + bin := byteio.NewReader(new(MockReader)) + for i := 0; i < b.N; i++ { + _, _ = byteio.ReadUint64BE(bin) + } +} diff --git a/val_write.go b/val_write.go new file mode 100644 index 0000000..f9ea245 --- /dev/null +++ b/val_write.go @@ -0,0 +1,153 @@ +package byteio + +import "math" + +// WriteUint16BE writes an unsigned uint16 in big-endian (network) byte order. +func WriteUint16BE(bout Writer, n uint16) error { + if err := bout.WriteByte(byte(n >> 8)); err != nil { + return err + } + return bout.WriteByte(byte(n)) +} + +// WriteInt16BE writes a signed int16 in big-endian (network) byte order. +func WriteInt16BE(bout Writer, i int16) error { + return WriteUint16BE(bout, uint16(i)) +} + +// WriteUint32BE writes an unsigned uint32 in big-endian (network) byte order. +func WriteUint32BE(bout Writer, n uint32) error { + if err := bout.WriteByte(byte(n >> 24)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 16)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 8)); err != nil { + return err + } + return bout.WriteByte(byte(n)) +} + +// WriteInt32BE writes a signed int32 in big-endian byte order. +func WriteInt32BE(bout Writer, i int32) error { + return WriteUint32BE(bout, uint32(i)) +} + +// WriteFloat32BE writes an IEEE-754 32-bit floating point number in big-endian +// (network) byte order. +func WriteFloat32BE(bout Writer, f float32) error { + return WriteUint32BE(bout, math.Float32bits(f)) +} + +// WriteUint64BE writes an unsigned uint64 in big-endian (network) byte order. +func WriteUint64BE(bout Writer, n uint64) error { + if err := bout.WriteByte(byte(n >> 56)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 48)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 40)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 32)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 24)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 16)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 8)); err != nil { + return err + } + return bout.WriteByte(byte(n)) +} + +// WriteUint64BE writes a signed int64 in big-endian (network) byte order. +func WriteInt64BE(bout Writer, i int64) error { + return WriteUint64BE(bout, uint64(i)) +} + +// WriteFloat32BE writes an IEEE-754 64-bit floating point number in big-endian +// (network) byte order. +func WriteFloat64BE(bout Writer, f float64) error { + return WriteUint64BE(bout, math.Float64bits(f)) +} + +// WriteUint16LE writes an unsigned uint16 in little-endian byte order. +func WriteUint16LE(bout Writer, n uint16) error { + if err := bout.WriteByte(byte(n)); err != nil { + return err + } + return bout.WriteByte(byte(n >> 8)) +} + +// WriteInt16LE writes a signed int16 in little-endian byte order. +func WriteInt16LE(bout Writer, i int16) error { + return WriteUint16LE(bout, uint16(i)) +} + +// WriteUint32LE writes an unsigned uint32 in little-endian byte order. +func WriteUint32LE(bout Writer, n uint32) error { + if err := bout.WriteByte(byte(n)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 8)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 16)); err != nil { + return err + } + return bout.WriteByte(byte(n >> 24)) +} + +// WriteInt32LE writes a signed int32 in little-endian byte order. +func WriteInt32LE(bout Writer, i int32) error { + return WriteUint32LE(bout, uint32(i)) +} + +// WriteFloat32LE writes an IEEE-754 32-bit floating point number in +// little-endian byte order. +func WriteFloat32LE(bout Writer, f float32) error { + return WriteUint32LE(bout, math.Float32bits(f)) +} + +// WriteUint64LE writes an unsigned uint64 in little-endian byte order. +func WriteUint64LE(bout Writer, n uint64) error { + if err := bout.WriteByte(byte(n)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 8)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 16)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 24)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 32)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 40)); err != nil { + return err + } + if err := bout.WriteByte(byte(n >> 48)); err != nil { + return err + } + return bout.WriteByte(byte(n >> 56)) +} + +// WriteInt64LE writes a signed int64 in little-endian byte order. +func WriteInt64LE(bout Writer, i int64) error { + return WriteUint64LE(bout, uint64(i)) +} + +// WriteFloat64LE writes an IEEE-754 64-bit floating point number in +// little-endian byte order. +func WriteFloat64LE(bout Writer, f float64) error { + return WriteUint64LE(bout, math.Float64bits(f)) +} diff --git a/val_write_test.go b/val_write_test.go new file mode 100644 index 0000000..5fda6b9 --- /dev/null +++ b/val_write_test.go @@ -0,0 +1,260 @@ +package byteio_test + +import ( + "bytes" + "encoding/binary" + "errors" + "math" + "testing" + "unicode/utf8" + + "src.lwithers.me.uk/go/byteio" +) + +// TestWriteUint ensures correct operation of the WriteUint variants by +// verifying written data integrity against encoding/binary. +func TestWriteUint(t *testing.T) { + checkErr := func(err error) { + if err != nil { + t.Fatalf("unexpected I/O error: %v", err) + } + } + + // prepare a buffer of test data + vals := []uint64{0, 1, 0x8000, 0xFFFF, 0x81828384858687} + buf := bytes.NewBuffer(nil) + for _, val := range vals { + checkErr(byteio.WriteUint16BE(buf, uint16(val))) + checkErr(byteio.WriteUint32BE(buf, uint32(val))) + checkErr(byteio.WriteUint64BE(buf, val)) + checkErr(byteio.WriteUint16LE(buf, uint16(val))) + checkErr(byteio.WriteUint32LE(buf, uint32(val))) + checkErr(byteio.WriteUint64LE(buf, val)) + } + + // verify its integrity + for _, exp64 := range vals { + var ( + act16, exp16 uint16 = 0, uint16(exp64) + act32, exp32 uint32 = 0, uint32(exp64) + act64 uint64 + ) + + checkErr(binary.Read(buf, binary.BigEndian, &act16)) + if act16 != exp16 { + t.Errorf("WriteUint16BE: act %X ≠ exp %X", act16, exp16) + } + + checkErr(binary.Read(buf, binary.BigEndian, &act32)) + if act32 != exp32 { + t.Errorf("WriteUint32BE: act %X ≠ exp %X", act32, exp32) + } + + checkErr(binary.Read(buf, binary.BigEndian, &act64)) + if act64 != exp64 { + t.Errorf("WriteUint64BE: act %X ≠ exp %X", act64, exp64) + } + + checkErr(binary.Read(buf, binary.LittleEndian, &act16)) + if act16 != exp16 { + t.Errorf("WriteUint16LE: act %X ≠ exp %X", act16, exp16) + } + + checkErr(binary.Read(buf, binary.LittleEndian, &act32)) + if act32 != exp32 { + t.Errorf("WriteUint32LE: act %X ≠ exp %X", act32, exp32) + } + + checkErr(binary.Read(buf, binary.LittleEndian, &act64)) + if act64 != exp64 { + t.Errorf("WriteUint64LE: act %X ≠ exp %X", act64, exp64) + } + } +} + +// TestWriteInt ensures correct operation of the WriteInt variants by verifying +// written data integrity against encoding/binary. +func TestWriteInt(t *testing.T) { + checkErr := func(err error) { + if err != nil { + t.Fatalf("unexpected I/O error: %v", err) + } + } + + // prepare a buffer of test data + vals := []int64{-1, 0, 1, -0x8000, 0xFFFF, 0x01020304050607} + buf := bytes.NewBuffer(nil) + for _, val := range vals { + checkErr(byteio.WriteInt16BE(buf, int16(val))) + checkErr(byteio.WriteInt32BE(buf, int32(val))) + checkErr(byteio.WriteInt64BE(buf, val)) + checkErr(byteio.WriteInt16LE(buf, int16(val))) + checkErr(byteio.WriteInt32LE(buf, int32(val))) + checkErr(byteio.WriteInt64LE(buf, val)) + } + + // verify its integrity + for _, exp64 := range vals { + var ( + act16, exp16 int16 = 0, int16(exp64) + act32, exp32 int32 = 0, int32(exp64) + act64 int64 + ) + + checkErr(binary.Read(buf, binary.BigEndian, &act16)) + if act16 != exp16 { + t.Errorf("WriteInt16BE: act %X ≠ exp %X", act16, exp16) + } + + checkErr(binary.Read(buf, binary.BigEndian, &act32)) + if act32 != exp32 { + t.Errorf("WriteInt32BE: act %X ≠ exp %X", act32, exp32) + } + + checkErr(binary.Read(buf, binary.BigEndian, &act64)) + if act64 != exp64 { + t.Errorf("WriteInt64BE: act %X ≠ exp %X", act64, exp64) + } + + checkErr(binary.Read(buf, binary.LittleEndian, &act16)) + if act16 != exp16 { + t.Errorf("WriteInt16LE: act %X ≠ exp %X", act16, exp16) + } + + checkErr(binary.Read(buf, binary.LittleEndian, &act32)) + if act32 != exp32 { + t.Errorf("WriteInt32LE: act %X ≠ exp %X", act32, exp32) + } + + checkErr(binary.Read(buf, binary.LittleEndian, &act64)) + if act64 != exp64 { + t.Errorf("WriteInt64LE: act %X ≠ exp %X", act64, exp64) + } + } +} + +// TestWriteFloat verifies that byteio-written floating point numbers can be +// read back correctly using encoding/binary. +func TestWriteFloat(t *testing.T) { + vals := []float64{ + 0, 1, -1, + math.MaxFloat32, math.SmallestNonzeroFloat32, + math.Inf(1), math.Inf(-1), math.NaN(), + } + checkErr := func(err error) { + if err != nil { + t.Fatalf("unexpected I/O error: %v", err) + } + } + + // prepare buffer of encoded data + buf := bytes.NewBuffer(nil) + for _, val := range vals { + checkErr(byteio.WriteFloat32BE(buf, float32(val))) + checkErr(byteio.WriteFloat32LE(buf, float32(val))) + checkErr(byteio.WriteFloat64BE(buf, val)) + checkErr(byteio.WriteFloat64LE(buf, val)) + } + + // verify integrity using encoding/binary + for _, exp64 := range vals { + var ( + act32, exp32 float32 = 0, float32(exp64) + act64 float64 + ) + + checkErr(binary.Read(buf, binary.BigEndian, &act32)) + if act32 != exp32 && !math.IsNaN(float64(act32)) && !math.IsNaN(exp64) { + t.Errorf("act %f (0x%X) ≠ exp %f (0x%X)", + act32, math.Float32bits(act32), + exp32, math.Float32bits(exp32)) + } + checkErr(binary.Read(buf, binary.LittleEndian, &act32)) + if act32 != exp32 && !math.IsNaN(float64(act32)) && !math.IsNaN(exp64) { + t.Errorf("act %f (0x%X) ≠ exp %f (0x%X)", + act32, math.Float32bits(act32), + exp32, math.Float32bits(exp32)) + } + + checkErr(binary.Read(buf, binary.BigEndian, &act64)) + if act64 != exp64 && !math.IsNaN(act64) && !math.IsNaN(exp64) { + t.Errorf("act %f (0x%X) ≠ exp %f (0x%X)", + act64, math.Float64bits(act64), + exp64, math.Float64bits(exp64)) + } + checkErr(binary.Read(buf, binary.LittleEndian, &act64)) + if act64 != exp64 && !math.IsNaN(act64) && !math.IsNaN(exp64) { + t.Errorf("act %f (0x%X) ≠ exp %f (0x%X)", + act64, math.Float64bits(act64), + exp64, math.Float64bits(exp64)) + } + } +} + +// ErrAbortWriter is returned by AbortWriter when the limit is reached. +var ErrAbortWriter = errors.New("aborted write") + +// AbortWriter will abort after a certain number of bytes have been written. It +// implements byteio.Writer since we do not want it to be wrapped by a +// bufio.Writer, which would defer errors until a flush operation. +type AbortWriter struct { + when, cur int +} + +func (aw *AbortWriter) WriteByte(b byte) error { + if aw.when == aw.cur { + return ErrAbortWriter + } + aw.cur++ + return nil +} + +func (aw *AbortWriter) WriteRune(r rune) (int, error) { + len := utf8.RuneLen(r) + for i := 0; i < len; i++ { + if err := aw.WriteByte(' '); err != nil { + return i, err + } + } + return len, nil +} + +func (aw *AbortWriter) Write(buf []byte) (int, error) { + for i, b := range buf { + if err := aw.WriteByte(b); err != nil { + return i, err + } + } + return len(buf), nil +} + +// TestWriteIntErr ensures that an error is correctly returned when writing an +// integer and the underlying writer reports an error. +func TestWriteIntErr(t *testing.T) { + check := func(name string, size int, fn func(bout byteio.Writer) error) { + for i := 0; i <= size; i++ { + err := fn(&AbortWriter{when: i}) + switch err { + case nil: + if i < size { + t.Errorf("%s: expected err after %d "+ + "bytes", name, i) + } + case ErrAbortWriter: + if i == size { + t.Errorf("%s: unexpected err after %d "+ + "bytes", name, i) + } + default: + t.Errorf("%s: unexpected err %v", name, err) + } + } + } + + check("WriteUint16BE", 2, func(bout byteio.Writer) error { return byteio.WriteUint16BE(bout, 0) }) + check("WriteUint32BE", 4, func(bout byteio.Writer) error { return byteio.WriteUint32BE(bout, 0) }) + check("WriteUint64BE", 8, func(bout byteio.Writer) error { return byteio.WriteUint64BE(bout, 0) }) + check("WriteUint16LE", 2, func(bout byteio.Writer) error { return byteio.WriteUint16LE(bout, 0) }) + check("WriteUint32LE", 4, func(bout byteio.Writer) error { return byteio.WriteUint32LE(bout, 0) }) + check("WriteUint64LE", 8, func(bout byteio.Writer) error { return byteio.WriteUint64LE(bout, 0) }) +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..27dd1a2 --- /dev/null +++ b/writer.go @@ -0,0 +1,36 @@ +package byteio + +import ( + "bufio" + "io" +) + +// Writer provides byte-oriented writing routines. It is satisfied by +// bufio.Writer and bytes.Buffer. +type Writer interface { + io.Writer + io.ByteWriter + WriteRune(r rune) (n int, err error) +} + +// NewWriter adapts any io.Writer into a byteio.Writer, possibly returning +// a new bufio.Writer. +func NewWriter(out io.Writer) Writer { + if bout, ok := out.(Writer); ok { + return bout + } + return bufio.NewWriter(out) +} + +type flusher interface { + Flush() error +} + +// FlushIfNecessary tests whether out has a Flush method and if so calls it. If +// out does not have a Flush method this function silently does nothing. +func FlushIfNecessary(out io.Writer) error { + if fout, ok := out.(flusher); ok { + return fout.Flush() + } + return nil +} diff --git a/writer_test.go b/writer_test.go new file mode 100644 index 0000000..943ae64 --- /dev/null +++ b/writer_test.go @@ -0,0 +1,78 @@ +package byteio_test + +import ( + "bufio" + "bytes" + "os" + "testing" + + "src.lwithers.me.uk/go/byteio" +) + +// MockWriter discards data sent to it. It also implements flusher. +type MockWriter struct { + sawFlush bool +} + +func (w *MockWriter) Write(buf []byte) (n int, err error) { + w.sawFlush = false + return len(buf), nil +} + +func (w *MockWriter) Flush() error { + w.sawFlush = true + return nil +} + +// TestNewWriterBytesB checks that a bytes.Buffer is successfully transformed +// into a Writer. +func TestNewWriterBytesB(t *testing.T) { + orig := bytes.NewBuffer(nil) + bin := byteio.NewWriter(orig) + if act, ok := bin.(*bytes.Buffer); !ok { + t.Errorf("Writer(%T) returned unexpected %T", orig, bin) + } else if act != orig { + t.Errorf("Writer(%p) returned unexpected %p", orig, act) + } +} + +// TestNewWriterBufio checks that a bufio.Writer is successfully transformed +// into a Writer. +func TestNewWriterBufio(t *testing.T) { + orig := bufio.NewWriter(os.Stdin) + bin := byteio.NewWriter(orig) + if act, ok := bin.(*bufio.Writer); !ok { + t.Errorf("Writer(%T) returned unexpected %T", orig, bin) + } else if act != orig { + t.Errorf("Writer(%p) returned unexpected %p", orig, act) + } +} + +// TestNewWriter checks that an arbitrary io.Writer is successfully wrapped into +// a byteio.Writer. +func TestNewWriter(t *testing.T) { + orig := new(MockWriter) + bin := byteio.NewWriter(orig) + if _, ok := bin.(*bufio.Writer); !ok { + t.Errorf("Writer(%T) did not wrap to bufio.Writer (got %T)", + orig, bin) + } +} + +// TestFlushIfNecessary checks that no error is returned when called on +// something which does not require flush, and that Flush() is indeed called +// if it is presnet. +func TestFlushIfNecessary(t *testing.T) { + noflush := bytes.NewBuffer(nil) + if err := byteio.FlushIfNecessary(noflush); err != nil { + t.Errorf("unexpected error while not flushing: %v", err) + } + + w := new(MockWriter) + if err := byteio.FlushIfNecessary(w); err != nil { + t.Errorf("unexpected error while flushing: %v", err) + } + if !w.sawFlush { + t.Error("Flush() was not called") + } +}