Compare commits

..

No commits in common. "master" and "v1.5.0" have entirely different histories.

52 changed files with 154 additions and 2409 deletions

View File

@ -1,19 +0,0 @@
/*
Package bit aims to provide feature parity with stdlib's [encoding/hex].
It's a ludicrous tragedy that hex/base16, base32, base64 all have libraries for converting
to/from string representations... but there's nothing for binary ('01010001' etc.) whatsoever.
This package also provides some extra convenience functions and types in an attempt to provide
an abstracted bit-level fidelity in Go. A [Bit] is a bool type, in which that underlying bool
being false represents a 0 and that underlying bool being true represents a 1.
Note that a [Bit] or arbitrary-length or non-octal-aligned [][Bit] may take up more bytes in memory
than expected; a [Bit] will actually always occupy a single byte -- thus representing
`00000000 00000000` as a [][Bit] or [16][Bit] will actually occupy *sixteen bytes* in memory,
NOT 2 bytes (nor, obviously, [2][Byte])!
It is recommended instead to use a [Bits] instead of a [Bit] slice or array, as it will try to properly align to the
smallest memory allocation possible (at the cost of a few extra CPU cycles on adding/removing one or more [Bit]).
It will properly retain any appended, prepended, leading, or trailing bits that do not currently align to a byte.
*/
package bit

View File

@ -1,14 +0,0 @@
package bit
// TODO: Provide analogues of encoding/hex, encoding/base64, etc. functions etc.
/*
TODO: Also provide interfaces for the following:
* https://pkg.go.dev/encoding#BinaryAppender
* https://pkg.go.dev/encoding#BinaryMarshaler
* https://pkg.go.dev/encoding#BinaryUnmarshaler
* https://pkg.go.dev/encoding#TextAppender
* https://pkg.go.dev/encoding#TextMarshaler
* https://pkg.go.dev/encoding#TextUnmarshaler
*/

View File

@ -1,34 +0,0 @@
package bit
type (
// Bit aims to provide a native-like type for a single bit (Golang operates on the smallest fidelity level of *byte*/uint8).
Bit bool
// Bits is an arbitrary length of bits.
Bits struct {
/*
leading is a series of Bit that do not cleanly align to the beginning of Bits.b.
They will always be the bits at the *beginning* of the sequence.
len(Bits.leading) will *never* be more than 7;
it's converted into a byte, prepended to Bits.b, and cleared if it reaches that point.
*/
leading []Bit
// b is the condensed/memory-aligned alternative to an [][8]Bit (or []Bit, or [][]Bit, etc.).
b []byte
/*
remaining is a series of Bit that do not cleanly align to the end of Bits.b.
They will always be the bits at the *end* of the sequence.
len(Bits.remaining) will *never* be more than 7;
it's converted into a byte, appended to Bits.b, and cleared if it reaches that point.
*/
remaining []Bit
// fixedLen, if 0, represents a "slice". If >= 1, it represents an "array".
fixedLen uint
}
// Byte is this package's representation of a byte. It's primarily for convenience.
Byte byte
// Bytes is defined as a type for convenience single-call functions.
Bytes []Byte
)

View File

@ -2,8 +2,8 @@ package bitmask
import (
"bytes"
"encoding/binary"
"errors"
"encoding/binary"
"math/bits"
)
@ -34,56 +34,12 @@ func NewMaskBitExplicit(value uint) (m *MaskBit) {
return
}
/*
HasFlag is true if m has MaskBit flag set/enabled.
THIS WILL RETURN FALSE FOR OR'd FLAGS.
For example:
flagA MaskBit = 0x01
flagB MaskBit = 0x02
flagComposite = flagA | flagB
m *MaskBit = NewMaskBitExplicit(uint(flagA))
m.HasFlag(flagComposite) will return false even though flagComposite is an OR
that contains flagA.
Use [MaskBit.IsOneOf] instead if you do not desire this behavior,
and instead want to test composite flag *membership*.
(MaskBit.IsOneOf will also return true for non-composite equality.)
To be more clear, if MaskBit flag is a composite MaskBit (e.g. flagComposite above),
HasFlag will only return true of ALL bits in flag are also set in MaskBit m.
*/
// HasFlag is true if m has MaskBit flag set/enabled.
func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {
var b MaskBit = *m
if b&flag == flag {
r = true
}
return
}
/*
IsOneOf is like a "looser" form of [MaskBit.HasFlag]
in that it allows for testing composite membership.
See [MaskBit.HasFlag] for more information.
If composite is *not* an OR'd MaskBit (i.e.
it falls directly on a boundary -- 0, 1, 2, 4, 8, 16, etc.),
then IsOneOf will behave exactly like HasFlag.
If m is a composite MaskBit (it usually is) and composite is ALSO a composite MaskBit,
IsOneOf will return true if ANY of the flags set in m is set in composite.
*/
func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) {
var b MaskBit = *m
if b&composite != 0 {
if b&flag != 0 {
r = true
}
return
@ -100,7 +56,7 @@ func (m *MaskBit) AddFlag(flag MaskBit) {
// ClearFlag removes MaskBit flag from m.
func (m *MaskBit) ClearFlag(flag MaskBit) {
*m &^= flag
*m &= flag
return
}
@ -153,15 +109,6 @@ func (m *MaskBit) Bytes(trim bool) (b []byte) {
return
}
// Copy returns a pointer to a (new) copy of a MaskBit.
func (m *MaskBit) Copy() (newM *MaskBit) {
newM = new(MaskBit)
*newM = *m
return
}
// Value returns the current raw uint value of a MaskBit.
func (m *MaskBit) Value() (v uint) {

View File

@ -1,35 +1,9 @@
/*
Package bitmask handles a flag-like opt/bitmask system.
See https://yourbasic.org/golang/bitmask-flag-set-clear/ for basic information on what bitmasks are and why they're useful.
See https://yourbasic.org/golang/bitmask-flag-set-clear/ for more information.
Specifically, in the case of Go, they allow you to essentially manage many, many, many "booleans" as part of a single value.
A single bool value in Go takes up 8 bits/1 byte, unavoidably.
However, a [bitmask.MaskBit] is backed by a uint which (depending on your platform) is either 32 bits/4 bytes or 64 bits/8 bytes.
"But wait, that takes up more memory though!"
Yep, but bitmasking lets you store a "boolean" AT EACH BIT - it operates on
whether a bit in a byte/set of bytes at a given position is 0 or 1.
Which means on 32-bit platforms, a [MaskBit] can have up to 4294967295 "booleans" in a single value (0 to (2^32)-1).
On 64-bit platforms, a [MaskBit] can have up to 18446744073709551615 "booleans" in a single value (0 to (2^64)-1).
If you tried to do that with Go bool values, that'd take up 4294967295 bytes (4 GiB)
or 18446744073709551615 bytes (16 EiB - yes, that's [exbibytes]) of RAM for 32-bit/64-bit platforms respectively.
"But that has to be so slow to unpack that!"
Nope. It's not using compression or anything, the CPU is just comparing bit "A" vs. bit "B" 32/64 times. That's super easy work for a CPU.
There's a reason Doom used bitmasking for the "dmflags" value in its server configs.
# Usage
To use this library, set constants like thus:
To use this, set constants like thus:
package main
@ -68,95 +42,12 @@ But this would return false:
MyMask.HasFlag(OPT2)
# Technical Caveats
TARGETING
When implementing, you should always set MyMask (from Usage section above) as the actual value.
For example, if you are checking a permissions set for a user that has the value, say, 6
var userPerms uint = 6 // 0x0000000000000006
and your library has the following permission bits defined:
const PermsNone bitmask.MaskBit = 0
const (
PermsList bitmask.MaskBit = 1 << iota // 1
PermsRead // 2
PermsWrite // 4
PermsExec // 8
PermsAdmin // 16
)
And you want to see if the user has the PermsRead flag set, you would do:
userPermMask = bitmask.NewMaskBitExplicit(userPerms)
if userPermMask.HasFlag(PermsRead) {
// ...
}
NOT:
userPermMask = bitmask.NewMaskBitExplicit(PermsRead)
// Nor:
// userPermMask = PermsRead
if userPermMask.HasFlag(userPerms) {
// ...
}
This will be terribly, horribly wrong, cause incredibly unexpected results,
and quite possibly cause massive security issues. Don't do it.
COMPOSITES
If you want to define a set of flags that are a combination of other flags,
your inclination would be to bitwise-OR them together:
const (
flagA bitmask.MaskBit = 1 << iota // 1
flagB // 2
)
const (
flagAB bitmask.MaskBit = flagA | flagB // 3
)
Which is fine and dandy. But if you then have:
var myMask *bitmask.MaskBit = bitmask.NewMaskBit()
myMask.AddFlag(flagA)
You may expect this call to [MaskBit.HasFlag]:
myMask.HasFlag(flagAB)
to be true, since flagA is "in" flagAB.
It will return false - HasFlag does strict comparisons.
It will only return true if you then ALSO do:
// This would require setting flagA first.
// The order of setting flagA/flagB doesn't matter,
// but you must have both set for HasFlag(flagAB) to return true.
myMask.AddFlag(flagB)
or if you do:
// This can be done with or without additionally setting flagA.
myMask.AddFlag(flagAB)
Instead, if you want to see if a mask has membership within a composite flag,
you can use [MaskBit.IsOneOf].
# Other Options
If you need something with more flexibility (as always, at the cost of complexity),
you may be interested in one of the following libraries:
* [github.com/alvaroloes/enumer]
* [github.com/abice/go-enum]
* [github.com/jeffreyrichter/enum/enum]
. github.com/alvaroloes/enumer
. github.com/abice/go-enum
. github.com/jeffreyrichter/enum/enum
[exbibytes]: https://simple.wikipedia.org/wiki/Exbibyte
*/
package bitmask

15
go.mod
View File

@ -1,15 +1,10 @@
module r00t2.io/goutils
go 1.24.5
go 1.16
require (
github.com/coreos/go-systemd/v22 v22.5.0
github.com/google/uuid v1.6.0
golang.org/x/sys v0.34.0
r00t2.io/sysutils v1.14.0
)
require (
github.com/djherbis/times v1.6.0 // indirect
golang.org/x/sync v0.16.0 // indirect
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/google/uuid v1.3.0
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
r00t2.io/sysutils v1.1.1
)

21
go.sum
View File

@ -1,13 +1,8 @@
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
r00t2.io/sysutils v1.1.1 h1:q2P5u50HIIRk6muCPo1Gpapy6sNT4oaB1l2O/C/mi3A=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=

View File

@ -1,4 +0,0 @@
/*
Package iox includes extensions to the stdlib `io` module.
*/
package iox

View File

@ -1,9 +0,0 @@
package iox
import (
`errors`
)
var (
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
)

View File

@ -1,41 +0,0 @@
package iox
import (
`io`
)
/*
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.
Instead, it always reads 32 KiB from src, and writes n bytes to dst.
There are, of course, cases where this is deadfully undesired.
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.
*/
func CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
var b []byte
if n <= 0 {
err = ErrBufTooSmall
return
}
b = make([]byte, n)
written, err = io.CopyBuffer(dst, src, b)
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) {
written, err = io.CopyBuffer(dst, src, bufFunc())
return
}

View File

@ -1,8 +0,0 @@
package iox
type (
// RuneWriter matches the behavior of *(bytes.Buffer).WriteRune and *(bufio.Writer).WriteRune
RuneWriter interface {
WriteRune(r rune) (n int, err error)
}
)

View File

@ -1,25 +1,16 @@
- 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/generating-log-messages-from-your-code
-- no native Go support (yet)?
--- https://developer.apple.com/forums/thread/773369
- The log destinations for e.g. consts_nix.go et. al. probably should be unexported types.
- add a `log/slog` logging.Logger?
- Implement code line/func/etc. (only for debug?):
https://stackoverflow.com/a/24809646
https://golang.org/pkg/runtime/#Caller
-- log.LlongFile and log.Lshortfile flags don't currently work properly for StdLogger/FileLogger; they refer to the file in logging package rather than the caller.
-- ZeroLog seems to be able to do it, take a peek there.
- StdLogger2; where stdout and stderr are both logged to depending on severity level.
- make configurable via OR bitmask
- Suport remote loggers? (eventlog, syslog, journald)
- Suport remote loggers? (eventlog, syslog, systemd)
- JSON logger? YAML logger? XML logger?
- DOCS.
-- Done, but flesh out.
- Implement io.Writer interfaces

View File

@ -12,16 +12,3 @@ const (
// appendFlags are the flags used for testing the file (and opening/writing).
appendFlags int = os.O_APPEND | os.O_CREATE | os.O_WRONLY
)
const PriorityNone logPrio = 0
const (
PriorityEmergency logPrio = 1 << iota
PriorityAlert
PriorityCritical
PriorityError
PriorityWarning
PriorityNotice
PriorityInformational
PriorityDebug
)
const PriorityAll logPrio = PriorityEmergency | PriorityAlert | PriorityCritical | PriorityError | PriorityWarning | PriorityNotice | PriorityInformational | PriorityDebug

View File

@ -1,9 +0,0 @@
package logging
var (
// defLogPaths indicates default log paths.
defLogPaths = []string{
"/var/log/golang/program.log",
"~/Library/Logs/Golang/program.log",
}
)

View File

@ -1,5 +1,32 @@
package logging
import (
`log/syslog`
`r00t2.io/goutils/bitmask`
)
const (
// devlog is the path to the syslog char device.
devlog string = "/dev/log"
// syslogFacility is the facility to use; it's a little like a context or scope if you think of it in those terms.
syslogFacility syslog.Priority = syslog.LOG_USER
)
// Flags for logger configuration. These are used internally.
const (
// LogUndefined indicates an undefined Logger type.
LogUndefined bitmask.MaskBit = 1 << iota
// LogJournald flags a SystemDLogger Logger type.
LogJournald
// LogSyslog flags a SyslogLogger Logger type.
LogSyslog
// LogFile flags a FileLogger Logger type.
LogFile
// LogStdout flags a StdLogger Logger type.
LogStdout
)
var (
// defLogPaths indicates default log paths.
defLogPaths = []string{

View File

@ -1,34 +0,0 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
// I mean maybe it works for plan9 and ios, I don't know.
package logging
import (
"log/syslog"
"r00t2.io/goutils/bitmask"
)
const (
// devlog is the path to the syslog char device.
devlog string = "/dev/log"
// syslogFacility is the facility to use; it's a little like a context or scope if you think of it in those terms.
syslogFacility syslog.Priority = syslog.LOG_USER
)
// Flags for logger configuration. These are used internally.
// LogUndefined indicates an undefined Logger type.
const LogUndefined bitmask.MaskBit = iota
const (
// LogJournald flags a SystemDLogger Logger type. This will, for hopefully obvious reasons, only work on Linux systemd systems.
LogJournald bitmask.MaskBit = 1 << iota
// LogSyslog flags a SyslogLogger Logger type.
LogSyslog
// LogFile flags a FileLogger Logger type.
LogFile
// LogStdout flags a StdLogger Logger type.
LogStdout
)

View File

@ -8,11 +8,11 @@ import (
)
// Flags for logger configuration. These are used internally.
// LogUndefined indicates an undefined Logger type.
const LogUndefined bitmask.MaskBit = 0
const (
// LogUndefined indicates an undefined Logger type.
LogUndefined bitmask.MaskBit = 1 << iota
// LogWinLogger indicates a WinLogger Logger type (Event Log).
LogWinLogger bitmask.MaskBit = 1 << iota
LogWinLogger
// LogFile flags a FileLogger Logger type.
LogFile
// LogStdout flags a StdLogger Logger type.

View File

@ -7,16 +7,13 @@ These particular loggers (logging.Logger) available are:
StdLogger
FileLogger
SystemDLogger (Linux only)
SyslogLogger (Linux/macOS/other *NIX-like only)
SyslogLogger (Linux only)
WinLogger (Windows only)
There is a seventh type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call.
(This is similar to stdlib's io.MultiWriter()'s return value, but with priority awareness and fmt string support).
As you may have guessed, NullLogger doesn't actually log anything but is fully "functional" as a logging.Logger.
As you may have guessed, NullLogger doesn't actually log anything but is fully "functional" as a logging.Logger (similar to io.discard/io.Discard()'s return).
Note that for some Loggers, the prefix may be modified after the Logger has already initialized.
"Literal" loggers (StdLogger and FileLogger) will append a space to the end of the prefix by default.
Note that for some Loggers, the prefix may be modified - "literal" loggers (StdLogger and FileLogger) will append a space to the end of the prefix.
If this is undesired (unlikely), you will need to modify (Logger).Prefix and run (Logger).Logger.SetPrefix(yourPrefixHere) for the respective logger.
Every logging.Logger type has the following methods that correspond to certain "levels".
@ -48,17 +45,5 @@ logging.Logger types also have the following methods:
In some cases, Logger.Setup and Logger.Shutdown are no-ops. In other cases, they perform necessary initialization/cleanup and closing of the logger.
It is recommended to *always* run Setup and Shutdown before and after using, respectively, regardless of the actual logging.Logger type.
Lastly, all logging.Loggers have a ToLogger() method. This returns a *log.Logger (from stdlib log), which also conforms to io.Writer inherently.
In addition. all have a ToRaw() method, which extends a Logger even further and returns an unexported type (*logging.logWriter) compatible with:
- io.ByteWriter
- io.Writer
- io.WriteCloser (Shutdown() on the Logger backend is called during Close(), rendering the underlying Logger unsafe to use afterwards)
- io.StringWriter
and, if stdlib io ever defines an e.g. RuneWriter (WriteRune(r rune) (n int, err error)), it will conform to that too (see (r00t2.io/goutils/iox).RuneWriter).
Obviously this and io.ByteWriter are fairly silly, as they're intended to be high-speed throughput-optimized methods, but if you wanted to e.g.
log every single byte on a wire as a separate log message, go ahead; I'm not your dad.
*/
package logging

View File

@ -1,7 +1,7 @@
package logging
import (
"errors"
`errors`
)
var (
@ -12,8 +12,6 @@ var (
exists with too restrictive perms to write/append to, and/or could not be created.
*/
ErrInvalidFile error = errors.New("a FileLogger was requested but the file does not exist and cannot be created")
// ErrInvalidRune is returned if a rune was expected but it is not a valid UTF-8 codepoint.
ErrInvalidRune error = errors.New("specified rune is not valid UTF-8 codepoint")
// ErrNoEntry indicates that the user attempted to MultiLogger.RemoveLogger a Logger but one by that identifier does not exist.
ErrNoEntry error = errors.New("the Logger specified to be removed does not exist")
)

View File

@ -1,10 +1,18 @@
package logging
import (
"errors"
`errors`
`fmt`
)
var (
// ErrNoSysD indicates that the user attempted to add a SystemDLogger to a MultiLogger but systemd is unavailable.
ErrNoSysD error = errors.New("a systemd (journald) Logger was requested but systemd is unavailable on this system")
// ErrNoSyslog indicates that the user attempted to add a SyslogLogger to a MultiLogger but syslog's logger device is unavailable.
ErrNoSyslog error = errors.New("a Syslog Logger was requested but Syslog is unavailable on this system")
/*
ErrInvalidDevLog indicates that the user attempted to add a SyslogLogger to a MultiLogger but
the Syslog char device file is... not actually a char device file.
*/
ErrInvalidDevLog error = errors.New(fmt.Sprintf("a Syslog Logger was requested but %v is not a valid logger handle", devlog))
)

View File

@ -1,19 +0,0 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"errors"
"fmt"
)
var (
// ErrNoSyslog indicates that the user attempted to add a SyslogLogger to a MultiLogger but syslog's logger device is unavailable.
ErrNoSyslog error = errors.New("a Syslog Logger was requested but Syslog is unavailable on this system")
/*
ErrInvalidDevLog indicates that the user attempted to add a SyslogLogger to a MultiLogger but
the Syslog char device file is... not actually a char device file.
*/
ErrInvalidDevLog error = errors.New(fmt.Sprintf("a Syslog Logger was requested but %v is not a valid logger handle", devlog))
)

View File

@ -1,33 +1,9 @@
package logging
import (
"log"
"os"
)
/*
ToLog returns a stdlib *log.Logger from a logging.Logger. It simply wraps the (logging.Logger).ToLogger() methods.
prio is an OR'd logPrio of the Priority* constants.
*/
func ToLog(l Logger, prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = l.ToLogger(prio)
return
}
// ToRaw returns a *logWriter from a logging.Logger. It is an alternative to the (logging.Logger).ToRaw() methods.
func ToRaw(l Logger, prio logPrio) (raw *logWriter) {
raw = &logWriter{
backend: l,
prio: prio,
}
return
}
// testOpen attempts to open a file for writing to test for suitability as a LogFile path.
func testOpen(path string) (success bool, err error) {

View File

@ -220,22 +220,6 @@ func (l *FileLogger) Warning(s string, v ...interface{}) (err error) {
return
}
// ToLogger returns a stdlib log.Logger.
func (l *FileLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *FileLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}
// renderWrite prepares/formats a log message to be written to this FileLogger.
func (l *FileLogger) renderWrite(msg, prio string) {

View File

@ -5,7 +5,7 @@ import (
`os`
`path`
sysd `github.com/coreos/go-systemd/v22/journal`
sysd `github.com/coreos/go-systemd/journal`
`r00t2.io/goutils/bitmask`
`r00t2.io/sysutils/paths`
)

View File

@ -1,25 +0,0 @@
package logging
import (
`r00t2.io/goutils/bitmask`
)
// HasFlag provides a wrapper for functionality to the underlying bitmask.MaskBit.
func (l *logPrio) HasFlag(prio logPrio) (hasFlag bool) {
var m *bitmask.MaskBit
var p *bitmask.MaskBit
if l == nil {
return
}
m = bitmask.NewMaskBitExplicit(uint(*l))
p = bitmask.NewMaskBitExplicit(uint(prio))
// Use IsOneOf instead in case PriorityAll is passed for prio.
// hasFlag = m.HasFlag(*p)
hasFlag = m.IsOneOf(*p)
return
}

View File

@ -1,211 +0,0 @@
package logging
import (
"unicode/utf8"
"r00t2.io/goutils/multierr"
)
/*
Close calls Logger.Shutdown() on the underlying Logger.
The Logger *must not be used* after this; it will need to be re-initialized with Logger.Setup()
or a new Logger (and thuse new logWriter) must be created to replace it.
It (along with logWriter.Write()) conforms to WriteCloser().
*/
func (l *logWriter) Close() (err error) {
if err = l.backend.Shutdown(); err != nil {
return
}
return
}
/*
Write writes bytes b to the underlying Logger's priority level if the logWriter's priority level(s) match.
It conforms to io.Writer. n will *always* == len(b) on success, because otherwise n would technically be >= len(b)
(if multiple priorities are enabled), which is undefined behavior per io.Writer.
b is converted to a string to normalize to the underlying Logger.
*/
func (l *logWriter) Write(b []byte) (n int, err error) {
var s string
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
if b == nil {
return
}
s = string(b)
// Since this explicitly checks each priority level, there's no need for IsOneOf in case of PriorityAll.
if l.prio.HasFlag(PriorityEmergency) {
if err = l.backend.Emerg(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityAlert) {
if err = l.backend.Alert(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityCritical) {
if err = l.backend.Crit(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityError) {
if err = l.backend.Err(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityWarning) {
if err = l.backend.Warning(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityNotice) {
if err = l.backend.Notice(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityInformational) {
if err = l.backend.Info(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityDebug) {
if err = l.backend.Debug(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
n = len(b)
return
}
/*
WriteByte conforms a logWriter to an io.ByteWriter. (It just wraps logWriter.Write().)
You should probably never use this; the logging overhead/prefix is going to be more data than the single byte itself.
c is converted to a string to normalize to the underlying Logger.
*/
func (l *logWriter) WriteByte(c byte) (err error) {
if _, err = l.Write([]byte{c}); err != nil {
return
}
return
}
/*
WriteRune follows the same signature of (bytes.Buffer).WriteRune() and (bufio.Writer).WriteRune(); thus if `io` ever defines an io.RuneWriter interface, here ya go.
n will *always* be equal to (unicode/utf8).RuneLen(r), unless r is an "invalid rune" -- in which case n will be 0 and err will be ErrInvalidRune..
*/
func (l *logWriter) WriteRune(r rune) (n int, err error) {
var b []byte
n = utf8.RuneLen(r)
if n < 0 {
err = ErrInvalidRune
n = 0
return
}
b = make([]byte, n)
utf8.EncodeRune(b, r)
if n, err = l.Write(b); err != nil {
return
}
return
}
/*
WriteString writes string s to the underlying Logger's priority level if the logWriter's priority level(s) match.
It conforms to io.StringWriter. n will *always* == len(s) on success, because otherwise n would technically be >= len(s)
(if multiple priorities are enabled), which is undefined behavior per io.StringWriter.
*/
func (l *logWriter) WriteString(s string) (n int, err error) {
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
if l.prio.HasFlag(PriorityEmergency) {
if err = l.backend.Emerg(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityAlert) {
if err = l.backend.Alert(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityCritical) {
if err = l.backend.Crit(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityError) {
if err = l.backend.Err(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityWarning) {
if err = l.backend.Warning(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityNotice) {
if err = l.backend.Notice(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityInformational) {
if err = l.backend.Info(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if l.prio.HasFlag(PriorityDebug) {
if err = l.backend.Debug(s); err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
n = len(s)
return
}

View File

@ -3,7 +3,6 @@ package logging
import (
"errors"
"fmt"
"log"
"sync"
"r00t2.io/goutils/multierr"
@ -371,19 +370,3 @@ func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) {
return
}
// ToLogger returns a stdlib log.Logger.
func (m *MultiLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(m.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (m *MultiLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: m, prio: prio}
return
}

View File

@ -1,8 +1,11 @@
package logging
import (
sysd "github.com/coreos/go-systemd/v22/journal"
"github.com/google/uuid"
`os`
sysd `github.com/coreos/go-systemd/journal`
`github.com/google/uuid`
`r00t2.io/sysutils/paths`
)
/*
@ -77,3 +80,55 @@ func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
return
}
/*
AddSyslogLogger adds a SyslogLogger to a MultiLogger.
identifier is a string to use to identify the added SyslogLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.
*/
func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) {
var exists bool
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog
var prefix string
if identifier == "" {
identifier = uuid.New().String()
}
if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
} else if !hasSyslog {
err = ErrNoSyslog
return
}
if stat.Mode().IsRegular() {
err = ErrInvalidDevLog
return
}
m.Loggers[identifier] = &SyslogLogger{
EnableDebug: m.EnableDebug,
Prefix: m.Prefix,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}
if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
return
}
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
return
}

View File

@ -1,63 +0,0 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"os"
"github.com/google/uuid"
"r00t2.io/sysutils/paths"
)
/*
AddSyslogLogger adds a SyslogLogger to a MultiLogger.
identifier is a string to use to identify the added SyslogLogger in MultiLogger.Loggers.
If empty, one will be automatically generated.
*/
func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) {
var exists bool
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog
var prefix string
if identifier == "" {
identifier = uuid.New().String()
}
if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
} else if !hasSyslog {
err = ErrNoSyslog
return
}
if stat.Mode().IsRegular() {
err = ErrInvalidDevLog
return
}
m.Loggers[identifier] = &SyslogLogger{
EnableDebug: m.EnableDebug,
Prefix: m.Prefix,
}
if err = m.Loggers[identifier].Setup(); err != nil {
return
}
if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
return
}
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
return
}

View File

@ -1,41 +0,0 @@
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
// Linux is excluded because it has its own.
package logging
import (
"github.com/google/uuid"
)
/*
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.
identifier is a string to use to identify the added Logger in MultiLogger.Loggers.
If empty, one will be automatically generated.
See the documentation for GetLogger for details on other arguments.
*/
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
var l Logger
var exists bool
if identifier == "" {
identifier = uuid.New().String()
}
if _, exists = m.Loggers[identifier]; exists {
err = ErrExistingLogger
return
}
if l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...); err != nil {
return
}
m.Loggers[identifier] = l
return
}

View File

@ -1,9 +1,5 @@
package logging
import (
"log"
)
// Setup does nothing at all; it's here for interface compat. 🙃
func (l *NullLogger) Setup() (err error) {
return
@ -76,19 +72,3 @@ func (l *NullLogger) Notice(s string, v ...interface{}) (err error) {
func (l *NullLogger) Warning(s string, v ...interface{}) (err error) {
return
}
// ToLogger returns a stdlib log.Logger (that doesn't actually write to anything).
func (l *NullLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(&nullWriter{}, "", 0)
return
}
// ToRaw returns a *logWriter. (This is a little less efficient than using ToLogger's log.Logger as an io.Writer if that's all you need.)
func (l *NullLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@ -1,58 +0,0 @@
package logging
import (
"unicode/utf8"
)
// Close conforms a nullWriter to an io.WriteCloser. It obviously does nothing, and will always return with err == nil.
func (nw *nullWriter) Close() (err error) {
// NO-OP
return
}
// Write conforms a nullWriter to an io.Writer, but it writes... nothing. To avoid errors, however, in downstream code it pretends it does (n will *always* == len(b)).
func (nw *nullWriter) Write(b []byte) (n int, err error) {
if b == nil {
return
}
n = len(b)
return
}
// WriteByte conforms to an io.ByteWriter but again... nothing is actually written anywhere.
func (nw *nullWriter) WriteByte(c byte) (err error) {
// NO-OP
_ = c
return
}
/*
WriteRune conforms to the other Loggers. It WILL return the proper value for n (matching (bytes.Buffer).WriteRune() and (bufio.Writer).WriteRune() signatures,
and it WILL return an ErrInvalidRune if r is not a valid rune, but otherwise it will no-op.
*/
func (nw *nullWriter) WriteRune(r rune) (n int, err error) {
n = utf8.RuneLen(r)
if n < 0 {
err = ErrInvalidRune
n = 0
return
}
return
}
// WriteString conforms to an io.StringWriter but nothing is actually written. (n will *always* == len(s))
func (nw *nullWriter) WriteString(s string) (n int, err error) {
n = len(s)
return
}

View File

@ -1,138 +0,0 @@
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
// Linux is excluded because it has its own.
package logging
import (
native "log"
"os"
"path"
"r00t2.io/goutils/bitmask"
"r00t2.io/sysutils/paths"
)
var (
_ = native.Logger{}
_ = os.Interrupt
)
/*
GetLogger returns an instance of Logger that best suits your system's capabilities.
If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written.
If prefix is "\x00" (a null byte), then the default logging prefix will be used. If anything else, even an empty string,
is specified then that will be used instead for the prefix.
logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and
https://pkg.go.dev/log#pkg-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,
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.
If you want to log to multiple files simultaneously, use a MultiLogger instead.
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) {
var logPath string
var logFlags bitmask.MaskBit
var currentPrefix string
// Configure system-supported logger(s).
// If we can detect syslog, use that. If not, try to use a file logger (+ stdout).
// Last ditch, stdout.
var hasSyslog bool
var stat os.FileInfo
var devlogPath string = devlog
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
return
}
if hasSyslog && !stat.Mode().IsRegular() {
logFlags.AddFlag(LogSyslog)
} else {
var exists bool
var success bool
var ckLogPaths []string
logFlags.AddFlag(LogStdout)
ckLogPaths = defLogPaths
if logPaths != nil {
ckLogPaths = logPaths
}
for _, p := range ckLogPaths {
if exists, _ = paths.RealPathExists(&p); exists {
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logFlags.AddFlag(LogFile)
logPath = p
break
} else {
dirPath := path.Dir(p)
if err = paths.MakeDirIfNotExist(dirPath); err != nil {
continue
}
if success, err = testOpen(p); err != nil {
continue
} else if !success {
continue
}
logFlags.AddFlag(LogFile)
logPath = p
break
}
}
}
if logFlags.HasFlag(LogSyslog) {
logger = &SyslogLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
}
} else {
if logFlags.HasFlag(LogFile) {
logger = &FileLogger{
StdLogger: StdLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
LogFlags: logConfigFlags,
},
Path: logPath,
}
} else {
logger = &StdLogger{
Prefix: logPrefix,
EnableDebug: enableDebug,
LogFlags: logConfigFlags,
}
}
}
if prefix != "\x00" {
if err = logger.SetPrefix(prefix); err != nil {
return
}
}
if err = logger.Setup(); err != nil {
return
}
if currentPrefix, err = logger.GetPrefix(); err != nil {
return
}
logger.Debug("logger initialized of type %T with prefix %v", logger, currentPrefix)
return
}

View File

@ -235,22 +235,6 @@ func (l *StdLogger) Warning(s string, v ...interface{}) (err error) {
return
}
// ToLogger returns a stdlib log.Logger.
func (l *StdLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *StdLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}
// renderWrite prepares/formats a log message to be written to this StdLogger.
func (l *StdLogger) renderWrite(msg, prio string) {

View File

@ -4,7 +4,7 @@ import (
"fmt"
"log"
"github.com/coreos/go-systemd/v22/journal"
"github.com/coreos/go-systemd/journal"
)
/*
@ -223,19 +223,3 @@ func (l *SystemDLogger) renderWrite(msg string, prio journal.Priority) {
return
}
// ToLogger returns a stdlib log.Logger.
func (l *SystemDLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *SystemDLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@ -1,6 +1,3 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
@ -269,19 +266,3 @@ func (l *SyslogLogger) Warning(s string, v ...interface{}) (err error) {
return
}
// ToLogger returns a stdlib log.Logger.
func (l *SyslogLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *SyslogLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@ -3,7 +3,6 @@ package logging
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"syscall"
@ -343,19 +342,3 @@ func (l *WinLogger) Warning(s string, v ...interface{}) (err error) {
return
}
// ToLogger returns a stdlib log.Logger.
func (l *WinLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
stdLibLog = log.New(l.ToRaw(prio), "", 0)
return
}
// ToRaw returns a *logWriter.
func (l *WinLogger) ToRaw(prio logPrio) (raw *logWriter) {
raw = &logWriter{backend: l, prio: prio}
return
}

View File

@ -3,12 +3,8 @@ package logging
import (
"log"
"os"
"r00t2.io/goutils/bitmask"
)
type logPrio bitmask.MaskBit
/*
Logger is one of the various loggers offered by this module.
*/
@ -27,8 +23,6 @@ type Logger interface {
GetPrefix() (p string, err error)
Setup() (err error)
Shutdown() (err error)
ToLogger(prio logPrio) (stdLibLog *log.Logger)
ToRaw(prio logPrio) (raw *logWriter)
}
/*
@ -111,12 +105,3 @@ type MultiLogger struct {
*/
Loggers map[string]Logger
}
// logWriter is used as a log.Logger and is returned by <Logger>.ToLogger.
type logWriter struct {
backend Logger
prio logPrio
}
// nullWriter is used as a shortcut by NullLogger.ToLogger.
type nullWriter struct{}

View File

@ -1,5 +1,9 @@
package logging
import (
`log/syslog`
)
/*
SystemDLogger (yes, I'm aware it's actually written as "systemd") writes to journald on systemd-enabled systems.
*/
@ -7,3 +11,17 @@ type SystemDLogger struct {
EnableDebug bool
Prefix string
}
// SyslogLogger writes to syslog on syslog-enabled systems.
type SyslogLogger struct {
EnableDebug bool
Prefix string
alert,
crit,
debug,
emerg,
err,
info,
notice,
warning *syslog.Writer
}

View File

@ -1,22 +0,0 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
// +build !windows,!plan9,!wasip1,!js,!ios
package logging
import (
"log/syslog"
)
// SyslogLogger writes to syslog on syslog-enabled systems.
type SyslogLogger struct {
EnableDebug bool
Prefix string
alert,
crit,
debug,
emerg,
err,
info,
notice,
warning *syslog.Writer
}

View File

@ -69,9 +69,6 @@ func (e *MultiError) Error() (errStr string) {
numErrs = len(e.Errors)
}
e.lock.Lock()
defer e.lock.Unlock()
for idx, err := range e.Errors {
if (idx + 1) < numErrs {
errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep)
@ -90,9 +87,6 @@ func (e *MultiError) AddError(err error) {
return
}
e.lock.Lock()
defer e.lock.Unlock()
e.Errors = append(e.Errors, err)
}

View File

@ -1,14 +1,9 @@
package multierr
import (
`sync`
)
// MultiError is a type of error.Error that can contain multiple errors.
type MultiError struct {
// Errors is a slice of errors to combine/concatenate when .Error() is called.
Errors []error `json:"errors"`
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
ErrorSep string `json:"separator"`
lock sync.Mutex
}

View File

@ -1,4 +0,0 @@
/*
Package netx includes extensions to the stdlib `net` module.
*/
package netx

View File

@ -1,24 +0,0 @@
package inetcksum
import (
`encoding/binary`
)
const (
// EmptyCksum is returned for checksums of 0-length byte slices/buffers.
EmptyCksum uint16 = 0xffff
)
const (
// cksumMask is AND'd with a checksum to get the "carried ones".
cksumMask uint32 = 0x0000ffff
// cksumShift is used in the "carried-ones folding".
cksumShift uint32 = 0x00000010
// padShift is used to "pad out" a checksum for odd-length buffers by left-shifting.
padShift uint32 = 0x00000008
)
var (
// ord is the byte order used by the Internet Checksum.
ord binary.ByteOrder = binary.BigEndian
)

View File

@ -1,32 +0,0 @@
/*
Package inetcksum applies the "Internet Checksum" algorithm as specified/described in:
* [RFC 1071]
* [RFC 1141]
* [RFC 1624]
It provides [InetChecksum], which can be used as a:
* [hash.Hash]
* [io.ByteWriter]
* [io.StringWriter]
* [io.Writer]
* [io.WriterTo]
and allows one to retrieve the actual bytes that were checksummed.
It is also fully concurrency-safe.
There is also an [InetChecksumSimple] provided, which is more
tailored for performance/resource usage at the cost of no concurrency
safety and no data retention, which can be used as a:
* [hash.Hash]
* [io.ByteWriter]
* [io.StringWriter]
* [io.Writer]
[RFC 1071]: https://datatracker.ietf.org/doc/html/rfc1071
[RFC 1141]: https://datatracker.ietf.org/doc/html/rfc1141
[RFC 1624]: https://datatracker.ietf.org/doc/html/rfc1624
*/
package inetcksum

View File

@ -1,62 +0,0 @@
package inetcksum
import (
`io`
)
// New returns a new initialized [InetChecksum]. It will never panic.
func New() (i *InetChecksum) {
i = &InetChecksum{}
_ = i.Aligned()
return
}
/*
NewFromBytes returns a new [InetChecksum] initialized with explicit bytes.
b may be nil or 0-length; this will not cause an error.
*/
func NewFromBytes(b []byte) (i *InetChecksum, copied int, err error) {
var cksum InetChecksum
if b != nil && len(b) > 0 {
if copied, err = cksum.Write(b); err != nil {
return
}
_ = i.Aligned()
} else {
i = New()
return
}
i = &cksum
return
}
/*
NewFromBuf returns an [InetChecksum] from a specified [io.Reader].
buf may be nil. If it isn't, NewFromBuf 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 NewFromBuf(buf io.Reader) (i *InetChecksum, copied int64, err error) {
var cksum InetChecksum
_ = i.Aligned()
if buf != nil {
if copied, err = io.Copy(&cksum, buf); err != nil {
return
}
}
i = &cksum
return
}

View File

@ -1,351 +0,0 @@
package inetcksum
import (
`io`
)
/*
Aligned returns true if the current underlying buffer in an InetChecksum is
aligned to the algorithm's requirement for an even number of bytes.
Note that if Aligned returns false, a single null pad byte will be applied
to the underlying data buffer at time of a Sum* call, but will not be written
to the persistent underlying storage.
If aligned's underlying buffer/storage is empty or nil, aligned will be true.
Aligned will also force-set the internal state's aligned status.
*/
func (i *InetChecksum) Aligned() (aligned bool) {
i.alignLock.Lock()
defer i.alignLock.Unlock()
i.bufLock.RLock()
aligned = i.buf.Len()&2 == 0
i.bufLock.RUnlock()
i.aligned = aligned
return
}
// BlockSize returns the number of bytes at a time that InetChecksum operates on. (It will always return 1.)
func (i *InetChecksum) BlockSize() (blockSize int) {
blockSize = 1
return
}
/*
Bytes returns teh bytes currently in the internal storage.
curBuf will be nil if the internal storage has not yet been initialized.
*/
func (i *InetChecksum) Bytes() (curBuf []byte) {
i.bufLock.RLock()
defer i.bufLock.RUnlock()
if i.buf.Len() != 0 {
curBuf = i.buf.Bytes()
}
return
}
// Clear empties the internal buffer (but does not affect the checksum state).
func (i *InetChecksum) Clear() {
i.bufLock.Lock()
defer i.bufLock.Unlock()
i.buf.Reset()
}
/*
DisablePersist disables the internal persistence of an InetChecksum.
This is recommended for integrations that desire the concurrency safety
of an InetChecksum but want a smaller memory footprint and do not need a copy
of data that was hashed.
Any data existing in the buffer will NOT be cleared out if DisablePersist is called.
You must call [InetChecksum.Clear] to do that.
Persistence CANNOT be reenabled once disabled. [InetChecksum.Reset]
must be called to re-enable persistence.
*/
func (i *InetChecksum) DisablePersist() {
i.bufLock.Lock()
defer i.bufLock.Unlock()
i.disabledBuf = true
}
// Len returns the current amount of bytes stored in this InetChecksum's internal buffer.
func (i *InetChecksum) Len() (l int) {
i.bufLock.RLock()
defer i.bufLock.RUnlock()
l = i.buf.Len()
return
}
/*
Reset resets the internal buffer/storage to an empty state.
If persistence was disabled ([InetChecksum.DisablePersist]),
this method will re-enable it with an empty buffer.
If you wish the buffer to be disabled, you must invoke [InetChecksum.DisablePersist]
again.
If you only wish to clear the buffer without losing the checksum state,
use [InetChecksum.Clear].
*/
func (i *InetChecksum) Reset() {
i.alignLock.Lock()
i.bufLock.Lock()
i.sumLock.Lock()
i.lastLock.Lock()
i.aligned = false
i.alignLock.Unlock()
i.buf.Reset()
i.disabledBuf = false
i.bufLock.Unlock()
i.last = 0x00
i.lastLock.Unlock()
i.sum = 0
i.sumLock.Unlock()
}
// Size returns how many bytes a checksum is. (It will always return 2.)
func (i *InetChecksum) Size() (bufSize int) {
bufSize = 2
return
}
// Sum computes the checksum cksum of the current buffer and appends it as big-endian bytes to b.
func (i *InetChecksum) Sum(b []byte) (cksumAppended []byte) {
var sum16 []byte = i.Sum16Bytes()
cksumAppended = append(b, sum16...)
return
}
/*
Sum16 computes the checksum of the current buffer and returns it as a uint16.
This is the native number used in the IPv4 header.
All other Sum* methods wrap this method.
If the underlying buffer is empty or nil, cksum will be 0xffff (65535)
in line with common implementations.
*/
func (i *InetChecksum) Sum16() (cksum uint16) {
var thisSum uint32
i.alignLock.RLock()
i.lastLock.RLock()
i.sumLock.RLock()
thisSum = i.sum
i.sumLock.RUnlock()
if !i.aligned {
/*
"Pad" at the end of the additive ops - a bitshift is used on the sum integer itself
instead of a binary.Append() or append() or such to avoid additional memory allocation.
*/
thisSum += uint32(i.last) << padShift
}
i.lastLock.RUnlock()
i.alignLock.RUnlock()
// Fold the "carried ones".
for thisSum > cksumMask {
thisSum = (thisSum & cksumMask) + (thisSum >> cksumShift)
}
cksum = ^uint16(thisSum)
return
}
/*
Sum16Bytes is a convenience wrapper around [InetChecksum.Sum16]
which returns a slice of the uint16 as a 2-byte-long slice instead.
*/
func (i *InetChecksum) Sum16Bytes() (cksum []byte) {
var sum16 uint16 = i.Sum16()
cksum = make([]byte, 2)
ord.PutUint16(cksum, sum16)
return
}
/*
Write writes data to the underlying InetChecksum buffer. It conforms to [io.Writer].
If this operation returns an error, you MUST call [InetChecksum.Reset] as the instance
being used can no longer be considered to be in a consistent state.
p may be nil or empty; no error will be returned and n will be 0 if so.
Write is concurrency safe; a copy of p is made first and all hashing/internal
storage writing is performed on/which that copy.
*/
func (i *InetChecksum) Write(p []byte) (n int, err error) {
var idx int
var bufLen int
var buf []byte
var iter int
var origLast byte
var origAligned bool
var origSum uint32
if p == nil || len(p) == 0 {
return
}
// The TL;DR here is the checksum boils down to:
// cksum = cksum + ((high << 8) | low)
bufLen = len(p)
buf = make([]byte, bufLen)
copy(buf, p)
i.alignLock.Lock()
defer i.alignLock.Unlock()
i.bufLock.Lock()
defer i.bufLock.Unlock()
i.sumLock.Lock()
defer i.sumLock.Unlock()
i.lastLock.Lock()
defer i.lastLock.Unlock()
origLast = i.last
origAligned = i.aligned
origSum = i.sum
if !i.aligned {
// Last write was unaligned, so pair i.last in.
i.sum += (uint32(i.last) << padShift) | uint32(buf[0])
i.aligned = true
idx = 1
}
// Operate on bytepairs.
// Note that idx is set to either 0 or 1 depending on if
// buf[0] has already been summed in.
for iter = idx; iter < bufLen; iter += 2 {
if iter+1 < bufLen {
// Technically could use "i.sum += uint32(ord.Uint16(buf[iter:iter+2))" here instead.
i.sum += (uint32(buf[iter]) << padShift) | uint32(buf[iter+1])
} else {
i.last = buf[iter]
i.aligned = false
break
}
}
if !i.disabledBuf {
if n, err = i.buf.Write(buf); err != nil {
i.sum = origSum
i.aligned = origAligned
i.last = origLast
return
}
}
return
}
// WriteByte writes a single byte to the underlying storage. It conforms to [io.ByteWriter].
func (i *InetChecksum) WriteByte(c byte) (err error) {
var origLast byte
var origAligned bool
var origSum uint32
i.alignLock.Lock()
defer i.alignLock.Unlock()
i.bufLock.Lock()
defer i.bufLock.Unlock()
i.sumLock.Lock()
defer i.sumLock.Unlock()
i.lastLock.Lock()
defer i.lastLock.Unlock()
origLast = i.last
origAligned = i.aligned
origSum = i.sum
if i.aligned {
// Since it's a single byte, we just set i.last and unalign.
i.last = c
i.aligned = false
} else {
// It's unaligned, so join with i.last and align.
i.sum += (uint32(i.last) << padShift) | uint32(c)
i.aligned = true
}
if !i.disabledBuf {
if err = i.WriteByte(c); err != nil {
i.sum = origSum
i.aligned = origAligned
i.last = origLast
return
}
}
return
}
// WriteString writes a string to the underlying storage. It conforms to [io.StringWriter].
func (i *InetChecksum) WriteString(s string) (n int, err error) {
if n, err = i.Write([]byte(s)); err != nil {
return
}
return
}
// WriteTo writes the current contents of the underlying buffer to w. The contents are not drained. Noop if persistence is disabled.
func (i *InetChecksum) WriteTo(w io.Writer) (n int64, err error) {
var wrtn int
if i.disabledBuf {
return
}
i.bufLock.RLock()
defer i.bufLock.RUnlock()
if wrtn, err = w.Write(i.buf.Bytes()); err != nil {
n = int64(wrtn)
return
}
n = int64(wrtn)
return
}

View File

@ -1,172 +0,0 @@
package inetcksum
/*
Aligned returns true if the current checksum for an InetChecksumSimple is
aligned to the algorithm's requirement for an even number of bytes.
Note that if Aligned returns false, a single null pad byte will be applied
to the underlying data buffer at time of a Sum* call.
*/
func (i *InetChecksumSimple) Aligned() (aligned bool) {
aligned = i.aligned
return
}
// BlockSize returns the number of bytes at a time that InetChecksumSimple operates on. (It will always return 1.)
func (i *InetChecksumSimple) BlockSize() (blockSize int) {
blockSize = 1
return
}
// Reset resets the state of an InetChecksumSimple.
func (i *InetChecksumSimple) Reset() {
i.last = 0x00
i.sum = 0
i.last = 0x00
}
// Size returns how many bytes a checksum is. (It will always return 2.)
func (i *InetChecksumSimple) Size() (bufSize int) {
bufSize = 2
return
}
// Sum computes the checksum cksum of the current buffer and appends it as big-endian bytes to b.
func (i *InetChecksumSimple) Sum(b []byte) (cksumAppended []byte) {
var sum16 []byte = i.Sum16Bytes()
cksumAppended = append(b, sum16...)
return
}
/*
Sum16 computes the checksum of the current buffer and returns it as a uint16.
This is the native number used in the IPv4 header.
All other Sum* methods wrap this method.
If the underlying buffer is empty or nil, cksum will be 0xffff (65535)
in line with common implementations.
*/
func (i *InetChecksumSimple) Sum16() (cksum uint16) {
var thisSum uint32
thisSum = i.sum
if !i.aligned {
/*
"Pad" at the end of the additive ops - a bitshift is used on the sum integer itself
instead of a binary.Append() or append() or such to avoid additional memory allocation.
*/
thisSum += uint32(i.last) << padShift
}
// Fold the "carried ones".
for thisSum > cksumMask {
thisSum = (thisSum & cksumMask) + (thisSum >> cksumShift)
}
cksum = ^uint16(thisSum)
return
}
/*
Sum16Bytes is a convenience wrapper around [InetChecksumSimple.Sum16]
which returns a slice of the uint16 as a 2-byte-long slice instead.
*/
func (i *InetChecksumSimple) Sum16Bytes() (cksum []byte) {
var sum16 uint16 = i.Sum16()
cksum = make([]byte, 2)
ord.PutUint16(cksum, sum16)
return
}
/*
Write writes data to the underlying InetChecksumSimple buffer. It conforms to [io.Writer].
p may be nil or empty; no error will be returned and n will be 0 if so.
A copy of p is made first and all hashing operations are performed on that copy.
*/
func (i *InetChecksumSimple) Write(p []byte) (n int, err error) {
var idx int
var bufLen int
var buf []byte
var iter int
if p == nil || len(p) == 0 {
return
}
// The TL;DR here is the checksum boils down to:
// cksum = cksum + ((high << 8) | low)
bufLen = len(p)
buf = make([]byte, bufLen)
copy(buf, p)
if !i.aligned {
// Last write was unaligned, so pair i.last in.
i.sum += (uint32(i.last) << padShift) | uint32(buf[0])
i.aligned = true
idx = 1
}
// Operate on bytepairs.
// Note that idx is set to either 0 or 1 depending on if
// buf[0] has already been summed in.
for iter = idx; iter < bufLen; iter += 2 {
if iter+1 < bufLen {
// Technically could use "i.sum += uint32(ord.Uint16(buf[iter:iter+2))" here instead.
i.sum += (uint32(buf[iter]) << padShift) | uint32(buf[iter+1])
} else {
i.last = buf[iter]
i.aligned = false
break
}
}
return
}
// WriteByte checksums a single byte. It conforms to [io.ByteWriter].
func (i *InetChecksumSimple) WriteByte(c byte) (err error) {
if i.aligned {
// Since it's a single byte, we just set i.last and unalign.
i.last = c
i.aligned = false
} else {
// It's unaligned, so join with i.last and align.
i.sum += (uint32(i.last) << padShift) | uint32(c)
i.aligned = true
}
return
}
// WriteString checksums a string. It conforms to [io.StringWriter].
func (i *InetChecksumSimple) WriteString(s string) (n int, err error) {
if n, err = i.Write([]byte(s)); err != nil {
return
}
return
}

View File

@ -1,68 +0,0 @@
package inetcksum
import (
`bytes`
`sync`
)
type (
/*
InetChecksum implements [hash.Hash] and various other stdlib interfaces.
If the current data in an InetChecksum's buffer is not aligned
to an even number of bytes -- e.g. InetChecksum.buf.Len() % 2 != 0,
[InetChecksum.Aligned] will return false (otherwise it will return
true).
If [InetChecksum.Aligned] returns false, the checksum result of an
[InetChecksum.Sum] or [InetChecksum.Sum16] (or any other operation
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
the persistent underlying storage.
InetChecksum differs from [InetChecksumSimple] in that it:
* Is MUCH better-suited/safer for concurrent operations - ALL
methods are concurrency-safe.
* Allows the data that is hashed to be recovered from a
sequential internal buffer. (See [InetChecksum.DisablePersist]
to disable the persistent internal buffer.)
At the cost of increased memory usage and additional cycles for mutexing.
Note that once persistence is disabled for an InetChecksum, it cannot be
re-enabled until/unless [InetChecksum.Reset] is called (which will reset
the persistence to enabled with a fresh buffer). Any data within the
persistent buffer will be removed if [InetChecksum.DisablePersist] is called.
*/
InetChecksum struct {
buf bytes.Buffer
disabledBuf bool
aligned bool
last byte
sum uint32
bufLock sync.RWMutex
alignLock sync.RWMutex
lastLock sync.RWMutex
sumLock sync.RWMutex
}
/*
InetChecksumSimple is like [InetChecksum], but with a few key differences.
It is MUCH much more performant/optimized for *single throughput* operations.
Because it also does not retain a buffer of what was hashed, it uses *far* less
memory over time.
However, the downside is it is NOT concurrency safe. There are no promises made
about safety or proper checksum ordering with concurrency for this type, but it
should have much better performance for non-concurrent use.
It behaves much more like a traditional [hash.Hash].
*/
InetChecksumSimple struct {
aligned bool
last byte
sum uint32
}
)

View File

@ -1,4 +0,0 @@
/*
Package remap provides convenience functions around regular expressions, primarily offering maps for named capture groups.
*/
package remap

View File

@ -1,488 +0,0 @@
package remap
/*
Map returns a map[string][]<match bytes> for regexes with named capture groups matched in bytes b.
Note that this supports non-unique group names; [regexp.Regexp] allows for patterns with multiple groups
using the same group name (though your IDE might complain; I know GoLand does).
Each match for each group is in a slice keyed under that group name, with that slice
ordered by the indexing done by the regex match itself.
In summary, the parameters are as follows:
# inclNoMatch
If true, then attempt to return a non-nil matches (as long as b isn't nil).
Group keys will be populated and explicitly defined as nil.
For example, if a pattern
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but b does not match then matches will be:
map[string][][]byte{
"g1": nil,
"g2": nil,
}
# inclNoMatchStrict
If true (and inclNoMatch is true), instead of a single nil the group's values will be
a slice of nil values explicitly matching the number of times the group name is specified
in the pattern.
For example, if a pattern:
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but b does not match then matches will be:
map[string][][]byte{
"g1": [][]byte{
nil,
nil,
},
"g2": [][]byte{
nil,
},
}
# mustMatch
If true, matches will be nil if the entirety of b does not match the pattern (and thus
no capture groups matched) (overrides inclNoMatch) -- explicitly:
matches == nil
Otherwise if false (and assuming inclNoMatch is false), matches will be:
map[string][][]byte{}{}
# Condition Tree
In detail, matches and/or its values may be nil or empty under the following condition tree:
IF b is nil:
THEN matches will always be nil
ELSE:
IF all of b does not match pattern
IF mustMuch is true
THEN matches == nil
ELSE
THEN matches == map[string][][]byte{} (non-nil but empty)
ELSE IF pattern has no named capture groups
IF inclNoMatch is true
THEN matches == map[string][][]byte{} (non-nil but empty)
ELSE
THEN matches == nil
ELSE
IF there are no named group matches
IF inclNoMatch is true
THEN matches is non-nil; matches[<group name>, ...] is/are defined but nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches == nil
ELSE
IF <group name> does not have a match
IF inclNoMatch is true
IF inclNoMatchStrict is true
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
(matches[<group name>] == [][]byte{nil[, nil...]})
ELSE
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches[<group name>] is not defined (_, ok = matches[<group name>]; ok == false)
ELSE
matches[<group name>] == []{<match>[, <match>...]}
*/
func (r *ReMap) Map(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][][]byte) {
var ok bool
var mIdx int
var match []byte
var grpNm string
var names []string
var matchBytes [][]byte
var tmpMap map[string][][]byte = make(map[string][][]byte)
if b == nil {
return
}
names = r.Regexp.SubexpNames()
matchBytes = r.Regexp.FindSubmatch(b)
if matchBytes == nil {
// b does not match pattern
if !mustMatch {
matches = make(map[string][][]byte)
}
return
}
if names == nil || len(names) == 0 || len(names) == 1 {
/*
no named capture groups;
technically only the last condition would be the case.
*/
if inclNoMatch {
matches = make(map[string][][]byte)
}
return
}
names = names[1:]
if len(matchBytes) == 0 || len(matchBytes) == 1 {
/*
no submatches whatsoever.
*Technically* I don't think this condition can actually be reached.
This is more of a safe-return before we re-slice.
*/
matches = make(map[string][][]byte)
if inclNoMatch {
if len(names) >= 1 {
for _, grpNm = range names {
matches[grpNm] = nil
}
}
}
return
}
matchBytes = matchBytes[1:]
for mIdx, match = range matchBytes {
grpNm = names[mIdx]
/*
Thankfully, it's actually a build error if a pattern specifies a named
capture group with an empty name.
So we don't need to worry about accounting for that,
and can just skip over grpNm == "" (which is an *unnamed* capture group).
*/
if grpNm == "" {
continue
}
if match == nil {
// group did not match
if !inclNoMatch {
continue
}
if _, ok = tmpMap[grpNm]; !ok {
if !inclNoMatchStrict {
tmpMap[grpNm] = nil
} else {
tmpMap[grpNm] = [][]byte{nil}
}
} else {
if inclNoMatchStrict {
tmpMap[grpNm] = append(tmpMap[grpNm], nil)
}
}
continue
}
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = make([][]byte, 0)
}
tmpMap[grpNm] = append(tmpMap[grpNm], match)
}
// This *technically* should be completely handled above.
if inclNoMatch {
for _, grpNm = range names {
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = nil
}
}
}
if len(tmpMap) > 0 {
matches = tmpMap
}
return
}
/*
MapString is exactly like ReMap.Map(), but operates on (and returns) strings instead.
(matches will always be nil if s == .)
A small deviation, though; empty strings instead of nils (because duh) will occupy slice placeholders (if `inclNoMatchStrict` is specified).
This unfortunately *does not provide any indication* if an empty string positively matched the pattern (a "hit") or if it was simply
not matched at all (a "miss"). If you need definitive determination between the two conditions, it is instead recommended to either
*not* use inclNoMatchStrict or to use ReMap.Map() instead and convert any non-nil values to strings after.
Particularly:
# inclNoMatch
If true, then attempt to return a non-nil matches (as long as s isn't empty).
Group keys will be populated and explicitly defined as nil.
For example, if a pattern
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but s does not match then matches will be:
map[string][]string{
"g1": nil,
"g2": nil,
}
# inclNoMatchStrict
If true (and inclNoMatch is true), instead of a single nil the group's values will be
a slice of eempty string values explicitly matching the number of times the group name is specified
in the pattern.
For example, if a pattern:
^(?P<g1>foo)(?P<g1>bar)(?P<g2>baz)$
is provided but s does not match then matches will be:
map[string][]string{
"g1": []string{
"",
"",
},
"g2": []string{
"",
},
}
# mustMatch
If true, matches will be nil if the entirety of s does not match the pattern (and thus
no capture groups matched) (overrides inclNoMatch) -- explicitly:
matches == nil
Otherwise if false (and assuming inclNoMatch is false), matches will be:
map[string][]string{}{}
# Condition Tree
In detail, matches and/or its values may be nil or empty under the following condition tree:
IF s is empty:
THEN matches will always be nil
ELSE:
IF all of s does not match pattern
IF mustMuch is true
THEN matches == nil
ELSE
THEN matches == map[string][]string{} (non-nil but empty)
ELSE IF pattern has no named capture groups
IF inclNoMatch is true
THEN matches == map[string][]string{} (non-nil but empty)
ELSE
THEN matches == nil
ELSE
IF there are no named group matches
IF inclNoMatch is true
THEN matches is non-nil; matches[<group name>, ...] is/are defined but nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches == nil
ELSE
IF <group name> does not have a match
IF inclNoMatch is true
IF inclNoMatchStrict is true
THEN matches[<group name>] is defined and non-nil, but populated with placeholder nils
(matches[<group name>] == []string{""[, ""...]})
ELSE
THEN matches[<group name>] is guaranteed defined but may be nil (_, ok = matches[<group name>]; ok == true)
ELSE
THEN matches[<group name>] is not defined (_, ok = matches[<group name>]; ok == false)
ELSE
matches[<group name>] == []{<match>[, <match>...]}
*/
func (r *ReMap) MapString(s string, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][]string) {
var ok bool
var endIdx int
var startIdx int
var chunkIdx int
var grpNm string
var names []string
var matchStr string
/*
A slice of indices or index pairs.
For each element `e` in idxChunks,
* if `e` is nil, no group match.
* if len(e) == 1, only a single character was matched.
* otherwise len(e) == 2, the start and end of the match.
*/
var idxChunks [][]int
var matchIndices []int
var chunkIndices []int // always 2 elements; start pos and end pos
var tmpMap map[string][]string = make(map[string][]string)
/*
OK so this is a bit of a deviation.
It's not as straightforward as above, because there isn't an explicit way
like above to determine if a pattern was *matched as an empty string* vs.
*not matched*.
So instead do roundabout index-y things.
*/
if s == "" {
return
}
/*
I'm not entirely sure how serious they are about "the slice should not be modified"...
DO NOT sort or dedupe `names`! If the same name for groups is duplicated,
it will be duplicated here in proper order and the ordering is tied to
the ordering of matchIndices.
*/
names = r.Regexp.SubexpNames()[:]
matchIndices = r.Regexp.FindStringSubmatchIndex(s)
if matchIndices == nil {
// s does not match pattern at all.
if !mustMatch {
matches = make(map[string][]string)
}
return
}
if names == nil || len(names) <= 1 {
/*
No named capture groups;
technically only the last condition would be the case,
as (regexp.Regexp).SubexpNames() will ALWAYS at the LEAST
return a `[]string{""}`.
*/
if inclNoMatch {
matches = make(map[string][]string)
}
return
}
if len(matchIndices) == 0 || len(matchIndices) == 1 {
/*
No (sub)matches whatsoever.
*technically* I don't think this condition can actually be reached;
matchIndices should ALWAYS either be `nil` or len will be at LEAST 2,
and modulo 2 thereafter since they're PAIRS of indices...
Why they didn't just return a [][]int or [][2]int or something
instead of an []int, who knows.
But we're correcting that poor design.
This is more of a safe-return before we chunk the indices.
*/
matches = make(map[string][]string)
if inclNoMatch {
for _, grpNm = range names {
if grpNm != "" {
matches[grpNm] = nil
}
}
}
return
}
/*
A reslice of `matchIndices` could technically start at 2 (as long as `names` is sliced [1:])
because they're in pairs: []int{<start>, <end>, <start>, <end>, ...}
and the first pair is the entire pattern match (un-resliced names[0]).
Thus the len(matchIndices) == 2*len(names), *even* if you
Keep in mind that since the first element of names is removed,
the first pair here is skipped.
This provides a bit more consistent readability, though.
*/
idxChunks = make([][]int, len(names))
chunkIdx = 0
endIdx = 0
for startIdx = 0; endIdx < len(matchIndices); startIdx += 2 {
endIdx = startIdx + 2
// This technically should never happen.
if endIdx > len(matchIndices) {
endIdx = len(matchIndices)
}
chunkIndices = matchIndices[startIdx:endIdx]
if chunkIndices[0] == -1 || chunkIndices[1] == -1 {
// group did not match
chunkIndices = nil
} else {
if chunkIndices[0] == chunkIndices[1] {
chunkIndices = []int{chunkIndices[0]}
} else {
chunkIndices = matchIndices[startIdx:endIdx]
}
}
idxChunks[chunkIdx] = chunkIndices
chunkIdx++
}
// Now associate with names and pull the string sequence.
for chunkIdx, chunkIndices = range idxChunks {
grpNm = names[chunkIdx]
/*
Thankfully, it's actually a build error if a pattern specifies a named
capture group with an empty name.
So we don't need to worry about accounting for that,
and can just skip over grpNm == ""
(which is either an *unnamed* capture group
OR the first element in `names`, which is always
the entire match).
*/
if grpNm == "" {
continue
}
if chunkIndices == nil || len(chunkIndices) == 0 {
// group did not match
if !inclNoMatch {
continue
}
if _, ok = tmpMap[grpNm]; !ok {
if !inclNoMatchStrict {
tmpMap[grpNm] = nil
} else {
tmpMap[grpNm] = []string{""}
}
} else {
if inclNoMatchStrict {
tmpMap[grpNm] = append(tmpMap[grpNm], "")
}
}
continue
}
switch len(chunkIndices) {
case 1:
// Single character
matchStr = string(s[chunkIndices[0]])
case 2:
// Multiple characters
matchStr = s[chunkIndices[0]:chunkIndices[1]]
}
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = make([]string, 0)
}
tmpMap[grpNm] = append(tmpMap[grpNm], matchStr)
}
// This *technically* should be completely handled above.
if inclNoMatch {
for _, grpNm = range names {
if _, ok = tmpMap[grpNm]; !ok {
tmpMap[grpNm] = nil
}
}
}
if len(tmpMap) > 0 {
matches = tmpMap
}
return
}

View File

@ -1,27 +0,0 @@
package remap
import (
"regexp"
)
type (
// ReMap provides some map-related functions around a regexp.Regexp.
ReMap struct {
*regexp.Regexp
}
// TODO?
/*
ExplicitStringMatch is used with ReMap.MapStringExplicit to indicate if a
capture group result is a hit (a group matched, but e.g. the match value is empty string)
or not (a group did not match).
*/
/*
ExplicitStringMatch struct {
Group string
IsMatch bool
Value string
}
*/
)