Initial commit; import from github.com/lwithers/pkg

This commit is contained in:
Laurence Withers 2020-01-03 13:42:38 +00:00
commit 08044ce5ef
14 changed files with 1389 additions and 0 deletions

19
LICENSE Normal file
View File

@ -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.

57
README.md Normal file
View File

@ -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.

25
doc.go Normal file
View File

@ -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

64
example_test.go Normal file
View File

@ -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 |........|
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module src.lwithers.me.uk/go/byteio
go 1.13

0
go.sum Normal file
View File

23
reader.go Normal file
View File

@ -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)
}

71
reader_test.go Normal file
View File

@ -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)
}
}

271
val_read.go Normal file
View File

@ -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
}

329
val_read_test.go Normal file
View File

@ -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)
}
}

153
val_write.go Normal file
View File

@ -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))
}

260
val_write_test.go Normal file
View File

@ -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) })
}

36
writer.go Normal file
View File

@ -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
}

78
writer_test.go Normal file
View File

@ -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")
}
}