5 Commits

Author SHA1 Message Date
brent saner
01adbfc605 v1.12.0
FIXED:
* logging package on Windows had a non-conformant GetLogger().
  While this fix technically breaks API, this was a horribly broken
  thing so I'm including it as a minor bump instead of major and
  thus breaking SemVer. Too bad, so sad, deal with it; Go modules
  have versioning for a reason.
  The previous logging.GetLogger() behavior on Windows has been moved
  to logging.GetLoggerWindows().
2025-11-22 15:53:38 -05:00
brent saner
b1d8ea34a6 v1.11.0
ADDED:
* `stringsx` package
** `stringsx.Indent()`, to indent/prefix multiline strings
** `stringsx.Redact()`, to mask strings
** `stringsx.TrimLines()`, like strings.TrimSpace() but multiline
** `stringsx.TrimSpaceLeft()`, like strings.TrimSpace() but only to the
    left of a string.
** `stringsx.TrimSpaceRight()`, like strings.TrimSpace() but only to the
    right of a string.
2025-11-14 01:02:59 -05:00
brent saner
e101758187 v1.10.3
ADDED:
* netx now has a ton of netmask conversion functions for IPv4 netmasks.
  (IPv6 doesn't really *have* netmasks, so it was intentionally
  excluded).
2025-10-13 15:56:07 -04:00
brent saner
3c49a5b70a v1.10.2
FIXED:
* Windows logging needs to import bitmnask
2025-09-09 08:50:47 -04:00
brent saner
965657d1b2 v1.10.1
FIXED:
* Missed a Reset on the inetcksum.InetChecksumSimple.
2025-09-05 18:55:01 -04:00
19 changed files with 1385 additions and 21 deletions

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.24.5
require ( require (
github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/go-systemd/v22 v22.5.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/sys v0.34.0 golang.org/x/sys v0.34.0
r00t2.io/sysutils v1.14.0 r00t2.io/sysutils v1.14.0
) )

3
go.sum
View File

@@ -5,9 +5,12 @@ github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYC
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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.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 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
r00t2.io/sysutils v1.14.0 h1:Lrio3uPi9CuUdg+sg3WkVV1CK/qcOpV9GdFCGFG1KJs=
r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw= r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw=

View File

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

View File

@@ -3,6 +3,8 @@ package logging
import ( import (
`os` `os`
`path/filepath` `path/filepath`
`r00t2.io/goutils/bitmask`
) )
// Flags for logger configuration. These are used internally. // Flags for logger configuration. These are used internally.

View File

@@ -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 = GetLoggerWindows(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...)
} else { } else {
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags) l, err = GetLoggerWindows(m.EnableDebug, m.Prefix, eventIDs, logFlags)
} }
if err != nil { if err != nil {
return return

View File

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

View File

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

13
netx/consts_nix.go Normal file
View 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
View 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
View 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
View 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
View 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)
}
}

View File

@@ -13,11 +13,17 @@ It provides [InetChecksum], which can be used as a:
* [io.Writer] * [io.Writer]
* [io.WriterTo] * [io.WriterTo]
and is concurrency-safe. 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 There is also an [InetChecksumSimple] provided, which is more
tailored for performance/resource usage at the cost of concurrency tailored for performance/resource usage at the cost of no concurrency
safety and data retention. 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 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

View File

@@ -22,6 +22,15 @@ func (i *InetChecksumSimple) BlockSize() (blockSize int) {
return 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.) // Size returns how many bytes a checksum is. (It will always return 2.)
func (i *InetChecksumSimple) Size() (bufSize int) { func (i *InetChecksumSimple) Size() (bufSize int) {
@@ -151,3 +160,13 @@ func (i *InetChecksumSimple) WriteByte(c byte) (err error) {
return 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
}

5
stringsx/TODO Normal file
View 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?

11
stringsx/consts.go Normal file
View File

@@ -0,0 +1,11 @@
package stringsx
const (
// DefMaskStr is the string used as the default maskStr if left empty in [Redact].
DefMaskStr string = "***"
)
const (
// DefIndentStr is the string used as the default indent if left empty in [Indent].
DefIndentStr string = "\t"
)

4
stringsx/doc.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package stringsx aims to extend functionality of the stdlib [strings] module.
*/
package stringsx

249
stringsx/funcs.go Normal file
View File

@@ -0,0 +1,249 @@
package stringsx
import (
`strings`
`unicode`
)
/*
Indent takes string s and indents it with string `indent` `level` times.
If indent is an empty string, [DefIndentStr] will be used.
If ws is true, lines consisting of only whitespace will be indented as well.
(To then trim any extraneous trailing space, you may want to use [TrimSpaceRight]
or [TrimLines].)
If empty is true, lines with no content will be replaced with lines that purely
consist of (indent * level) (otherwise they will be left as empty lines).
This function can also be used to prefix lines with arbitrary strings as well.
e.g:
Indent("foo\nbar\nbaz\n", "# ", 1, false, false)
would yield:
# foo
# bar
# baz
<empty line>
thus allowing you to "comment out" multiple lines at once.
*/
func Indent(s, indent string, level uint, ws, empty bool) (indented string) {
var i string
var nl string
var endsNewline bool
var sb strings.Builder
var lineStripped string
if indent == "" {
indent = DefIndentStr
}
// This condition functionally won't do anything, so just return the input as-is.
if level == 0 {
indented = s
return
}
i = strings.Repeat(indent, int(level))
// This condition functionally won't do anything, so just return the input as-is.
if s == "" {
if empty {
indented = i
}
return
}
for line := range strings.Lines(s) {
lineStripped = strings.TrimSpace(line)
nl = getNewLine(line)
endsNewline = nl != ""
// fmt.Printf("%#v => %#v\n", line, lineStripped)
if lineStripped == "" {
// fmt.Printf("WS/EMPTY LINE (%#v) (ws %v, empty %v): \n", s, ws, empty)
if line != (lineStripped + nl) {
// whitespace-only line
if ws {
sb.WriteString(i)
}
} else {
// empty line
if empty {
sb.WriteString(i)
}
}
sb.WriteString(line)
continue
}
// non-empty/non-whitespace-only line.
sb.WriteString(i + line)
}
// If it ends with a trailing newline and nothing after, strings.Lines() will skip the last (empty) line.
if endsNewline && empty {
nl = getNewLine(s)
sb.WriteString(i)
}
indented = sb.String()
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.
*/
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 to be used outside of this package 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
}

451
stringsx/funcs_test.go Normal file
View File

@@ -0,0 +1,451 @@
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 TestIndent(t *testing.T) {
var out string
var tests []testIndentSet = []testIndentSet{
testIndentSet{
name: "standard, no trailing newline",
orig: "foo\nbar\nbaz",
indent: "",
lvl: 1,
ws: false,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz",
},
testIndentSet{
name: "standard, trailing newline",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: false,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n",
},
testIndentSet{
name: "standard, trailing newline with empty",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: false,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t",
},
testIndentSet{
name: "standard, trailing newline with ws",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: true,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n",
},
testIndentSet{
name: "standard, trailing newline with ws and empty",
orig: "foo\nbar\nbaz\n",
indent: "",
lvl: 1,
ws: true,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t",
},
testIndentSet{
name: "standard, trailing ws newline with empty",
orig: "foo\nbar\nbaz\n ",
indent: "",
lvl: 1,
ws: false,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n ",
},
testIndentSet{
name: "standard, trailing ws newline with ws",
orig: "foo\nbar\nbaz\n ",
indent: "",
lvl: 1,
ws: true,
empty: false,
tgt: "\tfoo\n\tbar\n\tbaz\n\t ",
},
testIndentSet{
name: "standard, trailing ws newline with ws and empty",
orig: "foo\nbar\nbaz\n \n",
indent: "",
lvl: 1,
ws: true,
empty: true,
tgt: "\tfoo\n\tbar\n\tbaz\n\t \n\t",
},
testIndentSet{
name: "comment",
orig: "foo\nbar\nbaz",
indent: "# ",
lvl: 1,
ws: false,
empty: false,
tgt: "# foo\n# bar\n# baz",
},
}
for idx, ts := range tests {
out = Indent(ts.orig, ts.indent, ts.lvl, ts.ws, ts.empty)
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 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,
)
}
}
}