Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
145c32268e
|
||
|
|
6ddfcdb416
|
||
|
|
79f10b7611
|
||
|
|
01adbfc605
|
||
|
|
b1d8ea34a6
|
||
|
|
e101758187
|
11
go.mod
11
go.mod
@@ -1,15 +1,16 @@
|
|||||||
module r00t2.io/goutils
|
module r00t2.io/goutils
|
||||||
|
|
||||||
go 1.24.5
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0
|
github.com/coreos/go-systemd/v22 v22.6.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
golang.org/x/sys v0.34.0
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
r00t2.io/sysutils v1.14.0
|
golang.org/x/sys v0.39.0
|
||||||
|
r00t2.io/sysutils v1.15.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/djherbis/times v1.6.0 // indirect
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
18
go.sum
18
go.sum
@@ -1,13 +1,15 @@
|
|||||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw=
|
r00t2.io/sysutils v1.15.0 h1:FSnREfbXDhBQEO7LMpnRQeKlPshozxk9XHw3YgWRgRg=
|
||||||
|
r00t2.io/sysutils v1.15.0/go.mod h1:28qB0074EIRQ8Sy/ybaA5jC3qA32iW2aYLkMCRhyAFM=
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Package iox includes extensions to the stdlib `io` module.
|
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
|
package iox
|
||||||
|
|||||||
@@ -6,4 +6,12 @@ import (
|
|||||||
|
|
||||||
var (
|
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")
|
||||||
)
|
)
|
||||||
|
|||||||
222
iox/funcs.go
222
iox/funcs.go
@@ -1,20 +1,21 @@
|
|||||||
package iox
|
package iox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
`context`
|
||||||
`io`
|
`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.
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyBufWith allows for specifying a buffer allocator function, otherwise acts as CopyBufN.
|
// CopyCtxBufN copies from `src` to `dst`, `n` bytes at a time, interruptible by `ctx`.
|
||||||
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
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
|
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
28
iox/funcs_chunklocker.go
Normal 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
173
iox/funcs_ctxio.go
Normal 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
40
iox/funcs_xio.go
Normal 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)
|
||||||
|
}
|
||||||
203
iox/types.go
203
iox/types.go
@@ -1,8 +1,209 @@
|
|||||||
package iox
|
package iox
|
||||||
|
|
||||||
|
import (
|
||||||
|
`bytes`
|
||||||
|
`context`
|
||||||
|
`io`
|
||||||
|
`sync`
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
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 {
|
RuneWriter interface {
|
||||||
WriteRune(r rune) (n int, err error)
|
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{}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
- logging probably needs mutexes
|
||||||
|
|
||||||
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System".
|
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System".
|
||||||
-- https://developer.apple.com/documentation/os/logging
|
-- https://developer.apple.com/documentation/os/logging
|
||||||
-- https://developer.apple.com/documentation/os/generating-log-messages-from-your-code
|
-- https://developer.apple.com/documentation/os/generating-log-messages-from-your-code
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
||||||
Currently this will almost always return a WinLogger.
|
Currently this will almost always return a WinLogger.
|
||||||
*/
|
*/
|
||||||
func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, logFlags int, logPaths ...string) (err error) {
|
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
|
||||||
|
|
||||||
var l Logger
|
var l Logger
|
||||||
var exists bool
|
var exists bool
|
||||||
@@ -36,9 +36,9 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if logPaths != nil {
|
if logPaths != nil {
|
||||||
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...)
|
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...)
|
||||||
} else {
|
} else {
|
||||||
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags)
|
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,32 +10,63 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GetLogger returns an instance of Logger that best suits your system's capabilities. Note that this is a VERY generalized interface to the Windows Event Log.
|
GetLogger returns an instance of Logger that best suits your system's capabilities.
|
||||||
|
Note that this is a VERY generalized interface to the Windows Event Log to conform with multiplatform compat.
|
||||||
|
You'd have a little more flexibility with [GetLoggerWindows] (this function wraps that one).
|
||||||
|
If you need more custom behavior than that, I recommend using [golang.org/x/sys/windows/svc/eventlog] directly
|
||||||
|
(or using another logging module).
|
||||||
|
|
||||||
|
If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).
|
||||||
|
|
||||||
|
The `prefix` correlates to the `source` parameter in [GetLoggerWindows], and this function inherently uses [DefaultEventID],
|
||||||
|
but otherwise it remains the same as [GetLoggerWindows] - refer to it for documentation on the other parameters.
|
||||||
|
|
||||||
|
If you call [GetLogger], you will only get a single ("best") logger your system supports.
|
||||||
|
If you want to log to multiple [Logger] destinations at once (or want to log to an explicit [Logger] type),
|
||||||
|
use [GetMultiLogger].
|
||||||
|
*/
|
||||||
|
func GetLogger(enableDebug bool, prefix string, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
||||||
|
|
||||||
|
if logger, err = GetLoggerWindows(enableDebug, prefix, DefaultEventID, logConfigFlags, logPaths...); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetLoggerWindows returns an instance of Logger that best suits your system's capabilities.
|
||||||
|
This is a slightly less (but still quite) generalized interface to the Windows Event Log than [GetLogger].
|
||||||
|
|
||||||
If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
|
If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
|
||||||
you will want to set up your own logger (golang.org/x/sys/windows/svc/eventlog).
|
you will want to set up your own logger via [golang.org/x/sys/windows/svc/eventlog].
|
||||||
|
|
||||||
If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).
|
If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data)
|
||||||
|
are rendered and written (otherwise they are ignored).
|
||||||
|
|
||||||
A blank source will return an error as it's used as the source name. Other functions, struct fields, etc. will refer to this as the "prefix".
|
A blank `source` will return an error as it's used as the source name.
|
||||||
|
Throughout the rest of this documentation you will see this referred to as the `prefix` to remain platform-agnostic.
|
||||||
|
|
||||||
A pointer to a WinEventID struct may be specified for eventIDs to map extended logging levels (as Windows only supports three levels natively).
|
A pointer to a [WinEventID] struct may be specified for `eventIDs` to map extended logging levels
|
||||||
|
(as Windows only supports three levels natively).
|
||||||
If it is nil, a default one (DefaultEventID) will be used.
|
If it is nil, a default one (DefaultEventID) will be used.
|
||||||
|
|
||||||
logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and
|
`logConfigFlags` is the corresponding flag(s) OR'd for [StdLogger.LogFlags] (and/or the [StdLogger.LogFlags] for [FileLogger])
|
||||||
https://pkg.go.dev/log#pkg-constants for details.
|
if either is selected. See [StdLogger.LogFlags] and [stdlib log's constants] for details.
|
||||||
|
|
||||||
logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to,
|
`logPaths` is an (optional) list of strings to use as paths to test for writing.
|
||||||
it will be used (assuming you have no higher-level loggers available).
|
If the file can be created/written to, it will be used (assuming you have no higher-level loggers available).
|
||||||
|
|
||||||
Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
Only the first `logPaths` entry that "works" will be used, later entries will be ignored.
|
||||||
Currently this will almost always return a WinLogger.
|
Currently this will almost always return a [WinLogger].
|
||||||
|
|
||||||
If you call GetLogger, you will only get a single ("best") logger your system supports.
|
If you call [GetLoggerWindows], you will only get a single ("best") logger your system supports.
|
||||||
If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type),
|
If you want to log to multiple [Logger] destinations at once (or want to log to an explicit [Logger] type),
|
||||||
use GetMultiLogger.
|
use [GetMultiLogger].
|
||||||
|
|
||||||
|
[stdlib log's constants]: https://pkg.go.dev/log#pkg-constants
|
||||||
*/
|
*/
|
||||||
func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
func GetLoggerWindows(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
||||||
|
|
||||||
var logPath string
|
var logPath string
|
||||||
var logFlags bitmask.MaskBit
|
var logFlags bitmask.MaskBit
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func TestDefaultLogger(t *testing.T) {
|
|||||||
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if l, err = GetLogger(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
|
if l, err = GetLoggerWindows(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
|
||||||
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
|
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func TestMultiLogger(t *testing.T) {
|
|||||||
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
|
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = l.AddDefaultLogger("DefaultLogger", DefaultEventID, logFlags, tempfilePath); err != nil {
|
if err = l.AddDefaultLogger("DefaultLogger", logFlags, tempfilePath); err != nil {
|
||||||
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
|
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
mapsx/doc.go
Normal file
4
mapsx/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
Package mapsx includes functions that probably should have been in [maps] but aren't.
|
||||||
|
*/
|
||||||
|
package mapsx
|
||||||
9
mapsx/errs.go
Normal file
9
mapsx/errs.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package mapsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`errors`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("key not found")
|
||||||
|
)
|
||||||
43
mapsx/funcs.go
Normal file
43
mapsx/funcs.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package mapsx
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get mimics Python's [dict.get()] behavior, returning value `v` if key `k`
|
||||||
|
is not found in map `m`.
|
||||||
|
|
||||||
|
See also [GetOk], [Must].
|
||||||
|
|
||||||
|
[dict.get()]: https://docs.python.org/3/library/stdtypes.html#dict.get
|
||||||
|
*/
|
||||||
|
func Get[Map ~map[K]V, K comparable, V any](m Map, k K, v V) (val V) {
|
||||||
|
|
||||||
|
val, _ = GetOk(m, k, v)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOk is like [Get] but also explicitly indicates whether `k` was found or not. See also [Must].
|
||||||
|
func GetOk[Map ~map[K]V, K comparable, V any](m Map, k K, v V) (val V, found bool) {
|
||||||
|
|
||||||
|
if val, found = m[k]; !found {
|
||||||
|
val = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Must, unlike [Get] or [GetOk], requires that `k` be in map `m`.
|
||||||
|
|
||||||
|
A panic with error [ErrNotFound] will be raised if `k` is not present.
|
||||||
|
Otherwise the found value will be returned.
|
||||||
|
*/
|
||||||
|
func Must[Map ~map[K]V, K comparable, V any](m Map, k K) (val V) {
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if val, ok = m[k]; !ok {
|
||||||
|
panic(ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
13
netx/consts_nix.go
Normal file
13
netx/consts_nix.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`golang.org/x/sys/unix`
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AFUnspec uint16 = unix.AF_UNSPEC
|
||||||
|
AFInet uint16 = unix.AF_INET
|
||||||
|
AFInet6 uint16 = unix.AF_INET6
|
||||||
|
)
|
||||||
13
netx/consts_windows.go
Normal file
13
netx/consts_windows.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`golang.org/x/sys/windows`
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AFUnspec uint16 = windows.AF_UNSPEC
|
||||||
|
AFInet uint16 = windows.AF_INET
|
||||||
|
AFInet6 uint16 = windows.AF_INET6
|
||||||
|
)
|
||||||
10
netx/errors.go
Normal file
10
netx/errors.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`errors`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBadMask4Str error = errors.New("netx: unknown/bad IPv4 netmask dotted quad")
|
||||||
|
ErrBadNetFam error = errors.New("netx: unknown/bad IP network family")
|
||||||
|
)
|
||||||
410
netx/funcs.go
Normal file
410
netx/funcs.go
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`math/bits`
|
||||||
|
`net`
|
||||||
|
`net/netip`
|
||||||
|
`strconv`
|
||||||
|
`strings`
|
||||||
|
|
||||||
|
`go4.org/netipx`
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
AddrRfc returns an RFC-friendly string from an IP address ([net/netip.Addr]).
|
||||||
|
|
||||||
|
If addr is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1").
|
||||||
|
|
||||||
|
If addr is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]").
|
||||||
|
|
||||||
|
If the version can't be determined, rfcStr will be an empty string.
|
||||||
|
*/
|
||||||
|
func AddrRfc(addr netip.Addr) (rfcStr string) {
|
||||||
|
|
||||||
|
if addr.Is4() {
|
||||||
|
rfcStr = addr.String()
|
||||||
|
} else if addr.Is6() {
|
||||||
|
rfcStr = "[" + addr.String() + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Cidr4ToIPMask takes an IPv4 CIDR/bit size/prefix length and returns the [net.IPMask].
|
||||||
|
It's (essentially) the inverse of [net.IPMask.Size].
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Cidr4ToMask]
|
||||||
|
* [Cidr4ToStr]
|
||||||
|
|
||||||
|
Inverse of [IPMask4ToCidr].
|
||||||
|
*/
|
||||||
|
func Cidr4ToIPMask(cidr uint8) (ipMask net.IPMask, err error) {
|
||||||
|
|
||||||
|
if cidr > 32 {
|
||||||
|
err = ErrBadNetFam
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ipMask = net.CIDRMask(int(cidr), 32)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Cidr4ToMask takes an IPv4 CIDR/bit size/prefix length and returns the netmask *in bitmask form*.
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Cidr4ToIPMask]
|
||||||
|
* [Cidr4ToStr]
|
||||||
|
|
||||||
|
Inverse of [Mask4ToCidr].
|
||||||
|
*/
|
||||||
|
func Cidr4ToMask(cidr uint8) (mask uint32, err error) {
|
||||||
|
|
||||||
|
if cidr > 32 {
|
||||||
|
err = ErrBadNetFam
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// COULD do (1 << 32) - (1 << (32 - ip.Bits())) instead but in EXTREME edge cases that could cause an overflow.
|
||||||
|
// We're basically converting the CIDR size ("number of bits"/"number of ones") to an integer mask ("number AS bits")
|
||||||
|
mask = uint32(0xffffffff) << uint32(32-cidr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Cidr4ToStr is a convenience wrapper around [IPMask4ToStr]([Cidr4ToMask](cidr)).
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Cidr4ToIPMask]
|
||||||
|
* [Cidr4ToMask]
|
||||||
|
|
||||||
|
Inverse of [Mask4StrToCidr].
|
||||||
|
*/
|
||||||
|
func Cidr4ToStr(cidr uint8) (maskStr string, err error) {
|
||||||
|
|
||||||
|
var ipMask net.IPMask
|
||||||
|
|
||||||
|
if ipMask, err = Cidr4ToIPMask(cidr); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if maskStr, err = IPMask4ToStr(ipMask); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetAddrFamily returns the network family of a [net/netip.Addr].
|
||||||
|
|
||||||
|
See also [GetIpFamily].
|
||||||
|
|
||||||
|
If addr is not a "valid" IP address or the version can't be determined, family will be AFUnspec (usually 0x00/0).
|
||||||
|
*/
|
||||||
|
func GetAddrFamily(addr netip.Addr) (family uint16) {
|
||||||
|
|
||||||
|
family = AFUnspec
|
||||||
|
|
||||||
|
if !addr.IsValid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Is4() {
|
||||||
|
family = AFInet
|
||||||
|
} else if addr.Is6() {
|
||||||
|
family = AFInet6
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetIpFamily returns the network family of a [net.IP].
|
||||||
|
|
||||||
|
See also [GetAddrFamily].
|
||||||
|
|
||||||
|
If ip is not a "valid" IP address or the version can't be determined,
|
||||||
|
family will be [golang.org/x/sys/unix.AF_UNSPEC] or [golang.org/x/sys/windows.AF_UNSPEC] depending on platform (usually 0x00/0).
|
||||||
|
*/
|
||||||
|
func GetIpFamily(ip net.IP) (family uint16) {
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
var addr netip.Addr
|
||||||
|
|
||||||
|
if addr, ok = netipx.FromStdIP(ip); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
family = GetAddrFamily(addr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IpRfc returns an RFC-friendly string from an IP address ([net.IP]).
|
||||||
|
|
||||||
|
If ip is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1").
|
||||||
|
|
||||||
|
If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]").
|
||||||
|
|
||||||
|
If the version can't be determined, rfcStr will be an empty string.
|
||||||
|
*/
|
||||||
|
func IpRfc(ip net.IP) (rfcStr string) {
|
||||||
|
|
||||||
|
if ip.To4() != nil {
|
||||||
|
rfcStr = ip.To4().String()
|
||||||
|
} else if ip.To16() != nil {
|
||||||
|
rfcStr = "[" + ip.To16().String() + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask].
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [IPMask4ToMask]
|
||||||
|
* [IPMask4ToStr]
|
||||||
|
|
||||||
|
Inverse of [Cidr4ToIPMask].
|
||||||
|
*/
|
||||||
|
func IPMask4ToCidr(ipMask net.IPMask) (cidr uint8, err error) {
|
||||||
|
|
||||||
|
var ones int
|
||||||
|
var total int
|
||||||
|
|
||||||
|
ones, total = ipMask.Size()
|
||||||
|
|
||||||
|
if total != 32 {
|
||||||
|
err = ErrBadNetFam
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ones > 32 {
|
||||||
|
err = ErrBadNetFam
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cidr = uint8(ones)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IPMask4ToMask returns the mask *in bitmask form* from a [net.IPMask].
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [IPMask4ToCidr]
|
||||||
|
* [IPMask4ToStr]
|
||||||
|
|
||||||
|
Inverse of [Mask4ToIPMask].
|
||||||
|
*/
|
||||||
|
func IPMask4ToMask(ipMask net.IPMask) (mask uint32, err error) {
|
||||||
|
|
||||||
|
var cidr uint8
|
||||||
|
|
||||||
|
if cidr, err = IPMask4ToCidr(ipMask); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mask, err = Cidr4ToMask(cidr); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IPMask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a [net.IPMask].
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [IPMask4ToCidr]
|
||||||
|
* [IPMask4ToMask]
|
||||||
|
|
||||||
|
Inverse of [Mask4StrToIPMask].
|
||||||
|
*/
|
||||||
|
func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var b []byte
|
||||||
|
var quads []string = make([]string, 4)
|
||||||
|
|
||||||
|
b = []byte(ipMask)
|
||||||
|
if len(b) != 4 {
|
||||||
|
err = ErrBadNetFam
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx = 0; idx < len(b); idx++ {
|
||||||
|
quads[idx] = strconv.Itoa(int(b[idx]))
|
||||||
|
}
|
||||||
|
|
||||||
|
maskStr = strings.Join(quads, ".")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mask4ToCidr converts an IPv4 netmask *in bitmask form* to a CIDR prefix size/bit size/bit length.
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Mask4ToIPMask]
|
||||||
|
* [Mask4ToStr]
|
||||||
|
|
||||||
|
Inverse of [Cidr4ToMask].
|
||||||
|
*/
|
||||||
|
func Mask4ToCidr(mask uint32) (cidr uint8, err error) {
|
||||||
|
|
||||||
|
cidr = 32 - uint8(bits.LeadingZeros32(mask))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mask4ToIPMask returns mask *in bitmask form* as a [net.IPMask].
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Mask4ToCidr]
|
||||||
|
* [Mask4ToStr]
|
||||||
|
|
||||||
|
Inverse of [IPMask4ToMask].
|
||||||
|
*/
|
||||||
|
func Mask4ToIPMask(mask uint32) (ipMask net.IPMask, err error) {
|
||||||
|
|
||||||
|
var cidr uint8
|
||||||
|
|
||||||
|
if cidr, err = Mask4ToCidr(mask); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ipMask = net.CIDRMask(int(cidr), 32)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a netmask *in bitmask form*.
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Mask4ToCidr]
|
||||||
|
* [Mask4ToIPMask]
|
||||||
|
|
||||||
|
Inverse of [Mask4StrToMask].
|
||||||
|
*/
|
||||||
|
func Mask4ToStr(mask uint32) (maskStr string, err error) {
|
||||||
|
|
||||||
|
var ipMask net.IPMask
|
||||||
|
|
||||||
|
if ipMask, err = Mask4ToIPMask(mask); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if maskStr, err = IPMask4ToStr(ipMask); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mask4StrToCidr parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns am IPv4 CIDR/bit size/prefix length.
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Mask4StrToIPMask]
|
||||||
|
* [Mask4StrToMask]
|
||||||
|
|
||||||
|
Inverse of [Cidr4ToMaskStr].
|
||||||
|
*/
|
||||||
|
func Mask4StrToCidr(maskStr string) (cidr uint8, err error) {
|
||||||
|
|
||||||
|
var ipMask net.IPMask
|
||||||
|
|
||||||
|
if ipMask, err = Mask4StrToIPMask(maskStr); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cidr, err = IPMask4ToCidr(ipMask); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mask4StrToIPMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a [net.IPMask].
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Mask4StrToCidr]
|
||||||
|
* [Mask4StrToMask]
|
||||||
|
|
||||||
|
Inverse of [IPMask4ToStr].
|
||||||
|
*/
|
||||||
|
func Mask4StrToIPMask(maskStr string) (mask net.IPMask, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var s string
|
||||||
|
var u64 uint64
|
||||||
|
var b []byte = make([]byte, 4)
|
||||||
|
var sl []string = strings.Split(maskStr, ".")
|
||||||
|
|
||||||
|
if len(sl) != 4 {
|
||||||
|
err = ErrBadMask4Str
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// A net.IPMask is just a []byte.
|
||||||
|
for idx = 0; idx < len(sl); idx++ {
|
||||||
|
s = sl[idx]
|
||||||
|
if u64, err = strconv.ParseUint(s, 10, 8); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b[idx] = byte(u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
mask = net.IPMask(b)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Mask4StrToMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a netmask *in bitmask form*.
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
* [Mask4StrToCidr]
|
||||||
|
* [Mask4StrToIPMask]
|
||||||
|
|
||||||
|
Inverse of [Mask4ToStr].
|
||||||
|
*/
|
||||||
|
func Mask4StrToMask(maskStr string) (mask uint32, err error) {
|
||||||
|
|
||||||
|
var ipMask net.IPMask
|
||||||
|
|
||||||
|
if ipMask, err = Mask4StrToIPMask(maskStr); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mask, err = IPMask4ToMask(ipMask); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
134
netx/funcs_test.go
Normal file
134
netx/funcs_test.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`math`
|
||||||
|
`net`
|
||||||
|
`net/netip`
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFuncsIP(t *testing.T) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var ip net.IP
|
||||||
|
var addr netip.Addr
|
||||||
|
var ipFamily uint16
|
||||||
|
var tgtFamily uint16
|
||||||
|
var addrFamily uint16
|
||||||
|
|
||||||
|
// IPv4 on even indexes, IPv6 on odd.
|
||||||
|
for idx, s := range []string{
|
||||||
|
"203.0.113.10",
|
||||||
|
"2001:db8::203:0:113:10",
|
||||||
|
} {
|
||||||
|
if ip = net.ParseIP(s); ip == nil {
|
||||||
|
t.Fatalf("ip %s not valid", s)
|
||||||
|
}
|
||||||
|
if addr, err = netip.ParseAddr(s); err != nil {
|
||||||
|
t.Fatalf("addr %s not valid", s)
|
||||||
|
}
|
||||||
|
ipFamily = GetIpFamily(ip)
|
||||||
|
addrFamily = GetAddrFamily(addr)
|
||||||
|
if ipFamily == AFUnspec {
|
||||||
|
t.Fatalf("GetIpFamily: Failed on IP %s (unspecified family)", s)
|
||||||
|
}
|
||||||
|
if addrFamily == AFUnspec {
|
||||||
|
t.Fatalf("GetAddrFamily: Failed on IP %s (unspecified family)", s)
|
||||||
|
}
|
||||||
|
switch idx%2 == 0 {
|
||||||
|
case true:
|
||||||
|
tgtFamily = AFInet
|
||||||
|
case false:
|
||||||
|
tgtFamily = AFInet6
|
||||||
|
}
|
||||||
|
if ipFamily != tgtFamily {
|
||||||
|
t.Fatalf("GetIpFamily: Failed on IP %s (expected %d, got %d)", s, AFInet, tgtFamily)
|
||||||
|
}
|
||||||
|
if addrFamily != tgtFamily {
|
||||||
|
t.Fatalf("GetAddrFamily: Failed on IP %s (expected %d, got %d)", s, AFInet, tgtFamily)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncsMask(t *testing.T) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var cidr uint8
|
||||||
|
var mask uint32
|
||||||
|
var maskStr string
|
||||||
|
var ipMask net.IPMask
|
||||||
|
|
||||||
|
var cidrTgt uint8 = 32
|
||||||
|
var maskTgt uint32 = math.MaxUint32
|
||||||
|
var maskStrTgt string = "255.255.255.255"
|
||||||
|
var ipMaskTgt net.IPMask = net.IPMask{255, 255, 255, 255}
|
||||||
|
|
||||||
|
// To CIDR
|
||||||
|
if cidr, err = Mask4ToCidr(maskTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if cidr != cidrTgt {
|
||||||
|
t.Fatalf("Mask4ToCidr: cidr %d != cidrTgt %d", cidr, cidrTgt)
|
||||||
|
}
|
||||||
|
if cidr, err = IPMask4ToCidr(ipMaskTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if cidr != cidrTgt {
|
||||||
|
t.Fatalf("IPMask4ToCidr: cidr %d != cidrTgt %d", cidr, cidrTgt)
|
||||||
|
}
|
||||||
|
if cidr, err = Mask4StrToCidr(maskStrTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if cidr != cidrTgt {
|
||||||
|
t.Fatalf("Mask4StrToCidr cidr %d != cidrTgt %d", cidr, cidrTgt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To net.IPMask
|
||||||
|
if ipMask, err = Cidr4ToIPMask(cidrTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if ipMaskTgt.String() != ipMask.String() {
|
||||||
|
t.Fatalf("Cidr4ToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String())
|
||||||
|
}
|
||||||
|
if ipMask, err = Mask4ToIPMask(maskTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if ipMaskTgt.String() != ipMask.String() {
|
||||||
|
t.Fatalf("Mask4ToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String())
|
||||||
|
}
|
||||||
|
if ipMask, err = Mask4StrToIPMask(maskStrTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if ipMaskTgt.String() != ipMask.String() {
|
||||||
|
t.Fatalf("Mask4StrToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// To bitmask
|
||||||
|
if mask, err = Cidr4ToMask(cidrTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if mask != maskTgt {
|
||||||
|
t.Fatalf("Cidr4ToMask mask %d != maskTgt %d", mask, maskTgt)
|
||||||
|
}
|
||||||
|
if mask, err = IPMask4ToMask(ipMaskTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if mask != maskTgt {
|
||||||
|
t.Fatalf("IPMask4ToMask mask %d != maskTgt %d", mask, maskTgt)
|
||||||
|
}
|
||||||
|
if mask, err = Mask4StrToMask(maskStrTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if mask != maskTgt {
|
||||||
|
t.Fatalf("Mask4StrToMask mask %d != maskTgt %d", mask, maskTgt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To string
|
||||||
|
if maskStr, err = Cidr4ToStr(cidrTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if maskStr != maskStrTgt {
|
||||||
|
t.Fatalf("Cidr4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt)
|
||||||
|
}
|
||||||
|
if maskStr, err = IPMask4ToStr(ipMaskTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if maskStr != maskStrTgt {
|
||||||
|
t.Fatalf("IPMask4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt)
|
||||||
|
}
|
||||||
|
if maskStr, err = Mask4ToStr(maskTgt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if maskStr != maskStrTgt {
|
||||||
|
t.Fatalf("Mask4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,20 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// cksumMask is AND'd with a checksum to get the "carried ones".
|
/*
|
||||||
|
cksumMask is AND'd with a checksum to get the "carried ones"
|
||||||
|
(the lower 16 bits before folding carries).
|
||||||
|
*/
|
||||||
cksumMask uint32 = 0x0000ffff
|
cksumMask uint32 = 0x0000ffff
|
||||||
// cksumShift is used in the "carried-ones folding".
|
/*
|
||||||
|
cksumShift is used in the "carried-ones folding";
|
||||||
|
it's the number of bits to right-shift the carry-over.
|
||||||
|
*/
|
||||||
cksumShift uint32 = 0x00000010
|
cksumShift uint32 = 0x00000010
|
||||||
// padShift is used to "pad out" a checksum for odd-length buffers by left-shifting.
|
/*
|
||||||
|
padShift is used to "pad out" a checksum for odd-length buffers by left-shifting.
|
||||||
|
It positions the high-byte of a 16-byte "word" (big-endian, as per ord below).
|
||||||
|
*/
|
||||||
padShift uint32 = 0x00000008
|
padShift uint32 = 0x00000008
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ safety and no data retention, which can be used as a:
|
|||||||
* [io.StringWriter]
|
* [io.StringWriter]
|
||||||
* [io.Writer]
|
* [io.Writer]
|
||||||
|
|
||||||
|
If you don't need all these interfaces, a reasonable alternative may be
|
||||||
|
to use gVisor's [gvisor.dev/gvisor/pkg/tcpip/checksum] instead.
|
||||||
|
|
||||||
[RFC 1071]: https://datatracker.ietf.org/doc/html/rfc1071
|
[RFC 1071]: https://datatracker.ietf.org/doc/html/rfc1071
|
||||||
[RFC 1141]: https://datatracker.ietf.org/doc/html/rfc1141
|
[RFC 1141]: https://datatracker.ietf.org/doc/html/rfc1141
|
||||||
[RFC 1624]: https://datatracker.ietf.org/doc/html/rfc1624
|
[RFC 1624]: https://datatracker.ietf.org/doc/html/rfc1624
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
// New returns a new initialized [InetChecksum]. It will never panic.
|
// New returns a new initialized [InetChecksum]. It will never panic.
|
||||||
func New() (i *InetChecksum) {
|
func New() (i *InetChecksum) {
|
||||||
|
|
||||||
i = &InetChecksum{}
|
i = &InetChecksum{
|
||||||
_ = i.Aligned()
|
aligned: true,
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -21,15 +22,14 @@ b may be nil or 0-length; this will not cause an error.
|
|||||||
func NewFromBytes(b []byte) (i *InetChecksum, copied int, err error) {
|
func NewFromBytes(b []byte) (i *InetChecksum, copied int, err error) {
|
||||||
|
|
||||||
var cksum InetChecksum
|
var cksum InetChecksum
|
||||||
|
var cptr *InetChecksum = &cksum
|
||||||
|
|
||||||
|
cksum.aligned = true
|
||||||
|
|
||||||
if b != nil && len(b) > 0 {
|
if b != nil && len(b) > 0 {
|
||||||
if copied, err = cksum.Write(b); err != nil {
|
if copied, err = cptr.Write(b); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = i.Aligned()
|
|
||||||
} else {
|
|
||||||
i = New()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i = &cksum
|
i = &cksum
|
||||||
@@ -48,7 +48,64 @@ func NewFromBuf(buf io.Reader) (i *InetChecksum, copied int64, err error) {
|
|||||||
|
|
||||||
var cksum InetChecksum
|
var cksum InetChecksum
|
||||||
|
|
||||||
_ = i.Aligned()
|
cksum.aligned = true
|
||||||
|
|
||||||
|
if buf != nil {
|
||||||
|
if copied, err = io.Copy(&cksum, buf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i = &cksum
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSimple returns a new initialized [InetChecksumSimple]. It will never panic.
|
||||||
|
func NewSimple() (i *InetChecksumSimple) {
|
||||||
|
|
||||||
|
i = &InetChecksumSimple{
|
||||||
|
aligned: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewSimpleFromBytes returns a new [InetChecksumSimple] initialized with explicit bytes.
|
||||||
|
|
||||||
|
b may be nil or 0-length; this will not cause an error.
|
||||||
|
*/
|
||||||
|
func NewSimpleFromBytes(b []byte) (i *InetChecksumSimple, copied int, err error) {
|
||||||
|
|
||||||
|
var cksum InetChecksumSimple
|
||||||
|
var cptr *InetChecksumSimple = &cksum
|
||||||
|
|
||||||
|
cksum.aligned = true
|
||||||
|
|
||||||
|
if b != nil && len(b) > 0 {
|
||||||
|
if copied, err = cptr.Write(b); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i = &cksum
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewSimpleFromBuf returns an [InetChecksumSimple] from a specified [io.Reader].
|
||||||
|
|
||||||
|
buf may be nil. If it isn't, NewSimpleFromBuf will call [io.Copy] on buf.
|
||||||
|
Note that this may exhaust your passed buf or advance its current seek position/offset,
|
||||||
|
depending on its type.
|
||||||
|
*/
|
||||||
|
func NewSimpleFromBuf(buf io.Reader) (i *InetChecksumSimple, copied int64, err error) {
|
||||||
|
|
||||||
|
var cksum InetChecksumSimple
|
||||||
|
|
||||||
|
cksum.aligned = true
|
||||||
|
|
||||||
if buf != nil {
|
if buf != nil {
|
||||||
if copied, err = io.Copy(&cksum, buf); err != nil {
|
if copied, err = io.Copy(&cksum, buf); err != nil {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func (i *InetChecksum) Aligned() (aligned bool) {
|
|||||||
defer i.alignLock.Unlock()
|
defer i.alignLock.Unlock()
|
||||||
|
|
||||||
i.bufLock.RLock()
|
i.bufLock.RLock()
|
||||||
aligned = i.buf.Len()&2 == 0
|
aligned = i.buf.Len()%2 == 0
|
||||||
i.bufLock.RUnlock()
|
i.bufLock.RUnlock()
|
||||||
|
|
||||||
i.aligned = aligned
|
i.aligned = aligned
|
||||||
@@ -113,7 +113,7 @@ func (i *InetChecksum) Reset() {
|
|||||||
i.sumLock.Lock()
|
i.sumLock.Lock()
|
||||||
i.lastLock.Lock()
|
i.lastLock.Lock()
|
||||||
|
|
||||||
i.aligned = false
|
i.aligned = true
|
||||||
i.alignLock.Unlock()
|
i.alignLock.Unlock()
|
||||||
|
|
||||||
i.buf.Reset()
|
i.buf.Reset()
|
||||||
@@ -308,7 +308,7 @@ func (i *InetChecksum) WriteByte(c byte) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !i.disabledBuf {
|
if !i.disabledBuf {
|
||||||
if err = i.WriteByte(c); err != nil {
|
if err = i.buf.WriteByte(c); err != nil {
|
||||||
i.sum = origSum
|
i.sum = origSum
|
||||||
i.aligned = origAligned
|
i.aligned = origAligned
|
||||||
i.last = origLast
|
i.last = origLast
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (i *InetChecksumSimple) Reset() {
|
|||||||
|
|
||||||
i.last = 0x00
|
i.last = 0x00
|
||||||
i.sum = 0
|
i.sum = 0
|
||||||
i.last = 0x00
|
i.aligned = true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ type (
|
|||||||
If [InetChecksum.Aligned] returns false, the checksum result of an
|
If [InetChecksum.Aligned] returns false, the checksum result of an
|
||||||
[InetChecksum.Sum] or [InetChecksum.Sum16] (or any other operation
|
[InetChecksum.Sum] or [InetChecksum.Sum16] (or any other operation
|
||||||
returning a sum) will INCLUDE THE PAD NULL BYTE (which is only
|
returning a sum) will INCLUDE THE PAD NULL BYTE (which is only
|
||||||
applied *at the time of the Sum/Sum32 call) and is NOT applied to
|
applied *at the time of the Sum/Sum32 call* and is NOT applied to
|
||||||
the persistent underlying storage.
|
the persistent underlying storage).
|
||||||
|
|
||||||
InetChecksum differs from [InetChecksumSimple] in that it:
|
InetChecksum differs from [InetChecksumSimple] in that it:
|
||||||
|
|
||||||
|
|||||||
5
stringsx/TODO
Normal file
5
stringsx/TODO
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
- Banner struct, with .Format(s string) method
|
||||||
|
-- draw border around multiline s
|
||||||
|
-- i have a version in python somewhere that does this, should dig that up
|
||||||
|
|
||||||
|
- create bytesx package that duplicates the functions here?
|
||||||
6
stringsx/consts.go
Normal file
6
stringsx/consts.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package stringsx
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefMaskStr is the string used as the default maskStr if left empty in [Redact].
|
||||||
|
DefMaskStr string = "***"
|
||||||
|
)
|
||||||
17
stringsx/doc.go
Normal file
17
stringsx/doc.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Package stringsx aims to extend functionality of the stdlib [strings] module.
|
||||||
|
|
||||||
|
Note that if you need a way of mimicking Bash's shell quoting rules, [desertbit/shlex] or [buildkite/shellwords]
|
||||||
|
would be better options than [google/shlex] but this package does not attempt to reproduce
|
||||||
|
any of that functionality.
|
||||||
|
|
||||||
|
For line splitting, one should use [muesli/reflow/wordwrap].
|
||||||
|
Likewise for indentation, one should use [muesli/reflow/indent].
|
||||||
|
|
||||||
|
[desertbit/shlex]: https://pkg.go.dev/github.com/desertbit/go-shlex
|
||||||
|
[buildkite/shellwords]: https://pkg.go.dev/github.com/buildkite/shellwords
|
||||||
|
[google/shlex]: https://pkg.go.dev/github.com/google/shlex
|
||||||
|
[muesli/reflow/wordwrap]: https://pkg.go.dev/github.com/muesli/reflow/wordwrap
|
||||||
|
[muesli/reflow/indent]: https://pkg.go.dev/github.com/muesli/reflow/indent
|
||||||
|
*/
|
||||||
|
package stringsx
|
||||||
326
stringsx/funcs.go
Normal file
326
stringsx/funcs.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
package stringsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`fmt`
|
||||||
|
`strings`
|
||||||
|
`unicode`
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
LenSplit formats string `s` to break at, at most, every `width` characters.
|
||||||
|
|
||||||
|
Any existing newlines (e.g. \r\n) will be removed during a string/
|
||||||
|
substring/line's length calculation. (e.g. `foobarbaz\n` and `foobarbaz\r\n` are
|
||||||
|
both considered to be lines of length 9, not 10 and 11 respectively).
|
||||||
|
|
||||||
|
This also means that any newlines (\n or \r\n) are inherently removed from
|
||||||
|
`out` (even if included in `wordWrap`; see below).
|
||||||
|
|
||||||
|
Note that if `s` is multiline (already contains newlines), they will be respected
|
||||||
|
as-is - that is, if a line ends with less than `width` chars and then has a newline,
|
||||||
|
it will be preserved as an empty element. That is to say:
|
||||||
|
|
||||||
|
"foo\nbar\n\n" → []string{"foo", "bar", ""}
|
||||||
|
"foo\n\nbar\n" → []string{"foo", "", "bar"}
|
||||||
|
|
||||||
|
This splitter is particularly simple. If you need wordwrapping, it should be done
|
||||||
|
with e.g. [github.com/muesli/reflow/wordwrap].
|
||||||
|
*/
|
||||||
|
func LenSplit(s string, width uint) (out []string) {
|
||||||
|
|
||||||
|
var end int
|
||||||
|
var line string
|
||||||
|
var lineRunes []rune
|
||||||
|
|
||||||
|
if width == 0 {
|
||||||
|
out = []string{s}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for line = range strings.Lines(s) {
|
||||||
|
line = strings.TrimRight(line, "\n")
|
||||||
|
line = strings.TrimRight(line, "\r")
|
||||||
|
|
||||||
|
lineRunes = []rune(line)
|
||||||
|
|
||||||
|
if uint(len(lineRunes)) <= width {
|
||||||
|
out = append(out, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(lineRunes); i += int(width) {
|
||||||
|
end = i + int(width)
|
||||||
|
if end > len(lineRunes) {
|
||||||
|
end = len(lineRunes)
|
||||||
|
}
|
||||||
|
out = append(out, string(lineRunes[i:end]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
LenSplitStr wraps [LenSplit] but recombines into a new string with newlines.
|
||||||
|
|
||||||
|
It's mostly just a convenience wrapper.
|
||||||
|
|
||||||
|
All arguments remain the same as in [LenSplit] with an additional one,
|
||||||
|
`winNewLine`, which if true will use \r\n as the newline instead of \n.
|
||||||
|
*/
|
||||||
|
func LenSplitStr(s string, width uint, winNewline bool) (out string) {
|
||||||
|
|
||||||
|
var outSl []string = LenSplit(s, width)
|
||||||
|
|
||||||
|
if winNewline {
|
||||||
|
out = strings.Join(outSl, "\r\n")
|
||||||
|
} else {
|
||||||
|
out = strings.Join(outSl, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Pad pads each element in `s` to length `width` using `pad`.
|
||||||
|
If `pad` is empty, a single space (0x20) will be assumed.
|
||||||
|
Note that `width` operates on rune size, not byte size.
|
||||||
|
(In ASCII, they will be the same size.)
|
||||||
|
|
||||||
|
If a line in `s` is greater than or equal to `width`,
|
||||||
|
no padding will be performed.
|
||||||
|
|
||||||
|
If `leftPad` is true, padding will be applied to the "left" (beginning")
|
||||||
|
of each element instead of the "right" ("end").
|
||||||
|
*/
|
||||||
|
func Pad(s []string, width uint, pad string, leftPad bool) (out []string) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var padIdx int
|
||||||
|
var runeIdx int
|
||||||
|
var padLen uint
|
||||||
|
var elem string
|
||||||
|
var unpadLen uint
|
||||||
|
var tmpPadLen int
|
||||||
|
var padRunes []rune
|
||||||
|
var tmpPad []rune
|
||||||
|
|
||||||
|
if width == 0 {
|
||||||
|
out = s
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out = make([]string, len(s))
|
||||||
|
|
||||||
|
// Easy; supported directly in fmt.
|
||||||
|
if pad == "" {
|
||||||
|
for idx, elem = range s {
|
||||||
|
if leftPad {
|
||||||
|
out[idx] = fmt.Sprintf("%*s", width, elem)
|
||||||
|
} else {
|
||||||
|
out[idx] = fmt.Sprintf("%-*s", width, elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This gets a little more tricky.
|
||||||
|
padRunes = []rune(pad)
|
||||||
|
padLen = uint(len(padRunes))
|
||||||
|
for idx, elem = range s {
|
||||||
|
// First we need to know the number of runes in elem.
|
||||||
|
unpadLen = uint(len([]rune(elem)))
|
||||||
|
// If it's more than/equal to width, as-is.
|
||||||
|
if unpadLen >= width {
|
||||||
|
out[idx] = elem
|
||||||
|
} else {
|
||||||
|
// Otherwise, we need to construct/calculate a pad.
|
||||||
|
if (width-unpadLen)%padLen == 0 {
|
||||||
|
// Also easy enough.
|
||||||
|
if leftPad {
|
||||||
|
out[idx] = fmt.Sprintf("%s%s", strings.Repeat(pad, int((width-unpadLen)/padLen)), elem)
|
||||||
|
} else {
|
||||||
|
out[idx] = fmt.Sprintf("%s%s", elem, strings.Repeat(pad, int((width-unpadLen)/padLen)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is where it gets a little hairy.
|
||||||
|
tmpPad = []rune{}
|
||||||
|
tmpPadLen = int(width - unpadLen)
|
||||||
|
idx = 0
|
||||||
|
padIdx = 0
|
||||||
|
for runeIdx = range tmpPadLen {
|
||||||
|
tmpPad[runeIdx] = padRunes[padIdx]
|
||||||
|
if uint(padIdx) >= padLen {
|
||||||
|
padIdx = 0
|
||||||
|
} else {
|
||||||
|
padIdx++
|
||||||
|
}
|
||||||
|
runeIdx++
|
||||||
|
}
|
||||||
|
if leftPad {
|
||||||
|
out[idx] = fmt.Sprintf("%s%s", string(tmpPad), elem)
|
||||||
|
} else {
|
||||||
|
out[idx] = fmt.Sprintf("%s%s", elem, string(tmpPad))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Redact provides a "masked" version of string s (e.g. `my_terrible_password` -> `my****************rd`).
|
||||||
|
|
||||||
|
maskStr is the character or sequence of characters
|
||||||
|
to repeat for every masked character of s.
|
||||||
|
If an empty string, the default [DefMaskStr] will be used.
|
||||||
|
(maskStr does not need to be a single character.
|
||||||
|
It is recommended to use a multi-char mask to help obfuscate a string's length.)
|
||||||
|
|
||||||
|
leading specifies the number of leading characters of s to leave *unmasked*.
|
||||||
|
If 0, no leading characters will be unmasked.
|
||||||
|
|
||||||
|
trailing specifies the number of trailing characters of s to leave *unmasked*.
|
||||||
|
if 0, no trailing characters will be unmasked.
|
||||||
|
|
||||||
|
newlines, if true, will preserve newline characters - otherwise
|
||||||
|
they will be treated as regular characters.
|
||||||
|
|
||||||
|
As a safety precaution, if:
|
||||||
|
|
||||||
|
len(s) <= (leading + trailing)
|
||||||
|
|
||||||
|
then the entire string will be *masked* and no unmasking will be performed.
|
||||||
|
|
||||||
|
Note that this DOES NOT do a string *replace*, it provides a masked version of `s` itself.
|
||||||
|
Wrap Redact with [strings.ReplaceAll] if you want to replace a certain value with a masked one.
|
||||||
|
*/
|
||||||
|
func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted string) {
|
||||||
|
|
||||||
|
var nl string
|
||||||
|
var numMasked int
|
||||||
|
var sb strings.Builder
|
||||||
|
var endIdx int = int(leading)
|
||||||
|
|
||||||
|
// This condition functionally won't do anything, so just return the input as-is.
|
||||||
|
if s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if maskStr == "" {
|
||||||
|
maskStr = DefMaskStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if newlines {
|
||||||
|
for line := range strings.Lines(s) {
|
||||||
|
nl = getNewLine(line)
|
||||||
|
sb.WriteString(
|
||||||
|
Redact(
|
||||||
|
strings.TrimSuffix(line, nl), maskStr, leading, trailing, false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
sb.WriteString(nl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(s) <= int(leading+trailing) {
|
||||||
|
redacted = strings.Repeat(maskStr, len(s))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if leading == 0 && trailing == 0 {
|
||||||
|
redacted = strings.Repeat(maskStr, len(s))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
numMasked = len(s) - int(leading+trailing)
|
||||||
|
endIdx = endIdx + numMasked
|
||||||
|
|
||||||
|
if leading > 0 {
|
||||||
|
sb.WriteString(s[:int(leading)])
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(strings.Repeat(maskStr, numMasked))
|
||||||
|
|
||||||
|
if trailing > 0 {
|
||||||
|
sb.WriteString(s[endIdx:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redacted = sb.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
|
||||||
|
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
|
||||||
|
The first encountered linebreak (`\n` vs. `\r\n`) are assumed to be
|
||||||
|
the canonical linebreak for the rest of s.
|
||||||
|
|
||||||
|
left, if true, performs a [TrimSpaceLeft] on each line (retaining the newline).
|
||||||
|
|
||||||
|
right, if true, performs a [TrimSpaceRight] on each line (retaining the newline).
|
||||||
|
*/
|
||||||
|
func TrimLines(s string, left, right bool) (trimmed string) {
|
||||||
|
|
||||||
|
var sl string
|
||||||
|
var nl string
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
// These conditions functionally won't do anything, so just return the input as-is.
|
||||||
|
if s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !left && !right {
|
||||||
|
trimmed = s
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for line := range strings.Lines(s) {
|
||||||
|
nl = getNewLine(line)
|
||||||
|
sl = strings.TrimSuffix(line, nl)
|
||||||
|
if left && right {
|
||||||
|
sl = strings.TrimSpace(sl)
|
||||||
|
} else if left {
|
||||||
|
sl = TrimSpaceLeft(sl)
|
||||||
|
} else if right {
|
||||||
|
sl = TrimSpaceRight(sl)
|
||||||
|
}
|
||||||
|
sb.WriteString(sl + nl)
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed = sb.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimSpaceLeft is like [strings.TrimSpace] but only removes leading whitespace from string `s`.
|
||||||
|
func TrimSpaceLeft(s string) (trimmed string) {
|
||||||
|
|
||||||
|
trimmed = strings.TrimLeftFunc(s, unicode.IsSpace)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TrimSpaceRight is like [strings.TrimSpace] but only removes trailing whitespace from string s.
|
||||||
|
*/
|
||||||
|
func TrimSpaceRight(s string) (trimmed string) {
|
||||||
|
|
||||||
|
trimmed = strings.TrimRightFunc(s, unicode.IsSpace)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNewLine is too unpredictable/nuanced to be used as part of a public API promise so it isn't exported.
|
||||||
|
func getNewLine(s string) (nl string) {
|
||||||
|
|
||||||
|
if strings.HasSuffix(s, "\r\n") {
|
||||||
|
nl = "\r\n"
|
||||||
|
} else if strings.HasSuffix(s, "\n") {
|
||||||
|
nl = "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
344
stringsx/funcs_test.go
Normal file
344
stringsx/funcs_test.go
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
package stringsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
testIndentSet struct {
|
||||||
|
name string
|
||||||
|
orig string
|
||||||
|
indent string
|
||||||
|
lvl uint
|
||||||
|
ws bool
|
||||||
|
empty bool
|
||||||
|
tgt string
|
||||||
|
}
|
||||||
|
testRedactSet struct {
|
||||||
|
name string
|
||||||
|
orig string
|
||||||
|
leading uint
|
||||||
|
trailing uint
|
||||||
|
tgt string
|
||||||
|
newline bool
|
||||||
|
mask string // defaults to DefMaskStr.
|
||||||
|
}
|
||||||
|
testTrimLinesSet struct {
|
||||||
|
name string
|
||||||
|
orig string
|
||||||
|
left bool
|
||||||
|
right bool
|
||||||
|
tgt string
|
||||||
|
}
|
||||||
|
testTrimSet struct {
|
||||||
|
name string
|
||||||
|
orig string
|
||||||
|
tgt string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRedact(t *testing.T) {
|
||||||
|
|
||||||
|
var out string
|
||||||
|
var tests []testRedactSet = []testRedactSet{
|
||||||
|
testRedactSet{
|
||||||
|
name: "empty in, empty out",
|
||||||
|
orig: "",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "standard",
|
||||||
|
orig: "password",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "************************",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "standard with newline",
|
||||||
|
orig: "pass\nword",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "************\n************",
|
||||||
|
newline: true,
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "standard with Windows newline",
|
||||||
|
orig: "pass\r\nword",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "************\r\n************",
|
||||||
|
newline: true,
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "standard with newline without newlines",
|
||||||
|
orig: "pass\nword",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "***************************",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "single leading",
|
||||||
|
orig: "password",
|
||||||
|
leading: 1,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "p*********************",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "single trailing",
|
||||||
|
orig: "password",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 1,
|
||||||
|
tgt: "*********************d",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "three leading",
|
||||||
|
orig: "password",
|
||||||
|
leading: 3,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "pas***************",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "three trailing",
|
||||||
|
orig: "password",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 3,
|
||||||
|
tgt: "***************ord",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "three leading and trailing",
|
||||||
|
orig: "password",
|
||||||
|
leading: 3,
|
||||||
|
trailing: 3,
|
||||||
|
tgt: "pas******ord",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "unmask overflow leading",
|
||||||
|
orig: "password",
|
||||||
|
leading: 5,
|
||||||
|
trailing: 4,
|
||||||
|
tgt: "************************",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "unmask overflow trailing",
|
||||||
|
orig: "password",
|
||||||
|
leading: 4,
|
||||||
|
trailing: 5,
|
||||||
|
tgt: "************************",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "single mask",
|
||||||
|
orig: "password",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "********",
|
||||||
|
mask: "*",
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "standard trailing newline with newlines",
|
||||||
|
orig: "password\n",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "************************\n",
|
||||||
|
newline: true,
|
||||||
|
},
|
||||||
|
testRedactSet{
|
||||||
|
name: "standard trailing newline without newlines",
|
||||||
|
orig: "password\n",
|
||||||
|
leading: 0,
|
||||||
|
trailing: 0,
|
||||||
|
tgt: "***************************",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, ts := range tests {
|
||||||
|
out = Redact(ts.orig, ts.mask, ts.leading, ts.trailing, ts.newline)
|
||||||
|
if out == ts.tgt {
|
||||||
|
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
|
||||||
|
} else {
|
||||||
|
t.Errorf(
|
||||||
|
"[%d] FAIL (%s): %#v (len %d):\n"+
|
||||||
|
"\t\t\texpected (len %d): %#v\n"+
|
||||||
|
"\t\t\tgot (len %d): %#v\n"+
|
||||||
|
"\t\t%#v",
|
||||||
|
idx, ts.name, ts.orig, len(ts.orig),
|
||||||
|
len(ts.tgt), ts.tgt,
|
||||||
|
len(out), out,
|
||||||
|
ts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimLines(t *testing.T) {
|
||||||
|
|
||||||
|
var out string
|
||||||
|
var tests []testTrimLinesSet = []testTrimLinesSet{
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "none",
|
||||||
|
orig: " foo \n bar \n baz ",
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
tgt: " foo \n bar \n baz ",
|
||||||
|
},
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "standard",
|
||||||
|
orig: " foo \n bar \n baz ",
|
||||||
|
left: true,
|
||||||
|
right: true,
|
||||||
|
tgt: "foo\nbar\nbaz",
|
||||||
|
},
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "left only",
|
||||||
|
orig: " foo \n bar \n baz ",
|
||||||
|
left: true,
|
||||||
|
right: false,
|
||||||
|
tgt: "foo \nbar \nbaz ",
|
||||||
|
},
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "right only",
|
||||||
|
orig: " foo \n bar \n baz ",
|
||||||
|
left: false,
|
||||||
|
right: true,
|
||||||
|
tgt: " foo\n bar\n baz",
|
||||||
|
},
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "standard, trailing newline",
|
||||||
|
orig: " foo \n bar \n baz \n",
|
||||||
|
left: true,
|
||||||
|
right: true,
|
||||||
|
tgt: "foo\nbar\nbaz\n",
|
||||||
|
},
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "left only, trailing newline",
|
||||||
|
orig: " foo \n bar \n baz \n",
|
||||||
|
left: true,
|
||||||
|
right: false,
|
||||||
|
tgt: "foo \nbar \nbaz \n",
|
||||||
|
},
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "right only, trailing newline",
|
||||||
|
orig: " foo \n bar \n baz \n",
|
||||||
|
left: false,
|
||||||
|
right: true,
|
||||||
|
tgt: " foo\n bar\n baz\n",
|
||||||
|
},
|
||||||
|
// Since there's no "non-space" boundary, both of these condition tests do the same thing.
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "left only, trailing newline and ws",
|
||||||
|
orig: " foo \n bar \n baz \n ",
|
||||||
|
left: true,
|
||||||
|
right: false,
|
||||||
|
tgt: "foo \nbar \nbaz \n",
|
||||||
|
},
|
||||||
|
testTrimLinesSet{
|
||||||
|
name: "right only, trailing newline and ws",
|
||||||
|
orig: " foo \n bar \n baz \n ",
|
||||||
|
left: false,
|
||||||
|
right: true,
|
||||||
|
tgt: " foo\n bar\n baz\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, ts := range tests {
|
||||||
|
out = TrimLines(ts.orig, ts.left, ts.right)
|
||||||
|
if out == ts.tgt {
|
||||||
|
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
|
||||||
|
} else {
|
||||||
|
t.Errorf(
|
||||||
|
"[%d] FAIL (%s): %#v (len %d):\n"+
|
||||||
|
"\t\t\texpected (len %d): %#v\n"+
|
||||||
|
"\t\t\tgot (len %d): %#v\n"+
|
||||||
|
"\t\t%#v",
|
||||||
|
idx, ts.name, ts.orig, len(ts.orig),
|
||||||
|
len(ts.tgt), ts.tgt,
|
||||||
|
len(out), out,
|
||||||
|
ts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimSpaceLeft(t *testing.T) {
|
||||||
|
|
||||||
|
var out string
|
||||||
|
var tests []testTrimSet = []testTrimSet{
|
||||||
|
testTrimSet{
|
||||||
|
name: "standard",
|
||||||
|
orig: " foo ",
|
||||||
|
tgt: "foo ",
|
||||||
|
},
|
||||||
|
testTrimSet{
|
||||||
|
name: "tabs",
|
||||||
|
orig: "\t\tfoo\t\t",
|
||||||
|
tgt: "foo\t\t",
|
||||||
|
},
|
||||||
|
testTrimSet{
|
||||||
|
name: "newlines",
|
||||||
|
orig: "\n\nfoo\n\n",
|
||||||
|
tgt: "foo\n\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, ts := range tests {
|
||||||
|
out = TrimSpaceLeft(ts.orig)
|
||||||
|
if out == ts.tgt {
|
||||||
|
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
|
||||||
|
} else {
|
||||||
|
t.Errorf(
|
||||||
|
"[%d] FAIL (%s): %#v (len %d):\n"+
|
||||||
|
"\t\t\texpected (len %d): %#v\n"+
|
||||||
|
"\t\t\tgot (len %d): %#v\n"+
|
||||||
|
"\t\t%#v",
|
||||||
|
idx, ts.name, ts.orig, len(ts.orig),
|
||||||
|
len(ts.tgt), ts.tgt,
|
||||||
|
len(out), out,
|
||||||
|
ts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimSpaceRight(t *testing.T) {
|
||||||
|
|
||||||
|
var out string
|
||||||
|
var tests []testTrimSet = []testTrimSet{
|
||||||
|
testTrimSet{
|
||||||
|
name: "standard",
|
||||||
|
orig: " foo ",
|
||||||
|
tgt: " foo",
|
||||||
|
},
|
||||||
|
testTrimSet{
|
||||||
|
name: "tabs",
|
||||||
|
orig: "\t\tfoo\t\t",
|
||||||
|
tgt: "\t\tfoo",
|
||||||
|
},
|
||||||
|
testTrimSet{
|
||||||
|
name: "newlines",
|
||||||
|
orig: "\n\nfoo\n\n",
|
||||||
|
tgt: "\n\nfoo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, ts := range tests {
|
||||||
|
out = TrimSpaceRight(ts.orig)
|
||||||
|
if out == ts.tgt {
|
||||||
|
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
|
||||||
|
} else {
|
||||||
|
t.Errorf(
|
||||||
|
"[%d] FAIL (%s): %#v (len %d):\n"+
|
||||||
|
"\t\t\texpected (len %d): %#v\n"+
|
||||||
|
"\t\t\tgot (len %d): %#v\n"+
|
||||||
|
"\t\t%#v",
|
||||||
|
idx, ts.name, ts.orig, len(ts.orig),
|
||||||
|
len(ts.tgt), ts.tgt,
|
||||||
|
len(out), out,
|
||||||
|
ts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user