ADDED:
* iox package
* mapsx package
* netx/inetcksum package
This commit is contained in:
brent saner
2025-12-18 04:47:31 -05:00
parent 6ddfcdb416
commit 145c32268e
19 changed files with 824 additions and 148 deletions

View File

@@ -1,4 +1,7 @@
/*
Package iox includes extensions to the stdlib `io` module.
Not everything in here is considered fully stabilized yet,
but it should be usable.
*/
package iox

View File

@@ -5,5 +5,13 @@ import (
)
var (
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
ErrChunkTooBig error = errors.New("chunk too big for method")
ErrChunkTooSmall error = errors.New("chunk too small for buffer")
ErrInvalidChunkSize error = errors.New("an invalid chunk size was passed")
ErrNilCtx error = errors.New("a nil context was passed")
ErrNilReader error = errors.New("a nil reader was passed")
ErrNilWriter error = errors.New("a nil writer was passed")
ErrShortRead error = errors.New("a read was cut short with no EOF")
ErrShortWrite error = errors.New("a write was cut short with no error")
)

View File

@@ -1,20 +1,21 @@
package iox
import (
`context`
`io`
)
/*
CopyBufN is a mix between io.CopyN and io.CopyBuffer.
CopyBufN is a mix between [io.CopyN] and [io.CopyBuffer].
Despite what the docs may suggest, io.CopyN does NOT *read* n bytes from src AND write n bytes to dst.
Despite what the docs may suggest, [io.CopyN] does NOT *read* n bytes from src AND write n bytes to dst.
Instead, it always reads 32 KiB from src, and writes n bytes to dst.
There are, of course, cases where this is deadfully undesired.
There are cases where this is dreadfully undesired.
One can, of course, use io.CopyBuffer, but this is a bit annoying since you then have to provide a buffer yourself.
One can, of course, use [io.CopyBuffer], but this is a bit annoying since you then have to provide a buffer yourself.
This convenience-wraps io.CopyBuffer to have a similar signature to io.CopyN but properly uses n for both reading and writing.
This convenience-wraps [io.CopyBuffer] to have a similar signature to [io.CopyN] but properly uses n for both reading and writing.
*/
func CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
@@ -32,10 +33,215 @@ func CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
return
}
// CopyBufWith allows for specifying a buffer allocator function, otherwise acts as CopyBufN.
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
// CopyCtxBufN copies from `src` to `dst`, `n` bytes at a time, interruptible by `ctx`.
func CopyCtxBufN(ctx context.Context, dst io.Writer, src io.Reader, n int64) (written int64, err error) {
written, err = io.CopyBuffer(dst, src, bufFunc())
var nr int
var nw int
var end bool
var buf []byte
if ctx == nil {
err = ErrNilCtx
return
}
if n <= 0 {
err = ErrBufTooSmall
return
}
endCopy:
for {
select {
case <-ctx.Done():
err = ctx.Err()
return
default:
buf = make([]byte, n)
nr, err = src.Read(buf)
if err == io.EOF {
err = nil
end = true
} else if err != nil {
return
}
buf = buf[:nr]
if nw, err = dst.Write(buf); err != nil {
written += int64(nw)
return
}
written += int64(nw)
if len(buf) != nw {
err = io.ErrShortWrite
return
}
if end {
break endCopy
}
}
}
return
}
/*
CopyBufWith allows for specifying a buffer allocator function, otherwise acts as [CopyBufN].
bufFunc *MUST NOT* return a nil or len == 0 buffer. [ErrBufTooSmall] will be returned if it does.
This uses a fixed buffer size from a single call to `bufFunc`.
If you need something with dynamic buffer sizing according to some state, use [CopyBufWithDynamic] instead.
(Note that CopyBufWithDynamic is generally a little slower, but it should only be noticeable on very large amounts of data.)
*/
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
var buf []byte = bufFunc()
if buf == nil || len(buf) == 0 {
err = ErrBufTooSmall
return
}
written, err = io.CopyBuffer(dst, src, buf)
return
}
/*
CopyBufWithDynamic is like [CopyBufWith] except it will call bufFunc after each previous buffer is written.
That is to say (using a particularly contrived example):
import time
func dynBuf() (b []byte) {
var t time.Time = time.Now()
b = make([]byte, t.Seconds())
return
}
Then:
CopyBufWithDynamic(w, r, dynBuf)
will use a buffer sized to the seconds of the time it reads in/writes out the next buffer, whereas with [CopyBufWith]:
CopyBufWith(w, r, dynBuf)
would use a *fixed* buffer size of whatever the seconds was equal to at the time of the *first call* to dynBuf.
`src` MUST return an [io.EOF] when its end is reached, but (as per e.g. [io.CopyBuffer]) the io.EOF error will not
be returned from CopyBufWithDynamic. (Any/all other errors encountered will be returned, however, and copying will
immediately cease.)
*/
func CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
var nr int
var nw int
var end bool
var buf []byte
for {
buf = bufFunc()
if buf == nil || len(buf) == 0 {
err = ErrBufTooSmall
return
}
nr, err = src.Read(buf)
if err == io.EOF {
err = nil
end = true
} else if err != nil {
return
}
buf = buf[:nr]
if nw, err = dst.Write(buf); err != nil {
written += int64(nw)
return
}
written += int64(nw)
if len(buf) != nw {
err = ErrShortWrite
return
}
if end {
break
}
}
return
}
// NewChunker returns a [ChunkLocker] ready to use.
func NewChunker(chunkSize uint) (c *ChunkLocker, err error) {
c = &ChunkLocker{}
err = c.SetChunkLen(chunkSize)
return
}
// NewCtxIO returns a [CtxIO].
func NewCtxIO(ctx context.Context, r io.Reader, w io.Writer, chunkSize uint) (c *CtxIO, err error) {
if r == nil {
err = ErrNilReader
return
}
if w == nil {
err = ErrNilWriter
return
}
if chunkSize == 0 {
err = ErrInvalidChunkSize
return
}
if ctx == nil {
err = ErrNilCtx
return
}
c = &CtxIO{
r: r,
w: w,
l: ChunkLocker{
chunkLen: chunkSize,
},
ctx: ctx,
}
return
}
/*
NewXIO returns a nil [XIO].
A weird "feature" of Golang is that a nil XIO is perfectly fine to use;
it's completely stateless and only has pointer receivers that only work with passed in
values so `new(XIO)` is completely unnecessary (as is NewXCopier).
In other words, this works fine:
var xc *iox.XIO
if n, err = xc.Copy(w, r); err != nil {
return
}
This function is just to maintain cleaner-looking code if you should so need it,
or want an XIO without declaring one:
if n, err = iox.NewXCopier().Copy(w, r); err != nil {
return
}
*/
func NewXIO() (x *XIO) {
// No-op lel
return
}

28
iox/funcs_chunklocker.go Normal file
View File

@@ -0,0 +1,28 @@
package iox
// GetChunkLen returns the current chunk size/length in bytes.
func (c *ChunkLocker) GetChunkLen() (size uint) {
c.lock.RLock()
defer c.lock.RUnlock()
size = c.chunkLen
return
}
// SetChunkLen sets the current chunk size/length in bytes.
func (c *ChunkLocker) SetChunkLen(size uint) (err error) {
if size == 0 {
err = ErrInvalidChunkSize
return
}
c.lock.Lock()
defer c.lock.Unlock()
c.chunkLen = size
return
}

173
iox/funcs_ctxio.go Normal file
View File

@@ -0,0 +1,173 @@
package iox
import (
`bytes`
`context`
`io`
`math`
)
func (c *CtxIO) Copy(dst io.Writer, src io.Reader) (written int64, err error) {
if c.l.chunkLen > math.MaxInt64 {
err = ErrChunkTooBig
}
return CopyCtxBufN(c.ctx, dst, src, int64(c.l.chunkLen))
}
func (c *CtxIO) CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
if n <= 0 {
err = ErrBufTooSmall
return
}
return CopyCtxBufN(c.ctx, dst, src, n)
}
func (c *CtxIO) GetChunkLen() (size uint) {
return c.l.GetChunkLen()
}
func (c *CtxIO) Read(p []byte) (n int, err error) {
var nr int64
if nr, err = c.ReadWithContext(c.ctx, p); err != nil {
if nr > math.MaxInt {
n = math.MaxInt
} else {
n = int(nr)
}
return
}
if nr > math.MaxInt {
n = math.MaxInt
} else {
n = int(nr)
}
return
}
func (c *CtxIO) ReadWithContext(ctx context.Context, p []byte) (n int64, err error) {
var nr int
var off int
var buf []byte
if p == nil || len(p) == 0 {
return
}
if c.buf.Len() == 0 {
err = io.EOF
return
}
if c.l.chunkLen > uint(len(p)) {
// Would normally be a single chunk, so one-shot it.
nr, err = c.buf.Read(p)
n = int64(nr)
return
}
// Chunk over it.
endRead:
for {
select {
case <-ctx.Done():
err = ctx.Err()
return
default:
/*
off(set) is the index of the *next position* to write to.
Therefore the last offset == len(p),
therefore:
* if off == len(p), "done" (return no error, do *not* read from buf)
* if off + c.l.chunkLen > len(p), buf should be len(p) - off instead
*/
if off == len(p) {
break endRead
}
if uint(off)+c.l.chunkLen > uint(len(p)) {
buf = make([]byte, len(p)-off)
} else {
buf = make([]byte, c.l.chunkLen)
}
nr, err = c.buf.Read(buf)
n += int64(nr)
if nr > 0 {
off += nr
copy(p[off:], buf[:nr])
}
if err == io.EOF {
break endRead
} else if err != nil {
return
}
}
}
return
}
func (c *CtxIO) SetChunkLen(size uint) (err error) {
return c.l.SetChunkLen(size)
}
func (c *CtxIO) SetContext(ctx context.Context) (err error) {
if ctx == nil {
err = ErrNilCtx
return
}
c.ctx = ctx
return
}
func (c *CtxIO) Write(p []byte) (n int, err error) {
var nw int64
if c.l.chunkLen > math.MaxInt64 {
err = ErrChunkTooBig
return
}
if nw, err = c.WriteNWithContext(c.ctx, p, int64(c.l.chunkLen)); err != nil {
if nw > math.MaxInt {
n = math.MaxInt
} else {
n = int(nw)
}
return
}
if nw > math.MaxInt {
n = math.MaxInt
} else {
n = int(nw)
}
return
}
func (c *CtxIO) WriteNWithContext(ctx context.Context, p []byte, n int64) (written int64, err error) {
return CopyCtxBufN(ctx, &c.buf, bytes.NewReader(p), n)
}
func (c *CtxIO) WriteRune(r rune) (n int, err error) {
// We don't even bother listening for the ctx.Done because it's a single rune.
n, err = c.buf.WriteRune(r)
return
}
func (c *CtxIO) WriteWithContext(ctx context.Context, p []byte) (n int64, err error) {
if c.l.chunkLen > math.MaxInt64 {
err = ErrChunkTooBig
return
}
return CopyCtxBufN(ctx, &c.buf, bytes.NewReader(p), int64(c.l.chunkLen))
}

40
iox/funcs_xio.go Normal file
View File

@@ -0,0 +1,40 @@
package iox
import (
`io`
)
// Copy copies [io.Reader] `src` to [io.Writer] `dst`. It implements [Copier].
func (x *XIO) Copy(dst io.Writer, src io.Reader) (written int64, err error) {
return io.Copy(dst, src)
}
// CopyBuffer copies [io.Reader] `src` to [io.Writer] `dst` using buffer `buf`. It implements [CopyBufferer].
func (x *XIO) CopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) {
return io.CopyBuffer(dst, src, buf)
}
// CopyBufWith copies [io.Reader] `src` to [io.Writer] `dst` using buffer returner `bufFunc`. It implements [SizedCopyBufferInvoker].
func (x *XIO) CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
return CopyBufWith(dst, src, bufFunc)
}
// CopyBufWithDynamic copies [io.Reader] `src` to [io.Writer] `dst` using buffer returner `bufFunc` for each chunk. It implements [DynamicSizedCopyBufferInvoker].
func (x *XIO) CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
return CopyBufWithDynamic(dst, src, bufFunc)
}
/*
CopyBufN reads buffered bytes from [io.Reader] `src` and copies to [io.Writer] `dst`
using the synchronous buffer size `n`.
It implements [SizedCopyBufferer].
*/
func (x *XIO) CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
return CopyBufN(dst, src, n)
}
// CopyN copies from [io.Reader] `src` to [io.Writer] `w`, `n` bytes at a time. It implements [SizedCopier].
func (x *XIO) CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
return io.CopyN(dst, src, n)
}

View File

@@ -1,8 +1,209 @@
package iox
import (
`bytes`
`context`
`io`
`sync`
)
type (
// RuneWriter matches the behavior of *(bytes.Buffer).WriteRune and *(bufio.Writer).WriteRune
/*
RuneWriter matches the behavior of [bytes.Buffer.WriteRune] and [bufio.Writer.WriteRune].
(Note that this package does not have a "RuneReader"; see [io.RuneReader] instead.)
*/
RuneWriter interface {
WriteRune(r rune) (n int, err error)
}
// Copier matches the signature/behavior of [io.Copy]. Implemented by [XIO].
Copier interface {
Copy(dst io.Writer, src io.Reader) (written int64, err error)
}
// CopyBufferer matches the signature/behavior of [io.CopyBuffer]. Implemented by [XIO].
CopyBufferer interface {
CopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error)
}
// SizedCopier matches the signature/behavior of [io.CopyN]. Implemented by [XIO].
SizedCopier interface {
CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
}
// SizedCopyBufferer matches the signature/behavior of [CopyBufN]. Implemented by [XIO].
SizedCopyBufferer interface {
CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
}
// SizedCopyBufferInvoker matches the signature/behavior of [CopyBufWith]. Implemented by [XIO].
SizedCopyBufferInvoker interface {
CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error)
}
// DynamicSizedCopyBufferInvoker matches the signature/behavior of [CopyBufWithDynamic]. Implemented by [XIO].
DynamicSizedCopyBufferInvoker interface {
CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error)
}
/*
Chunker is used by both [ContextReader] and [ContextWriter] to set/get the current chunk size.
Chunking is inherently required to be specified in order to interrupt reads/writes/copies with a [context.Context].
Implementations *must* use a [sync.RWMutex] to get (RLock) and set (Lock) the chunk size.
The chunk size *must not* be directly accessible to maintain concurrency safety assumptions.
*/
Chunker interface {
// GetChunkLen returns the current chunk size/length in bytes.
GetChunkLen() (size uint)
// SetChunkLen sets the current chunk size/length in bytes.
SetChunkLen(size uint) (err error)
}
/*
ChunkReader implements a chunking reader.
Third-party implementations *must* respect the chunk size locking (see [Chunker]).
The Read method should read in chunks of the internal chunk size.
*/
ChunkReader interface {
io.Reader
Chunker
}
/*
ChunkWriter implements a chunking writer.
Third-party implementations *must* respect the chunk size locking (see [Chunker]).
The Write method should write out in chunks of the internal chunk size.
*/
ChunkWriter interface {
io.Writer
Chunker
}
// ChunkReadWriter implements a chunking reader/writer.
ChunkReadWriter interface {
ChunkReader
ChunkWriter
}
/*
ContextSetter allows one to set an internal context.
A nil context should return an error.
*/
ContextSetter interface {
SetContext(context context.Context) (err error)
}
/*
ContextCopier is defined to allow for consumer-provided types. See [CtxIO] for a package-provided type.
The Copy method should use an internal context and chunk size
(and thus wrap [CopyCtxBufN] internally on an external call to Copy, etc.).
*/
ContextCopier interface {
Copier
Chunker
ContextSetter
SizedCopyBufferer
}
/*
ContextReader is primarily here to allow for consumer-provided types. See [CtxIO] for a package-provided type.
The Read method should use an internal context and chunk size.
The ReadWithContext method should use an internal chunk size.
*/
ContextReader interface {
ChunkReader
ContextSetter
ReadWithContext(ctx context.Context, p []byte) (n int64, err error)
}
/*
ContextWriter is primarily here to allow for consumer-provided types. See [CtxIO] for a package-provided type.
The Write method should use an internal context.
The WriteWithContext should use an internal chunk size.
*/
ContextWriter interface {
ChunkWriter
ContextSetter
WriteWithContext(ctx context.Context, p []byte) (n int64, err error)
WriteNWithContext(ctx context.Context, p []byte, n int64) (written int64, err error)
}
/*
ContextReadWriter is primarily here to allow for consumer-provided types.
See [CtxIO] for a package-provided type.
*/
ContextReadWriter interface {
ContextReader
ContextWriter
}
)
type (
// ChunkLocker implements [Chunker].
ChunkLocker struct {
lock sync.RWMutex
chunkLen uint
}
/*
CtxIO is a type used to demonstrate "stateful" I/O introduced by this package.
It implements:
* [Copier]
* [Chunker]
* [RuneWriter]
* [ChunkReader]
* [ChunkWriter]
* [ContextCopier]
* [ContextSetter]
* [ContextReader]
* [ContextWriter]
* [ChunkReadWriter]
* [ContextReadWriter]
* [SizedCopyBufferer]
Unlike [XIO], it must be non-nil (see [NewCtxIO]) since it maintains state
(though technically, one does not need to call [NewCtxIO] if they call
[CtxIO.SetChunkLen] and [CtxIO.SetContext] before any other methods).
[CtxIO.Read] and other Read methods writes to an internal buffer,
and [CtxIO.Write] and other Write methods writes out from it.
*/
CtxIO struct {
r io.Reader
w io.Writer
l ChunkLocker
buf bytes.Buffer
ctx context.Context
}
/*
XIO is a type used to demonstrate "stateless" I/O introduced by this package.
It implements:
* [Copier]
* [CopyBufferer]
* [SizedCopier]
* [SizedCopyBufferer]
* [SizedCopyBufferInvoker]
* [DynamicSizedCopyBufferInvoker]
Unlike [CtxIO], the zero-value is ready to use since it holds no state
or configuration whatsoever.
A nil XIO is perfectly usable but if you want something more idiomatic,
see [NewXIO].
*/
XIO struct{}
)