Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
927ad08057
|
||
|
|
2edbc9306d
|
||
|
|
bb71be187f
|
||
|
|
834395c050
|
||
|
|
ef56898d6b
|
||
|
|
006cf39fa1
|
||
|
|
145c32268e
|
||
|
|
6ddfcdb416
|
||
|
|
79f10b7611
|
||
|
|
01adbfc605
|
||
|
|
b1d8ea34a6
|
||
|
|
e101758187
|
||
|
|
3c49a5b70a
|
||
|
|
965657d1b2
|
||
|
|
970acd0ee4
|
||
|
|
2222cea7fb
|
||
|
|
688abd0874
|
||
|
|
a1f87d6b51
|
||
|
|
07951f1f03
|
||
|
|
bae0abe960
|
||
|
|
368ae0cb8e
|
||
|
|
154170c0e5
|
||
|
|
d9bd928edb
|
||
|
|
dc2ed32352
|
||
|
|
e734e847c4
|
||
|
|
2203de4e32
|
||
|
|
a0c6df14aa
|
||
|
|
fd720f2b34
|
||
|
|
3c543a05e7
|
||
|
|
e5191383a7
|
||
|
|
ae49f42c0c
|
||
|
|
b87934e8a9
|
||
|
70d6c2cbb3
|
|||
|
a445a51c0d
|
|||
|
a2a849600b
|
|||
|
94145fb4c7
|
|||
|
81a2d308f0
|
|||
|
c4b3c6441a
|
|||
|
1c5abd4083
|
|||
|
d98363c0d7
|
|||
|
39e0a1fd43
|
|||
|
3d0d420454
|
19
.encoding.TODO/bit/docs.go
Normal file
19
.encoding.TODO/bit/docs.go
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
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
|
||||
14
.encoding.TODO/bit/funcs.go
Normal file
14
.encoding.TODO/bit/funcs.go
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
*/
|
||||
34
.encoding.TODO/bit/types.go
Normal file
34
.encoding.TODO/bit/types.go
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
)
|
||||
31
.githooks/pre-commit/01-docgen
Executable file
31
.githooks/pre-commit/01-docgen
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
orig="${PWD}"
|
||||
|
||||
if ! command -v asciidoctor &> /dev/null;
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
for f in $(find . -type f -iname "README.adoc"); do
|
||||
filename=$(basename -- "${f}")
|
||||
docsdir=$(dirname -- "${f}")
|
||||
nosuffix="${filename%.*}"
|
||||
pfx="${docsdir}/${nosuffix}"
|
||||
|
||||
newf="${pfx}.html"
|
||||
asciidoctor -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
|
||||
echo "Generated ${newf} from ${f}"
|
||||
git add "${newf}"
|
||||
if command -v pandoc &> /dev/null;
|
||||
then
|
||||
newf="${pfx}.md"
|
||||
asciidoctor -a ROOTDIR="${orig}/" -b docbook -o - "${f}" | pandoc -f docbook -t markdown_strict -o "${newf}"
|
||||
echo "Generated ${newf} from ${f}"
|
||||
git add "${newf}"
|
||||
fi
|
||||
cd ${orig}
|
||||
done
|
||||
echo "Regenerated docs"
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -28,6 +28,11 @@
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
# But DO include the actual tests.
|
||||
!_test.go
|
||||
!*_test.go
|
||||
!*_test_*.go
|
||||
!*_test/
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
171
bitmask/bitmask.go
Normal file
171
bitmask/bitmask.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package bitmask
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
// MaskBit is a flag container.
|
||||
type MaskBit uint
|
||||
|
||||
/*
|
||||
NewMaskBit is a convenience function.
|
||||
It will return a MaskBit with a (referenced) value of 0, so set your consts up accordingly.
|
||||
|
||||
It is highly recommended to set this default as a "None" flag (separate from your iotas!)
|
||||
as shown in the example.
|
||||
*/
|
||||
func NewMaskBit() (m *MaskBit) {
|
||||
|
||||
m = new(MaskBit)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewMaskBitExplicit is like NewMaskBit, but allows you to specify a non-zero (0x0) value.
|
||||
func NewMaskBitExplicit(value uint) (m *MaskBit) {
|
||||
|
||||
var v MaskBit = MaskBit(value)
|
||||
|
||||
m = &v
|
||||
|
||||
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.
|
||||
*/
|
||||
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 {
|
||||
r = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddFlag adds MaskBit flag to m.
|
||||
func (m *MaskBit) AddFlag(flag MaskBit) {
|
||||
|
||||
*m |= flag
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ClearFlag removes MaskBit flag from m.
|
||||
func (m *MaskBit) ClearFlag(flag MaskBit) {
|
||||
|
||||
*m &^= flag
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ToggleFlag switches MaskBit flag in m to its inverse; if true, it is now false and vice versa.
|
||||
func (m *MaskBit) ToggleFlag(flag MaskBit) {
|
||||
|
||||
*m ^= flag
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Bytes returns the current value of a MasBit as a byte slice (big-endian).
|
||||
|
||||
If trim is false, b will (probably) be 4 bytes long if you're on a 32-bit size system,
|
||||
and b will (probably) be 8 bytes long if you're on a 64-bit size system. You can determine
|
||||
the size of the resulting slice via (math/)bits.UintSize / 8.
|
||||
|
||||
If trim is true, it will trim leading null bytes (if any). This will lead to an unpredictable
|
||||
byte slice length in b, but is most likely preferred for byte operations.
|
||||
|
||||
*/
|
||||
func (m *MaskBit) Bytes(trim bool) (b []byte) {
|
||||
|
||||
var b2 []byte
|
||||
var size int = bits.UintSize / 8
|
||||
var err error
|
||||
|
||||
b2 = make([]byte, size)
|
||||
|
||||
switch s := bits.UintSize; s {
|
||||
case 32:
|
||||
binary.BigEndian.PutUint32(b2[:], uint32(*m))
|
||||
case 64:
|
||||
binary.BigEndian.PutUint64(b2[:], uint64(*m))
|
||||
default:
|
||||
err = errors.New("unsupported Uint/system bit size")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if trim {
|
||||
b = bytes.TrimLeft(b2, "\x00")
|
||||
return
|
||||
} else {
|
||||
b = b2
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
v = uint(*m)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package bitmask
|
||||
|
||||
// MaskBit is a flag container.
|
||||
type MaskBit uint8
|
||||
|
||||
/*
|
||||
NewMaskBit is a convenience function.
|
||||
It will return a MaskBit with a (referenced) value of 0, so set your consts up accordingly.
|
||||
It is highly recommended to set this default as a "None" flag (separate from your iotas!)
|
||||
as shown in the example.
|
||||
*/
|
||||
func NewMaskBit() (m *MaskBit) {
|
||||
|
||||
m = new(MaskBit)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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 != 0 {
|
||||
r = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddFlag adds MaskBit flag to m.
|
||||
func (m *MaskBit) AddFlag(flag MaskBit) {
|
||||
*m |= flag
|
||||
return
|
||||
}
|
||||
|
||||
// ClearFlag removes MaskBit flag from m.
|
||||
func (m *MaskBit) ClearFlag(flag MaskBit) {
|
||||
*m &= flag
|
||||
return
|
||||
}
|
||||
|
||||
// ToggleFlag switches MaskBit flag in m to its inverse; if true, it is now false and vice versa.
|
||||
func (m *MaskBit) ToggleFlag(flag MaskBit) {
|
||||
*m ^= flag
|
||||
return
|
||||
}
|
||||
129
bitmask/doc.go
129
bitmask/doc.go
@@ -1,9 +1,35 @@
|
||||
/*
|
||||
Package bitmask handles a flag-like opt/bitmask system.
|
||||
|
||||
See https://yourbasic.org/golang/bitmask-flag-set-clear/ for more information.
|
||||
See https://yourbasic.org/golang/bitmask-flag-set-clear/ for basic information on what bitmasks are and why they're useful.
|
||||
|
||||
To use this, set constants like thus:
|
||||
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:
|
||||
|
||||
package main
|
||||
|
||||
@@ -11,18 +37,18 @@ To use this, set constants like thus:
|
||||
"r00t2.io/goutils/bitmask"
|
||||
)
|
||||
|
||||
const OPTNONE types.MaskBit = 0
|
||||
const OPTNONE bitmask.MaskBit = 0
|
||||
const (
|
||||
OPT1 types.MaskBit = 1 << iota
|
||||
OPT1 bitmask.MaskBit = 1 << iota
|
||||
OPT2
|
||||
OPT3
|
||||
// ...
|
||||
)
|
||||
|
||||
var MyMask *MaskBit
|
||||
var MyMask *bitmask.MaskBit
|
||||
|
||||
func main() {
|
||||
MyMask = types.NewMaskBit
|
||||
MyMask = bitmask.NewMaskBit()
|
||||
|
||||
MyMask.AddFlag(OPT1)
|
||||
MyMask.AddFlag(OPT3)
|
||||
@@ -41,5 +67,96 @@ As would this:
|
||||
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]
|
||||
|
||||
[exbibytes]: https://simple.wikipedia.org/wiki/Exbibyte
|
||||
*/
|
||||
package bitmask
|
||||
|
||||
27
go.mod
27
go.mod
@@ -1,10 +1,27 @@
|
||||
module r00t2.io/goutils
|
||||
|
||||
go 1.16
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
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
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/coreos/go-systemd/v22 v22.6.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/sys v0.39.0
|
||||
r00t2.io/sysutils v1.15.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
)
|
||||
|
||||
64
go.sum
64
go.sum
@@ -1,8 +1,56 @@
|
||||
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=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
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/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
r00t2.io/sysutils v1.15.1 h1:0EVZZAxTFqQN6jjfjqUKkXye0LMshUA5MO7l3Wd6wH8=
|
||||
r00t2.io/sysutils v1.15.1/go.mod h1:T0iOnaZaSG5NE1hbXTqojRZc0ia/u8TB73lV7zhMz58=
|
||||
|
||||
7
iox/docs.go
Normal file
7
iox/docs.go
Normal file
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
Package iox includes extensions to the stdlib `io` module.
|
||||
|
||||
Not everything in here is considered fully stabilized yet,
|
||||
but it should be usable.
|
||||
*/
|
||||
package iox
|
||||
17
iox/errs.go
Normal file
17
iox/errs.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
|
||||
ErrChunkTooBig error = errors.New("chunk too big for method")
|
||||
ErrChunkTooSmall error = errors.New("chunk too small for buffer")
|
||||
ErrInvalidChunkSize error = errors.New("an invalid chunk size was passed")
|
||||
ErrNilCtx error = errors.New("a nil context was passed")
|
||||
ErrNilReader error = errors.New("a nil reader was passed")
|
||||
ErrNilWriter error = errors.New("a nil writer was passed")
|
||||
ErrShortRead error = errors.New("a read was cut short with no EOF")
|
||||
ErrShortWrite error = errors.New("a write was cut short with no error")
|
||||
)
|
||||
247
iox/funcs.go
Normal file
247
iox/funcs.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
`context`
|
||||
`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 cases where this is dreadfully undesired.
|
||||
|
||||
One can, of course, use [io.CopyBuffer], but this is a bit annoying since you then have to provide a buffer yourself.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// CopyCtxBufN copies from `src` to `dst`, `n` bytes at a time, interruptible by `ctx`.
|
||||
func CopyCtxBufN(ctx context.Context, dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||
|
||||
var nr int
|
||||
var nw int
|
||||
var end bool
|
||||
var buf []byte
|
||||
|
||||
if ctx == nil {
|
||||
err = ErrNilCtx
|
||||
return
|
||||
}
|
||||
if n <= 0 {
|
||||
err = ErrBufTooSmall
|
||||
return
|
||||
}
|
||||
|
||||
endCopy:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
return
|
||||
default:
|
||||
buf = make([]byte, n)
|
||||
nr, err = src.Read(buf)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
end = true
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
buf = buf[:nr]
|
||||
|
||||
if nw, err = dst.Write(buf); err != nil {
|
||||
written += int64(nw)
|
||||
return
|
||||
}
|
||||
written += int64(nw)
|
||||
if len(buf) != nw {
|
||||
err = io.ErrShortWrite
|
||||
return
|
||||
}
|
||||
if end {
|
||||
break endCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CopyBufWith allows for specifying a buffer allocator function, otherwise acts as [CopyBufN].
|
||||
|
||||
bufFunc *MUST NOT* return a nil or len == 0 buffer. [ErrBufTooSmall] will be returned if it does.
|
||||
|
||||
This uses a fixed buffer size from a single call to `bufFunc`.
|
||||
If you need something with dynamic buffer sizing according to some state, use [CopyBufWithDynamic] instead.
|
||||
(Note that CopyBufWithDynamic is generally a little slower, but it should only be noticeable on very large amounts of data.)
|
||||
*/
|
||||
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||
|
||||
var buf []byte = bufFunc()
|
||||
|
||||
if buf == nil || len(buf) == 0 {
|
||||
err = ErrBufTooSmall
|
||||
return
|
||||
}
|
||||
|
||||
written, err = io.CopyBuffer(dst, src, buf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CopyBufWithDynamic is like [CopyBufWith] except it will call bufFunc after each previous buffer is written.
|
||||
|
||||
That is to say (using a particularly contrived example):
|
||||
|
||||
import time
|
||||
|
||||
func dynBuf() (b []byte) {
|
||||
|
||||
var t time.Time = time.Now()
|
||||
|
||||
b = make([]byte, t.Seconds())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Then:
|
||||
|
||||
CopyBufWithDynamic(w, r, dynBuf)
|
||||
|
||||
will use a buffer sized to the seconds of the time it reads in/writes out the next buffer, whereas with [CopyBufWith]:
|
||||
|
||||
CopyBufWith(w, r, dynBuf)
|
||||
|
||||
would use a *fixed* buffer size of whatever the seconds was equal to at the time of the *first call* to dynBuf.
|
||||
|
||||
`src` MUST return an [io.EOF] when its end is reached, but (as per e.g. [io.CopyBuffer]) the io.EOF error will not
|
||||
be returned from CopyBufWithDynamic. (Any/all other errors encountered will be returned, however, and copying will
|
||||
immediately cease.)
|
||||
*/
|
||||
func CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||
|
||||
var nr int
|
||||
var nw int
|
||||
var end bool
|
||||
var buf []byte
|
||||
|
||||
for {
|
||||
buf = bufFunc()
|
||||
if buf == nil || len(buf) == 0 {
|
||||
err = ErrBufTooSmall
|
||||
return
|
||||
}
|
||||
nr, err = src.Read(buf)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
end = true
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
buf = buf[:nr]
|
||||
|
||||
if nw, err = dst.Write(buf); err != nil {
|
||||
written += int64(nw)
|
||||
return
|
||||
}
|
||||
written += int64(nw)
|
||||
if len(buf) != nw {
|
||||
err = ErrShortWrite
|
||||
return
|
||||
}
|
||||
if end {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewChunker returns a [ChunkLocker] ready to use.
|
||||
func NewChunker(chunkSize uint) (c *ChunkLocker, err error) {
|
||||
|
||||
c = &ChunkLocker{}
|
||||
err = c.SetChunkLen(chunkSize)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewCtxIO returns a [CtxIO].
|
||||
func NewCtxIO(ctx context.Context, r io.Reader, w io.Writer, chunkSize uint) (c *CtxIO, err error) {
|
||||
|
||||
if r == nil {
|
||||
err = ErrNilReader
|
||||
return
|
||||
}
|
||||
if w == nil {
|
||||
err = ErrNilWriter
|
||||
return
|
||||
}
|
||||
|
||||
if chunkSize == 0 {
|
||||
err = ErrInvalidChunkSize
|
||||
return
|
||||
}
|
||||
|
||||
if ctx == nil {
|
||||
err = ErrNilCtx
|
||||
return
|
||||
}
|
||||
|
||||
c = &CtxIO{
|
||||
r: r,
|
||||
w: w,
|
||||
l: ChunkLocker{
|
||||
chunkLen: chunkSize,
|
||||
},
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
NewXIO returns a nil [XIO].
|
||||
|
||||
A weird "feature" of Golang is that a nil XIO is perfectly fine to use;
|
||||
it's completely stateless and only has pointer receivers that only work with passed in
|
||||
values so `new(XIO)` is completely unnecessary (as is NewXCopier).
|
||||
In other words, this works fine:
|
||||
|
||||
var xc *iox.XIO
|
||||
|
||||
if n, err = xc.Copy(w, r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
This function is just to maintain cleaner-looking code if you should so need it,
|
||||
or want an XIO without declaring one:
|
||||
|
||||
if n, err = iox.NewXCopier().Copy(w, r); err != nil {
|
||||
return
|
||||
}
|
||||
*/
|
||||
func NewXIO() (x *XIO) {
|
||||
// No-op lel
|
||||
return
|
||||
}
|
||||
28
iox/funcs_chunklocker.go
Normal file
28
iox/funcs_chunklocker.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package iox
|
||||
|
||||
// GetChunkLen returns the current chunk size/length in bytes.
|
||||
func (c *ChunkLocker) GetChunkLen() (size uint) {
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
size = c.chunkLen
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetChunkLen sets the current chunk size/length in bytes.
|
||||
func (c *ChunkLocker) SetChunkLen(size uint) (err error) {
|
||||
|
||||
if size == 0 {
|
||||
err = ErrInvalidChunkSize
|
||||
return
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.chunkLen = size
|
||||
|
||||
return
|
||||
}
|
||||
173
iox/funcs_ctxio.go
Normal file
173
iox/funcs_ctxio.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`context`
|
||||
`io`
|
||||
`math`
|
||||
)
|
||||
|
||||
func (c *CtxIO) Copy(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||
if c.l.chunkLen > math.MaxInt64 {
|
||||
err = ErrChunkTooBig
|
||||
}
|
||||
return CopyCtxBufN(c.ctx, dst, src, int64(c.l.chunkLen))
|
||||
}
|
||||
|
||||
func (c *CtxIO) CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||
if n <= 0 {
|
||||
err = ErrBufTooSmall
|
||||
return
|
||||
}
|
||||
return CopyCtxBufN(c.ctx, dst, src, n)
|
||||
}
|
||||
|
||||
func (c *CtxIO) GetChunkLen() (size uint) {
|
||||
return c.l.GetChunkLen()
|
||||
}
|
||||
|
||||
func (c *CtxIO) Read(p []byte) (n int, err error) {
|
||||
|
||||
var nr int64
|
||||
|
||||
if nr, err = c.ReadWithContext(c.ctx, p); err != nil {
|
||||
if nr > math.MaxInt {
|
||||
n = math.MaxInt
|
||||
} else {
|
||||
n = int(nr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if nr > math.MaxInt {
|
||||
n = math.MaxInt
|
||||
} else {
|
||||
n = int(nr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CtxIO) ReadWithContext(ctx context.Context, p []byte) (n int64, err error) {
|
||||
|
||||
var nr int
|
||||
var off int
|
||||
var buf []byte
|
||||
|
||||
if p == nil || len(p) == 0 {
|
||||
return
|
||||
}
|
||||
if c.buf.Len() == 0 {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
|
||||
if c.l.chunkLen > uint(len(p)) {
|
||||
// Would normally be a single chunk, so one-shot it.
|
||||
nr, err = c.buf.Read(p)
|
||||
n = int64(nr)
|
||||
return
|
||||
}
|
||||
|
||||
// Chunk over it.
|
||||
endRead:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
return
|
||||
default:
|
||||
/*
|
||||
off(set) is the index of the *next position* to write to.
|
||||
Therefore the last offset == len(p),
|
||||
therefore:
|
||||
|
||||
* if off == len(p), "done" (return no error, do *not* read from buf)
|
||||
* if off + c.l.chunkLen > len(p), buf should be len(p) - off instead
|
||||
*/
|
||||
if off == len(p) {
|
||||
break endRead
|
||||
}
|
||||
if uint(off)+c.l.chunkLen > uint(len(p)) {
|
||||
buf = make([]byte, len(p)-off)
|
||||
} else {
|
||||
buf = make([]byte, c.l.chunkLen)
|
||||
}
|
||||
nr, err = c.buf.Read(buf)
|
||||
n += int64(nr)
|
||||
if nr > 0 {
|
||||
off += nr
|
||||
copy(p[off:], buf[:nr])
|
||||
}
|
||||
if err == io.EOF {
|
||||
break endRead
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CtxIO) SetChunkLen(size uint) (err error) {
|
||||
return c.l.SetChunkLen(size)
|
||||
}
|
||||
|
||||
func (c *CtxIO) SetContext(ctx context.Context) (err error) {
|
||||
|
||||
if ctx == nil {
|
||||
err = ErrNilCtx
|
||||
return
|
||||
}
|
||||
|
||||
c.ctx = ctx
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CtxIO) Write(p []byte) (n int, err error) {
|
||||
|
||||
var nw int64
|
||||
|
||||
if c.l.chunkLen > math.MaxInt64 {
|
||||
err = ErrChunkTooBig
|
||||
return
|
||||
}
|
||||
if nw, err = c.WriteNWithContext(c.ctx, p, int64(c.l.chunkLen)); err != nil {
|
||||
if nw > math.MaxInt {
|
||||
n = math.MaxInt
|
||||
} else {
|
||||
n = int(nw)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if nw > math.MaxInt {
|
||||
n = math.MaxInt
|
||||
} else {
|
||||
n = int(nw)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CtxIO) WriteNWithContext(ctx context.Context, p []byte, n int64) (written int64, err error) {
|
||||
return CopyCtxBufN(ctx, &c.buf, bytes.NewReader(p), n)
|
||||
}
|
||||
|
||||
func (c *CtxIO) WriteRune(r rune) (n int, err error) {
|
||||
|
||||
// We don't even bother listening for the ctx.Done because it's a single rune.
|
||||
n, err = c.buf.WriteRune(r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CtxIO) WriteWithContext(ctx context.Context, p []byte) (n int64, err error) {
|
||||
if c.l.chunkLen > math.MaxInt64 {
|
||||
err = ErrChunkTooBig
|
||||
return
|
||||
}
|
||||
return CopyCtxBufN(ctx, &c.buf, bytes.NewReader(p), int64(c.l.chunkLen))
|
||||
}
|
||||
40
iox/funcs_xio.go
Normal file
40
iox/funcs_xio.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
`io`
|
||||
)
|
||||
|
||||
// Copy copies [io.Reader] `src` to [io.Writer] `dst`. It implements [Copier].
|
||||
func (x *XIO) Copy(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||
return io.Copy(dst, src)
|
||||
}
|
||||
|
||||
// CopyBuffer copies [io.Reader] `src` to [io.Writer] `dst` using buffer `buf`. It implements [CopyBufferer].
|
||||
func (x *XIO) CopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) {
|
||||
return io.CopyBuffer(dst, src, buf)
|
||||
}
|
||||
|
||||
// CopyBufWith copies [io.Reader] `src` to [io.Writer] `dst` using buffer returner `bufFunc`. It implements [SizedCopyBufferInvoker].
|
||||
func (x *XIO) CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||
return CopyBufWith(dst, src, bufFunc)
|
||||
}
|
||||
|
||||
// CopyBufWithDynamic copies [io.Reader] `src` to [io.Writer] `dst` using buffer returner `bufFunc` for each chunk. It implements [DynamicSizedCopyBufferInvoker].
|
||||
func (x *XIO) CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||
return CopyBufWithDynamic(dst, src, bufFunc)
|
||||
}
|
||||
|
||||
/*
|
||||
CopyBufN reads buffered bytes from [io.Reader] `src` and copies to [io.Writer] `dst`
|
||||
using the synchronous buffer size `n`.
|
||||
|
||||
It implements [SizedCopyBufferer].
|
||||
*/
|
||||
func (x *XIO) CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||
return CopyBufN(dst, src, n)
|
||||
}
|
||||
|
||||
// CopyN copies from [io.Reader] `src` to [io.Writer] `w`, `n` bytes at a time. It implements [SizedCopier].
|
||||
func (x *XIO) CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||
return io.CopyN(dst, src, n)
|
||||
}
|
||||
209
iox/types.go
Normal file
209
iox/types.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`context`
|
||||
`io`
|
||||
`sync`
|
||||
)
|
||||
|
||||
type (
|
||||
/*
|
||||
RuneWriter matches the behavior of [bytes.Buffer.WriteRune] and [bufio.Writer.WriteRune].
|
||||
|
||||
(Note that this package does not have a "RuneReader"; see [io.RuneReader] instead.)
|
||||
*/
|
||||
RuneWriter interface {
|
||||
WriteRune(r rune) (n int, err error)
|
||||
}
|
||||
|
||||
// Copier matches the signature/behavior of [io.Copy]. Implemented by [XIO].
|
||||
Copier interface {
|
||||
Copy(dst io.Writer, src io.Reader) (written int64, err error)
|
||||
}
|
||||
|
||||
// CopyBufferer matches the signature/behavior of [io.CopyBuffer]. Implemented by [XIO].
|
||||
CopyBufferer interface {
|
||||
CopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error)
|
||||
}
|
||||
|
||||
// SizedCopier matches the signature/behavior of [io.CopyN]. Implemented by [XIO].
|
||||
SizedCopier interface {
|
||||
CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
|
||||
}
|
||||
|
||||
// SizedCopyBufferer matches the signature/behavior of [CopyBufN]. Implemented by [XIO].
|
||||
SizedCopyBufferer interface {
|
||||
CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
|
||||
}
|
||||
|
||||
// SizedCopyBufferInvoker matches the signature/behavior of [CopyBufWith]. Implemented by [XIO].
|
||||
SizedCopyBufferInvoker interface {
|
||||
CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error)
|
||||
}
|
||||
|
||||
// DynamicSizedCopyBufferInvoker matches the signature/behavior of [CopyBufWithDynamic]. Implemented by [XIO].
|
||||
DynamicSizedCopyBufferInvoker interface {
|
||||
CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error)
|
||||
}
|
||||
|
||||
/*
|
||||
Chunker is used by both [ContextReader] and [ContextWriter] to set/get the current chunk size.
|
||||
Chunking is inherently required to be specified in order to interrupt reads/writes/copies with a [context.Context].
|
||||
|
||||
Implementations *must* use a [sync.RWMutex] to get (RLock) and set (Lock) the chunk size.
|
||||
The chunk size *must not* be directly accessible to maintain concurrency safety assumptions.
|
||||
*/
|
||||
Chunker interface {
|
||||
// GetChunkLen returns the current chunk size/length in bytes.
|
||||
GetChunkLen() (size uint)
|
||||
// SetChunkLen sets the current chunk size/length in bytes.
|
||||
SetChunkLen(size uint) (err error)
|
||||
}
|
||||
|
||||
/*
|
||||
ChunkReader implements a chunking reader.
|
||||
Third-party implementations *must* respect the chunk size locking (see [Chunker]).
|
||||
|
||||
The Read method should read in chunks of the internal chunk size.
|
||||
*/
|
||||
ChunkReader interface {
|
||||
io.Reader
|
||||
Chunker
|
||||
}
|
||||
|
||||
/*
|
||||
ChunkWriter implements a chunking writer.
|
||||
Third-party implementations *must* respect the chunk size locking (see [Chunker]).
|
||||
|
||||
The Write method should write out in chunks of the internal chunk size.
|
||||
*/
|
||||
ChunkWriter interface {
|
||||
io.Writer
|
||||
Chunker
|
||||
}
|
||||
|
||||
// ChunkReadWriter implements a chunking reader/writer.
|
||||
ChunkReadWriter interface {
|
||||
ChunkReader
|
||||
ChunkWriter
|
||||
}
|
||||
|
||||
/*
|
||||
ContextSetter allows one to set an internal context.
|
||||
|
||||
A nil context should return an error.
|
||||
*/
|
||||
ContextSetter interface {
|
||||
SetContext(context context.Context) (err error)
|
||||
}
|
||||
|
||||
/*
|
||||
ContextCopier is defined to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||
|
||||
The Copy method should use an internal context and chunk size
|
||||
(and thus wrap [CopyCtxBufN] internally on an external call to Copy, etc.).
|
||||
*/
|
||||
ContextCopier interface {
|
||||
Copier
|
||||
Chunker
|
||||
ContextSetter
|
||||
SizedCopyBufferer
|
||||
}
|
||||
|
||||
/*
|
||||
ContextReader is primarily here to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||
|
||||
The Read method should use an internal context and chunk size.
|
||||
|
||||
The ReadWithContext method should use an internal chunk size.
|
||||
*/
|
||||
ContextReader interface {
|
||||
ChunkReader
|
||||
ContextSetter
|
||||
ReadWithContext(ctx context.Context, p []byte) (n int64, err error)
|
||||
}
|
||||
|
||||
/*
|
||||
ContextWriter is primarily here to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||
|
||||
The Write method should use an internal context.
|
||||
|
||||
The WriteWithContext should use an internal chunk size.
|
||||
*/
|
||||
ContextWriter interface {
|
||||
ChunkWriter
|
||||
ContextSetter
|
||||
WriteWithContext(ctx context.Context, p []byte) (n int64, err error)
|
||||
WriteNWithContext(ctx context.Context, p []byte, n int64) (written int64, err error)
|
||||
}
|
||||
|
||||
/*
|
||||
ContextReadWriter is primarily here to allow for consumer-provided types.
|
||||
|
||||
See [CtxIO] for a package-provided type.
|
||||
*/
|
||||
ContextReadWriter interface {
|
||||
ContextReader
|
||||
ContextWriter
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
// ChunkLocker implements [Chunker].
|
||||
ChunkLocker struct {
|
||||
lock sync.RWMutex
|
||||
chunkLen uint
|
||||
}
|
||||
|
||||
/*
|
||||
CtxIO is a type used to demonstrate "stateful" I/O introduced by this package.
|
||||
It implements:
|
||||
|
||||
* [Copier]
|
||||
* [Chunker]
|
||||
* [RuneWriter]
|
||||
* [ChunkReader]
|
||||
* [ChunkWriter]
|
||||
* [ContextCopier]
|
||||
* [ContextSetter]
|
||||
* [ContextReader]
|
||||
* [ContextWriter]
|
||||
* [ChunkReadWriter]
|
||||
* [ContextReadWriter]
|
||||
* [SizedCopyBufferer]
|
||||
|
||||
Unlike [XIO], it must be non-nil (see [NewCtxIO]) since it maintains state
|
||||
(though technically, one does not need to call [NewCtxIO] if they call
|
||||
[CtxIO.SetChunkLen] and [CtxIO.SetContext] before any other methods).
|
||||
|
||||
[CtxIO.Read] and other Read methods writes to an internal buffer,
|
||||
and [CtxIO.Write] and other Write methods writes out from it.
|
||||
*/
|
||||
CtxIO struct {
|
||||
r io.Reader
|
||||
w io.Writer
|
||||
l ChunkLocker
|
||||
buf bytes.Buffer
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
/*
|
||||
XIO is a type used to demonstrate "stateless" I/O introduced by this package.
|
||||
It implements:
|
||||
|
||||
* [Copier]
|
||||
* [CopyBufferer]
|
||||
* [SizedCopier]
|
||||
* [SizedCopyBufferer]
|
||||
* [SizedCopyBufferInvoker]
|
||||
* [DynamicSizedCopyBufferInvoker]
|
||||
|
||||
Unlike [CtxIO], the zero-value is ready to use since it holds no state
|
||||
or configuration whatsoever.
|
||||
|
||||
A nil XIO is perfectly usable but if you want something more idiomatic,
|
||||
see [NewXIO].
|
||||
*/
|
||||
XIO struct{}
|
||||
)
|
||||
24
logging/TODO
24
logging/TODO
@@ -1,11 +1,27 @@
|
||||
- logging probably needs mutexes
|
||||
|
||||
- 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.
|
||||
|
||||
- Support simultaneous writing to multiple Loggers.
|
||||
- StdLogger2; where stdout and stderr are both logged to depending on severity level.
|
||||
- make configurable via OR bitmask
|
||||
|
||||
- Suport remote loggers? (eventlog, syslog, systemd)
|
||||
- Suport remote loggers? (eventlog, syslog, journald)
|
||||
|
||||
- JSON logger? YAML logger? XML logger?
|
||||
|
||||
- DOCS.
|
||||
|
||||
- Unit/Integration tests.
|
||||
-- Done, but flesh out.
|
||||
|
||||
@@ -12,3 +12,16 @@ 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
|
||||
|
||||
9
logging/consts_darwin.go
Normal file
9
logging/consts_darwin.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package logging
|
||||
|
||||
var (
|
||||
// defLogPaths indicates default log paths.
|
||||
defLogPaths = []string{
|
||||
"/var/log/golang/program.log",
|
||||
"~/Library/Logs/Golang/program.log",
|
||||
}
|
||||
)
|
||||
@@ -1,32 +1,5 @@
|
||||
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{
|
||||
|
||||
34
logging/consts_nix.go
Normal file
34
logging/consts_nix.go
Normal file
@@ -0,0 +1,34 @@
|
||||
//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
|
||||
)
|
||||
39
logging/consts_test.go
Normal file
39
logging/consts_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`log`
|
||||
)
|
||||
|
||||
/*
|
||||
The following are strings written to the Logger in the various tests.
|
||||
The %v is populated with the name of the type of Logger.
|
||||
*/
|
||||
const (
|
||||
testAlert string = "This is a test ALERT-priority log message for logger %v."
|
||||
testCrit string = "This is a test CRITICAL-priority (CRIT) log message for logger %v."
|
||||
testDebug string = "This is a test DEBUG-priority log message for logger %v."
|
||||
testEmerg string = "This is a test EMERGENCY-priority (EMERG) log message for logger %v."
|
||||
testErr string = "This is a test ERROR-priority (ERR) log message for logger %v."
|
||||
testInfo string = "This is a test INFO-priority log message for logger %v."
|
||||
testNotice string = "This is a test NOTICE-priority log message for logger %v."
|
||||
testWarning string = "This is a test WARNING-priority log message for logger %v."
|
||||
)
|
||||
|
||||
// Prefixes to use for tests.
|
||||
const (
|
||||
// TestLogPrefix is used as the initial prefix.
|
||||
TestLogPrefix string = "LOGGING_TESTRUN"
|
||||
// TestLogAltPrefix is used as the alternative prefix to Logger.SetPrefix.
|
||||
TestLogAltPrefix string = "LOGGING_TESTRUN_ALT"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvVarKeepLog is the env var key/var name to use to suppress removal of FileLogger.Path after tests complete.
|
||||
EnvVarKeepLog string = "LOGGING_KEEP_TEMPLOG"
|
||||
)
|
||||
|
||||
const (
|
||||
// logFlags are used to set the log flags for StdLogger (and FileLogger.StdLogger).
|
||||
// logFlags int = log.Ldate | log.Lmicroseconds | log.Llongfile | log.LUTC
|
||||
logFlags int = log.Ldate | log.Lmicroseconds | log.Lshortfile | log.LUTC
|
||||
)
|
||||
@@ -3,15 +3,16 @@ package logging
|
||||
import (
|
||||
`os`
|
||||
`path/filepath`
|
||||
`regexp`
|
||||
|
||||
`r00t2.io/goutils/bitmask`
|
||||
)
|
||||
|
||||
// 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 types.MaskBit = 1 << iota
|
||||
// LogWinLogger indicates a WinLogger Logger type (Event Log).
|
||||
LogWinLogger
|
||||
LogWinLogger bitmask.MaskBit = 1 << iota
|
||||
// LogFile flags a FileLogger Logger type.
|
||||
LogFile
|
||||
// LogStdout flags a StdLogger Logger type.
|
||||
@@ -26,8 +27,21 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// ptrnSourceExists is a regex pattern to check for a registry entry (Event Log entry) already existing.
|
||||
var ptrnSourceExists *regexp.Regexp = regexp.MustCompile(`registry\skey\salready\sexists$`)
|
||||
/*
|
||||
ptrnSourceExists is a regex pattern to check for a registry entry (Event Log entry) already existing.
|
||||
|
||||
Deprecated: this is handled differently now.
|
||||
*/
|
||||
// var ptrnSourceExists *regexp.Regexp = regexp.MustCompile(`registry\skey\salready\sexists$`)
|
||||
|
||||
const (
|
||||
EIDMin uint32 = 1
|
||||
EIDMax uint32 = 1000
|
||||
)
|
||||
|
||||
const (
|
||||
eventLogRegistryKey string = "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application"
|
||||
)
|
||||
|
||||
// Default WinEventID, (can be) used in GetLogger and MultiLogger.AddWinLogger.
|
||||
var DefaultEventID *WinEventID = &WinEventID{
|
||||
@@ -41,7 +55,7 @@ var DefaultEventID *WinEventID = &WinEventID{
|
||||
Warning: EventWarning,
|
||||
}
|
||||
|
||||
// Default Event IDs for WinEventID.
|
||||
// Default Event IDs for WinEventID (DefaultEventID, specifically).
|
||||
const (
|
||||
EventAlert uint32 = 1 << iota
|
||||
EventCrit
|
||||
|
||||
@@ -3,18 +3,22 @@ Package logging implements and presents various loggers under a unified interfac
|
||||
|
||||
These particular loggers (logging.Logger) available are:
|
||||
|
||||
NullLogger
|
||||
StdLogger
|
||||
FileLogger
|
||||
SystemDLogger (Linux only)
|
||||
SyslogLogger (Linux only)
|
||||
SyslogLogger (Linux/macOS/other *NIX-like only)
|
||||
WinLogger (Windows only)
|
||||
|
||||
There is a sixth type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call.
|
||||
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).
|
||||
|
||||
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.
|
||||
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.
|
||||
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".
|
||||
|
||||
Alert(s string, v ...interface{}) (err error)
|
||||
@@ -35,13 +39,26 @@ Note that in the case of a MultiLogger, err (if not nil) will be a (r00t2.io/gou
|
||||
|
||||
logging.Logger types also have the following methods:
|
||||
|
||||
DoDebug(d bool)
|
||||
SetPrefix(p string)
|
||||
GetPrefix() (p string)
|
||||
Setup()
|
||||
Shutdown()
|
||||
DoDebug(d bool) (err error)
|
||||
GetDebug() (d bool)
|
||||
SetPrefix(p string) (err error)
|
||||
GetPrefix() (p string, err error)
|
||||
Setup() (err error)
|
||||
Shutdown() (err error)
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`errors`
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -12,6 +12,8 @@ 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")
|
||||
)
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`fmt`
|
||||
"errors"
|
||||
)
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
19
logging/errs_nix.go
Normal file
19
logging/errs_nix.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//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))
|
||||
)
|
||||
15
logging/errs_windows.go
Normal file
15
logging/errs_windows.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`fmt`
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrBadBinPath is returned if installing a binary-registered Event Log source instead of using EventCreate.exe.
|
||||
ErrBadBinPath error = errors.New("evaluated binary path does not actually exist")
|
||||
// ErrBadPerms is returned if an access denied error is received when attempting to register, write to, close, etc. a source without proper perms.
|
||||
ErrBadPerms error = errors.New("access denied when attempting to register Event Log source")
|
||||
// ErrBadEid is returned if an event ID is within an invalid range.
|
||||
ErrBadEid error = errors.New(fmt.Sprintf("event IDs must be between %v and %v inclusive", EIDMin, EIDMax))
|
||||
)
|
||||
@@ -1,18 +1,45 @@
|
||||
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) {
|
||||
|
||||
var f *os.File
|
||||
|
||||
// Example #2, https://golang.org/pkg/os/#OpenFile
|
||||
if f, err = os.OpenFile(path, appendFlags, logPerm); err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if err = f.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
success = true
|
||||
|
||||
|
||||
@@ -1,61 +1,91 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Setup sets up/configures a FileLogger and prepares it for use.
|
||||
func (l *FileLogger) Setup() {
|
||||
func (l *FileLogger) Setup() (err error) {
|
||||
|
||||
var err error
|
||||
var multi io.Writer
|
||||
|
||||
l.Logger = log.Default()
|
||||
l.Logger.SetPrefix(l.Prefix + " ")
|
||||
// This uses a shared handle across the import. We don't want that.
|
||||
// l.Logger = log.Default()
|
||||
if l.Prefix != "" {
|
||||
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
|
||||
// l.Logger.SetPrefix(l.Prefix)
|
||||
}
|
||||
if l.writer, err = os.OpenFile(l.Path, appendFlags, logPerm); err != nil {
|
||||
log.Panicf("could not open log file \"%v\" for writing: %v\n", l.Path, err)
|
||||
return
|
||||
}
|
||||
// https://stackoverflow.com/a/36719588/733214
|
||||
if l.EnableStdOut {
|
||||
multi = io.MultiWriter(os.Stdout, l.writer)
|
||||
} else {
|
||||
multi = l.writer
|
||||
}
|
||||
l.Logger.SetOutput(multi)
|
||||
|
||||
l.Logger = log.New(l.writer, l.Prefix, l.LogFlags)
|
||||
// l.Logger.SetOutput(multi)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Shutdown cleanly shuts down a FileLogger.
|
||||
func (l *FileLogger) Shutdown() {
|
||||
|
||||
var err error
|
||||
func (l *FileLogger) Shutdown() (err error) {
|
||||
|
||||
if err = l.writer.Close(); err != nil {
|
||||
log.Panicf("could not close log file \"%v\": %v\n", l.Path, err)
|
||||
if !errors.Is(err, fs.ErrClosed) {
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// GetPrefix returns the prefix used by this FileLogger.
|
||||
func (l *FileLogger) GetPrefix() string {
|
||||
return l.Prefix
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
DoDebug sets the debug state of this FileLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
GetPrefix returns the prefix used by this FileLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *FileLogger) DoDebug(d bool) {
|
||||
l.EnableDebug = d
|
||||
func (l *FileLogger) GetPrefix() (prefix string, err error) {
|
||||
|
||||
prefix = l.Prefix
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetPrefix sets the prefix for this FileLogger.
|
||||
func (l *FileLogger) SetPrefix(prefix string) {
|
||||
/*
|
||||
DoDebug sets the debug state of this FileLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *FileLogger) DoDebug(d bool) (err error) {
|
||||
|
||||
l.Prefix = prefix + " "
|
||||
l.Logger.SetPrefix(prefix + " ")
|
||||
l.EnableDebug = d
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetDebug returns the debug status of this FileLogger.
|
||||
func (l *FileLogger) GetDebug() (d bool) {
|
||||
|
||||
d = l.EnableDebug
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SetPrefix sets the prefix for this FileLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *FileLogger) SetPrefix(prefix string) (err error) {
|
||||
|
||||
l.Prefix = prefix
|
||||
if prefix != "" {
|
||||
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
|
||||
}
|
||||
l.Logger.SetPrefix(l.Prefix)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Alert writes an ALERT-level message to this FileLogger.
|
||||
@@ -190,6 +220,22 @@ 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) {
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
`os`
|
||||
`path`
|
||||
|
||||
sysd `github.com/coreos/go-systemd/journal`
|
||||
sysd `github.com/coreos/go-systemd/v22/journal`
|
||||
`r00t2.io/goutils/bitmask`
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
@@ -24,6 +24,9 @@ var (
|
||||
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.
|
||||
@@ -32,10 +35,11 @@ var (
|
||||
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, logPaths ...string) (logger Logger, err error) {
|
||||
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 sysd.Enabled() {
|
||||
@@ -109,6 +113,7 @@ func GetLogger(enableDebug bool, prefix string, logPaths ...string) (logger Logg
|
||||
StdLogger: StdLogger{
|
||||
Prefix: logPrefix,
|
||||
EnableDebug: enableDebug,
|
||||
LogFlags: logConfigFlags,
|
||||
},
|
||||
Path: logPath,
|
||||
}
|
||||
@@ -116,17 +121,26 @@ func GetLogger(enableDebug bool, prefix string, logPaths ...string) (logger Logg
|
||||
logger = &StdLogger{
|
||||
Prefix: logPrefix,
|
||||
EnableDebug: enableDebug,
|
||||
LogFlags: logConfigFlags,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Setup()
|
||||
if prefix != "\x00" {
|
||||
logger.SetPrefix(prefix)
|
||||
if err = logger.SetPrefix(prefix); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = logger.Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("logger initialized of type %T with prefix %v", logger, logger.GetPrefix())
|
||||
if currentPrefix, err = logger.GetPrefix(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("logger initialized of type %T with prefix %v", logger, currentPrefix)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
270
logging/funcs_linux_test.go
Normal file
270
logging/funcs_linux_test.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`os`
|
||||
`testing`
|
||||
)
|
||||
|
||||
/*
|
||||
TestSysDLogger tests functionality for SystemDLogger.
|
||||
*/
|
||||
func TestSysDLogger(t *testing.T) {
|
||||
|
||||
var l *SystemDLogger
|
||||
var ltype string = "SystemDLogger"
|
||||
var prefix string
|
||||
var err error
|
||||
|
||||
l = &SystemDLogger{
|
||||
EnableDebug: true,
|
||||
Prefix: TestLogPrefix,
|
||||
}
|
||||
|
||||
if err = l.Setup(); err != nil {
|
||||
t.Fatalf("error when running Setup: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix != TestLogPrefix {
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else if l.EnableDebug {
|
||||
t.Fatalf("did not properly set Debug filter state")
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
|
||||
/*
|
||||
TestSyslogLogger tests functionality for SyslogLogger.
|
||||
*/
|
||||
func TestSyslogLogger(t *testing.T) {
|
||||
|
||||
var l *SyslogLogger
|
||||
var ltype string = "SyslogLogger"
|
||||
var prefix string
|
||||
var err error
|
||||
|
||||
l = &SyslogLogger{
|
||||
EnableDebug: true,
|
||||
Prefix: TestLogPrefix,
|
||||
}
|
||||
|
||||
if err = l.Setup(); err != nil {
|
||||
t.Fatalf("error when running Setup: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix != TestLogPrefix {
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else if l.EnableDebug {
|
||||
t.Fatalf("did not properly set Debug filter state")
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
|
||||
// TestDefaultLogger tests GetLogger.
|
||||
func TestDefaultLogger(t *testing.T) {
|
||||
|
||||
var l Logger
|
||||
var tempfile *os.File
|
||||
var tempfilePath string
|
||||
var keepLog bool
|
||||
var ltype string
|
||||
var prefix string
|
||||
var testPrefix string
|
||||
var err error
|
||||
|
||||
if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
|
||||
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
tempfilePath = tempfile.Name()
|
||||
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
|
||||
if err = tempfile.Close(); err != nil {
|
||||
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
|
||||
if l, err = GetLogger(true, TestLogPrefix, logFlags, tempfilePath); err != nil {
|
||||
t.Fatalf("error when spawning default Linux logger via GetLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
ltype = fmt.Sprintf("%T", l)
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if ltype == "StdLogger" || ltype == "FileLogger" { // StdLogger (and thus FileLogger) adds a space at the end.
|
||||
testPrefix = TestLogPrefix + " "
|
||||
} else {
|
||||
testPrefix = TestLogPrefix
|
||||
}
|
||||
|
||||
if prefix != testPrefix {
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
_, keepLog = os.LookupEnv(EnvVarKeepLog)
|
||||
|
||||
if !keepLog {
|
||||
if err = os.Remove(tempfilePath); err != nil {
|
||||
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
25
logging/funcs_logprio.go
Normal file
25
logging/funcs_logprio.go
Normal file
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
211
logging/funcs_logwriter.go
Normal file
211
logging/funcs_logwriter.go
Normal file
@@ -0,0 +1,211 @@
|
||||
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
|
||||
}
|
||||
@@ -1,93 +1,161 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`sync`
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
`r00t2.io/goutils/multierr`
|
||||
"r00t2.io/goutils/multierr"
|
||||
)
|
||||
|
||||
// Setup sets up/configures a MultiLogger (and all its MultiLogger.Loggers) and prepares it for use.
|
||||
func (m *MultiLogger) Setup() {
|
||||
func (m *MultiLogger) Setup() (err error) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var errs *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
go func(logger Logger, lName string) {
|
||||
var err2 error
|
||||
defer wg.Done()
|
||||
l.Setup()
|
||||
}()
|
||||
if err2 = logger.Setup(); err2 != nil {
|
||||
errs.AddError(errors.New(fmt.Sprintf("error on Setup for logger %v; follows (may be out of order):", lName)))
|
||||
errs.AddError(err2)
|
||||
err2 = nil
|
||||
}
|
||||
}(l, logName)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errs.Count() > 0 {
|
||||
err = errs
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Shutdown cleanly shuts down a MultiLogger (and all its MultiLogger.Loggers).
|
||||
func (m *MultiLogger) Shutdown() {
|
||||
func (m *MultiLogger) Shutdown() (err error) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var errs *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
go func(logger Logger, lName string) {
|
||||
var err2 error
|
||||
defer wg.Done()
|
||||
l.Shutdown()
|
||||
}()
|
||||
if err2 = logger.Shutdown(); err2 != nil {
|
||||
errs.AddError(errors.New(fmt.Sprintf("error on Shutdown for logger %v; follows (may be out of order):", lName)))
|
||||
errs.AddError(err2)
|
||||
err2 = nil
|
||||
}
|
||||
}(l, logName)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errs.Count() > 0 {
|
||||
err = errs
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetPrefix returns the prefix used by this MultiLogger (and all its MultiLogger.Loggers).
|
||||
func (m *MultiLogger) GetPrefix() string {
|
||||
/*
|
||||
GetPrefix returns the prefix used by this MultiLogger (and all its MultiLogger.Loggers).
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (m *MultiLogger) GetPrefix() (prefix string, err error) {
|
||||
|
||||
return m.Prefix
|
||||
prefix = m.Prefix
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
DoDebug sets the debug state of this MultiLogger (and all its MultiLogger.Loggers).
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
DoDebug sets the debug state of this MultiLogger (and all its MultiLogger.Loggers).
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
|
||||
If you had a logger-specific EnableDebug set, you will need to re-set it to your desired state after running this method.
|
||||
If you had a logger-specific EnableDebug set, you will need to re-set it to your desired state after running this method.
|
||||
*/
|
||||
func (m *MultiLogger) DoDebug(d bool) {
|
||||
func (m *MultiLogger) DoDebug(d bool) (err error) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var errs *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
m.EnableDebug = d
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
go func(logger Logger, lName string) {
|
||||
var err2 error
|
||||
defer wg.Done()
|
||||
l.DoDebug(d)
|
||||
}()
|
||||
if err2 = l.DoDebug(d); err2 != nil {
|
||||
errs.AddError(errors.New(fmt.Sprintf("error on DoDebug for logger %v; follows (may be out of order):", lName)))
|
||||
errs.AddError(err2)
|
||||
err2 = nil
|
||||
}
|
||||
}(l, logName)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errs.Count() > 0 {
|
||||
err = errs
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetDebug returns the debug status of this MultiLogger.
|
||||
func (m *MultiLogger) GetDebug() (d bool) {
|
||||
|
||||
d = m.EnableDebug
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SetPrefix sets the prefix for this MultiLogger (and all its MultiLogger.Loggers).
|
||||
SetPrefix sets the prefix for this MultiLogger (and all its MultiLogger.Loggers).
|
||||
|
||||
If you had a logger-specific Prefix set, you will need to re-set it to your desired prefix after running this method.
|
||||
If you had a logger-specific Prefix set, you will need to re-set it to your desired prefix after running this method.
|
||||
*/
|
||||
func (m *MultiLogger) SetPrefix(prefix string) {
|
||||
func (m *MultiLogger) SetPrefix(prefix string) (err error) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var errs *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
m.Prefix = prefix
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
go func(logger Logger, lName string) {
|
||||
var err2 error
|
||||
defer wg.Done()
|
||||
l.SetPrefix(prefix)
|
||||
}()
|
||||
if err2 = l.SetPrefix(prefix); err != nil {
|
||||
errs.AddError(errors.New(fmt.Sprintf("error on SetPrefix for logger %v; follows (may be out of order):", lName)))
|
||||
errs.AddError(err2)
|
||||
err2 = nil
|
||||
}
|
||||
}(l, logName)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errs.Count() > 0 {
|
||||
err = errs
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Alert writes an ALERT-level message to this MultiLogger (and all its MultiLogger.Loggers).
|
||||
@@ -96,20 +164,16 @@ func (m *MultiLogger) Alert(s string, v ...interface{}) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
var e *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func(logObj Logger, msg string, rplc ...interface{}) {
|
||||
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
|
||||
defer wg.Done()
|
||||
if rplc != nil {
|
||||
err = logObj.Alert(msg, rplc...)
|
||||
} else {
|
||||
err = logObj.Alert(msg)
|
||||
}
|
||||
if err != nil {
|
||||
if err = logObj.Alert(msg, rplc...); err != nil {
|
||||
e.AddError(errors.New(fmt.Sprintf("error on Alert for logger %v; follows (may be out of order):", lName)))
|
||||
e.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}(l, s, v)
|
||||
}(l, s, logName, v...)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -127,20 +191,16 @@ func (m *MultiLogger) Crit(s string, v ...interface{}) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
var e *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func(logObj Logger, msg string, rplc ...interface{}) {
|
||||
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
|
||||
defer wg.Done()
|
||||
if rplc != nil {
|
||||
err = logObj.Crit(msg, rplc...)
|
||||
} else {
|
||||
err = logObj.Crit(msg)
|
||||
}
|
||||
if err != nil {
|
||||
if err = logObj.Crit(msg, rplc...); err != nil {
|
||||
e.AddError(errors.New(fmt.Sprintf("error on Crit for logger %v; follows (may be out of order):", lName)))
|
||||
e.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}(l, s, v)
|
||||
}(l, s, logName, v...)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -158,20 +218,16 @@ func (m *MultiLogger) Debug(s string, v ...interface{}) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
var e *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func(logObj Logger, msg string, rplc ...interface{}) {
|
||||
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
|
||||
defer wg.Done()
|
||||
if rplc != nil {
|
||||
err = logObj.Debug(msg, rplc...)
|
||||
} else {
|
||||
err = logObj.Debug(msg)
|
||||
}
|
||||
if err != nil {
|
||||
if err = logObj.Debug(msg, rplc...); err != nil {
|
||||
e.AddError(errors.New(fmt.Sprintf("error on Debug for logger %v; follows (may be out of order):", lName)))
|
||||
e.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}(l, s, v)
|
||||
}(l, s, logName, v...)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -188,20 +244,16 @@ func (m *MultiLogger) Emerg(s string, v ...interface{}) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
var e *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func(logObj Logger, msg string, rplc ...interface{}) {
|
||||
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
|
||||
defer wg.Done()
|
||||
if rplc != nil {
|
||||
err = logObj.Emerg(msg, rplc...)
|
||||
} else {
|
||||
err = logObj.Emerg(msg)
|
||||
}
|
||||
if err != nil {
|
||||
if err = logObj.Emerg(msg, rplc...); err != nil {
|
||||
e.AddError(errors.New(fmt.Sprintf("error on Emerg for logger %v; follows (may be out of order):", lName)))
|
||||
e.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}(l, s, v)
|
||||
}(l, s, logName, v...)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -218,20 +270,16 @@ func (m *MultiLogger) Err(s string, v ...interface{}) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
var e *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func(logObj Logger, msg string, rplc ...interface{}) {
|
||||
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
|
||||
defer wg.Done()
|
||||
if rplc != nil {
|
||||
err = logObj.Err(msg, rplc...)
|
||||
} else {
|
||||
err = logObj.Err(msg)
|
||||
}
|
||||
if err != nil {
|
||||
if err = logObj.Err(msg, rplc...); err != nil {
|
||||
e.AddError(errors.New(fmt.Sprintf("error on Err for logger %v; follows (may be out of order):", lName)))
|
||||
e.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}(l, s, v)
|
||||
}(l, s, logName, v...)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -249,20 +297,16 @@ func (m *MultiLogger) Info(s string, v ...interface{}) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
var e *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func(logObj Logger, msg string, rplc ...interface{}) {
|
||||
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
|
||||
defer wg.Done()
|
||||
if rplc != nil {
|
||||
err = logObj.Info(msg, rplc...)
|
||||
} else {
|
||||
err = logObj.Info(msg)
|
||||
}
|
||||
if err != nil {
|
||||
if err = logObj.Info(msg, rplc...); err != nil {
|
||||
e.AddError(errors.New(fmt.Sprintf("error on Info for logger %v; follows (may be out of order):", lName)))
|
||||
e.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}(l, s, v)
|
||||
}(l, s, logName, v...)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -280,20 +324,16 @@ func (m *MultiLogger) Notice(s string, v ...interface{}) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
var e *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func(logObj Logger, msg string, rplc ...interface{}) {
|
||||
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
|
||||
defer wg.Done()
|
||||
if rplc != nil {
|
||||
err = logObj.Notice(msg, rplc...)
|
||||
} else {
|
||||
err = logObj.Notice(msg)
|
||||
}
|
||||
if err != nil {
|
||||
if err = logObj.Notice(msg, rplc...); err != nil {
|
||||
e.AddError(errors.New(fmt.Sprintf("error on Notice for logger %v; follows (may be out of order):", lName)))
|
||||
e.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}(l, s, v)
|
||||
}(l, s, logName, v...)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -311,20 +351,16 @@ func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
var e *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, l := range m.Loggers {
|
||||
for logName, l := range m.Loggers {
|
||||
wg.Add(1)
|
||||
go func(logObj Logger, msg string, rplc ...interface{}) {
|
||||
go func(logObj Logger, msg, lName string, rplc ...interface{}) {
|
||||
defer wg.Done()
|
||||
if rplc != nil {
|
||||
err = logObj.Warning(msg, rplc...)
|
||||
} else {
|
||||
err = logObj.Warning(msg)
|
||||
}
|
||||
if err != nil {
|
||||
if err = logObj.Warning(msg, rplc...); err != nil {
|
||||
e.AddError(errors.New(fmt.Sprintf("error on Warning for logger %v; follows (may be out of order):", lName)))
|
||||
e.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}(l, s, v)
|
||||
}(l, s, logName, v...)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -335,3 +371,19 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`path`
|
||||
"path"
|
||||
|
||||
`github.com/google/uuid`
|
||||
`r00t2.io/sysutils/paths`
|
||||
"github.com/google/uuid"
|
||||
"r00t2.io/sysutils/paths"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -21,7 +21,7 @@ func GetMultiLogger(enableDebug bool, prefix string) (m *MultiLogger) {
|
||||
|
||||
m = &MultiLogger{
|
||||
EnableDebug: enableDebug,
|
||||
Prefix: "",
|
||||
Prefix: logPrefix,
|
||||
Loggers: make(map[string]Logger),
|
||||
}
|
||||
if prefix != "\x00" {
|
||||
@@ -36,10 +36,19 @@ func GetMultiLogger(enableDebug bool, prefix string) (m *MultiLogger) {
|
||||
|
||||
identifier is a string to use to identify the added StdLogger in MultiLogger.Loggers.
|
||||
If empty, one will be automatically generated.
|
||||
|
||||
enableStdOut indicates that messages should be logged to STDOUT;
|
||||
it is *strongly encouraged* to set at least one of enableStdOut or enableStdErr to true.
|
||||
|
||||
enableStdErr indicates that messages should be logged to STDERR;
|
||||
it is *strongly encouraged* to set at least one of enableStdErr or enableStdOut to true.
|
||||
|
||||
See GetLogger's logConfigFlags argument and StdLogger.LogFlags for details on logFlags.
|
||||
*/
|
||||
func (m *MultiLogger) AddStdLogger(identifier string) (err error) {
|
||||
func (m *MultiLogger) AddStdLogger(identifier string, enableStdOut, enableStdErr bool, logFlags int) (err error) {
|
||||
|
||||
var exists bool
|
||||
var prefix string
|
||||
|
||||
if identifier == "" {
|
||||
identifier = uuid.New().String()
|
||||
@@ -54,10 +63,19 @@ func (m *MultiLogger) AddStdLogger(identifier string) (err error) {
|
||||
Logger: nil,
|
||||
EnableDebug: m.EnableDebug,
|
||||
Prefix: m.Prefix,
|
||||
LogFlags: logFlags,
|
||||
EnableStdOut: enableStdOut,
|
||||
EnableStdErr: enableStdErr,
|
||||
}
|
||||
if err = m.Loggers[identifier].Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
m.Loggers[identifier].Setup()
|
||||
|
||||
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix())
|
||||
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
|
||||
}
|
||||
@@ -70,11 +88,12 @@ func (m *MultiLogger) AddStdLogger(identifier string) (err error) {
|
||||
|
||||
logfilePath is a string for the path to the desired logfile.
|
||||
*/
|
||||
func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error) {
|
||||
func (m *MultiLogger) AddFileLogger(identifier string, logFlags int, logfilePath string) (err error) {
|
||||
|
||||
var exists bool
|
||||
var success bool
|
||||
var dirPath string
|
||||
var prefix string
|
||||
|
||||
if identifier == "" {
|
||||
identifier = uuid.New().String()
|
||||
@@ -85,7 +104,9 @@ func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error)
|
||||
return
|
||||
}
|
||||
|
||||
if exists, _ = paths.RealPathExists(&logfilePath); !exists {
|
||||
if exists, err = paths.RealPathExists(&logfilePath); err != nil {
|
||||
return
|
||||
} else if !exists {
|
||||
if success, err = testOpen(logfilePath); err != nil {
|
||||
return
|
||||
} else if !success {
|
||||
@@ -107,12 +128,53 @@ func (m *MultiLogger) AddFileLogger(identifier, logfilePath string) (err error)
|
||||
Logger: nil,
|
||||
EnableDebug: m.EnableDebug,
|
||||
Prefix: m.Prefix,
|
||||
LogFlags: logFlags,
|
||||
},
|
||||
Path: logfilePath,
|
||||
}
|
||||
m.Loggers[identifier].Setup()
|
||||
if err = m.Loggers[identifier].Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix())
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
AddNullLogger adds a NullLogger to a MultiLogger.
|
||||
|
||||
identifier is a string to use to identify the added NullLogger in MultiLogger.Loggers.
|
||||
If empty, one will be automatically generated.
|
||||
*/
|
||||
func (m *MultiLogger) AddNullLogger(identifier string) (err error) {
|
||||
|
||||
var exists bool
|
||||
var prefix string
|
||||
|
||||
if identifier == "" {
|
||||
identifier = uuid.New().String()
|
||||
}
|
||||
|
||||
if _, exists = m.Loggers[identifier]; exists {
|
||||
err = ErrExistingLogger
|
||||
return
|
||||
}
|
||||
|
||||
m.Loggers[identifier] = &NullLogger{}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`os`
|
||||
|
||||
sysd `github.com/coreos/go-systemd/journal`
|
||||
`github.com/google/uuid`
|
||||
`r00t2.io/sysutils/paths`
|
||||
sysd "github.com/coreos/go-systemd/v22/journal"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
/*
|
||||
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.
|
||||
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.
|
||||
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, logPaths ...string) (err error) {
|
||||
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
|
||||
|
||||
var l Logger
|
||||
var exists bool
|
||||
@@ -28,7 +27,7 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, logPaths ...string) (e
|
||||
return
|
||||
}
|
||||
|
||||
if l, err = GetLogger(m.EnableDebug, m.Prefix, logPaths...); err != nil {
|
||||
if l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,14 +37,15 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, logPaths ...string) (e
|
||||
}
|
||||
|
||||
/*
|
||||
AddSysdLogger adds a SystemDLogger to a MultiLogger.
|
||||
AddSysdLogger adds a SystemDLogger to a MultiLogger.
|
||||
|
||||
identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers.
|
||||
If empty, one will be automatically generated.
|
||||
identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers.
|
||||
If empty, one will be automatically generated.
|
||||
*/
|
||||
func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
|
||||
|
||||
var exists bool
|
||||
var prefix string
|
||||
|
||||
if identifier == "" {
|
||||
identifier = uuid.New().String()
|
||||
@@ -65,54 +65,15 @@ func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
|
||||
EnableDebug: m.EnableDebug,
|
||||
Prefix: m.Prefix,
|
||||
}
|
||||
m.Loggers[identifier].Setup()
|
||||
if err = m.Loggers[identifier].Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix())
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
}
|
||||
m.Loggers[identifier].Setup()
|
||||
|
||||
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix())
|
||||
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
|
||||
}
|
||||
|
||||
63
logging/funcs_multilogger_mgr_nix.go
Normal file
63
logging/funcs_multilogger_mgr_nix.go
Normal file
@@ -0,0 +1,63 @@
|
||||
//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
|
||||
}
|
||||
41
logging/funcs_multilogger_mgr_oldnix.go
Normal file
41
logging/funcs_multilogger_mgr_oldnix.go
Normal file
@@ -0,0 +1,41 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`github.com/google/uuid`
|
||||
)
|
||||
|
||||
/*
|
||||
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.
|
||||
|
||||
@@ -12,10 +16,12 @@ package logging
|
||||
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).
|
||||
|
||||
See the documentation for GetLogger for details on other arguments.
|
||||
|
||||
Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
||||
Currently this will almost always return a WinLogger.
|
||||
*/
|
||||
func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID) (err error) {
|
||||
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
|
||||
|
||||
var l Logger
|
||||
var exists bool
|
||||
@@ -29,7 +35,12 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID)
|
||||
return
|
||||
}
|
||||
|
||||
if l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logPaths...); err != nil {
|
||||
if logPaths != nil {
|
||||
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...)
|
||||
} else {
|
||||
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,9 +65,10 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID)
|
||||
|
||||
See GetLogger for details.
|
||||
*/
|
||||
func (m *MultiLogger) AddWinLogger(identifier, source string, eventIDs *WinEventID) (err error) {
|
||||
func (m *MultiLogger) AddWinLogger(identifier string, eventIDs *WinEventID) (err error) {
|
||||
|
||||
var exists bool
|
||||
var prefix string
|
||||
|
||||
if identifier == "" {
|
||||
identifier = uuid.New().String()
|
||||
@@ -72,13 +84,19 @@ func (m *MultiLogger) AddWinLogger(identifier, source string, eventIDs *WinEvent
|
||||
}
|
||||
|
||||
m.Loggers[identifier] = &WinLogger{
|
||||
Prefix: source,
|
||||
Prefix: m.Prefix,
|
||||
EnableDebug: m.EnableDebug,
|
||||
eids: eventIDs,
|
||||
EIDs: eventIDs,
|
||||
}
|
||||
if err = m.Loggers[identifier].Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
m.Loggers[identifier].Setup()
|
||||
|
||||
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], m.Loggers[identifier].GetPrefix())
|
||||
if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.Loggers[identifier].Info("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
94
logging/funcs_nulllogger.go
Normal file
94
logging/funcs_nulllogger.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// Setup does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Setup() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// DoDebug does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) DoDebug(d bool) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetDebug returns the debug status of this NullLogger. It will always return true. 🙃
|
||||
func (n *NullLogger) GetDebug() (d bool) {
|
||||
|
||||
d = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetPrefix does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) SetPrefix(p string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetPrefix does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) GetPrefix() (p string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Shutdown does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Shutdown() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Alert does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Alert(s string, v ...interface{}) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Crit does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Crit(s string, v ...interface{}) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Debug does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Debug(s string, v ...interface{}) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Emerg does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Emerg(s string, v ...interface{}) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Err does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Err(s string, v ...interface{}) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Info does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Info(s string, v ...interface{}) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Notice does nothing at all; it's here for interface compat. 🙃
|
||||
func (l *NullLogger) Notice(s string, v ...interface{}) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Warning does nothing at all; it's here for interface compat. 🙃
|
||||
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
|
||||
}
|
||||
58
logging/funcs_nullwriter.go
Normal file
58
logging/funcs_nullwriter.go
Normal file
@@ -0,0 +1,58 @@
|
||||
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
|
||||
}
|
||||
138
logging/funcs_oldnix.go
Normal file
138
logging/funcs_oldnix.go
Normal file
@@ -0,0 +1,138 @@
|
||||
//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
|
||||
}
|
||||
@@ -2,26 +2,66 @@ package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Setup sets up/configures a StdLogger and prepares it for use.
|
||||
func (l *StdLogger) Setup() {
|
||||
/*
|
||||
Setup sets up/configures a StdLogger and prepares it for use.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *StdLogger) Setup() (err error) {
|
||||
|
||||
l.Logger = log.Default()
|
||||
l.Logger.SetPrefix(l.Prefix + " ")
|
||||
var multi io.Writer
|
||||
|
||||
// This uses a shared handle across the import. We don't want that.
|
||||
// l.Logger = log.Default()
|
||||
if l.Prefix != "" {
|
||||
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
|
||||
// l.Logger.SetPrefix(l.Prefix)
|
||||
}
|
||||
// (stdlib).log.std is returned by log.Default(), which uses os.Stderr but we have flags for that.
|
||||
// https://stackoverflow.com/a/36719588/733214
|
||||
switch {
|
||||
case l.EnableStdErr && l.EnableStdOut:
|
||||
multi = io.MultiWriter(os.Stdout, os.Stderr)
|
||||
case l.EnableStdErr:
|
||||
multi = os.Stderr
|
||||
case l.EnableStdOut:
|
||||
multi = os.Stdout
|
||||
default:
|
||||
multi = nil
|
||||
}
|
||||
if multi != nil {
|
||||
l.Logger = log.New(multi, l.Prefix, l.LogFlags)
|
||||
} else {
|
||||
// This honestly should throw an error.
|
||||
l.Logger = &log.Logger{}
|
||||
l.Logger.SetPrefix(l.Prefix)
|
||||
l.Logger.SetFlags(l.LogFlags)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Shutdown cleanly shuts down a StdLogger.
|
||||
func (l *StdLogger) Shutdown() {
|
||||
/*
|
||||
Shutdown cleanly shuts down a StdLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *StdLogger) Shutdown() (err error) {
|
||||
|
||||
// NOOP
|
||||
_ = ""
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetPrefix returns the prefix used by this StdLogger.
|
||||
func (l *StdLogger) GetPrefix() (prefix string) {
|
||||
/*
|
||||
GetPrefix returns the prefix used by this StdLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *StdLogger) GetPrefix() (prefix string, err error) {
|
||||
|
||||
prefix = l.Prefix
|
||||
|
||||
@@ -29,17 +69,38 @@ func (l *StdLogger) GetPrefix() (prefix string) {
|
||||
}
|
||||
|
||||
/*
|
||||
DoDebug sets the debug state of this StdLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
DoDebug sets the debug state of this StdLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *StdLogger) DoDebug(d bool) {
|
||||
func (l *StdLogger) DoDebug(d bool) (err error) {
|
||||
|
||||
l.EnableDebug = d
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetPrefix sets the prefix for this StdLogger.
|
||||
func (l *StdLogger) SetPrefix(prefix string) {
|
||||
l.Prefix = prefix + " "
|
||||
l.Logger.SetPrefix(prefix + " ")
|
||||
// GetDebug returns the debug status of this StdLogger.
|
||||
func (l *StdLogger) GetDebug() (d bool) {
|
||||
|
||||
d = l.EnableDebug
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SetPrefix sets the prefix for this StdLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *StdLogger) SetPrefix(prefix string) (err error) {
|
||||
|
||||
l.Prefix = prefix
|
||||
if prefix != "" {
|
||||
l.Prefix = strings.TrimRight(l.Prefix, " ") + " "
|
||||
}
|
||||
l.Logger.SetPrefix(l.Prefix)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Alert writes an ALERT-level message to this StdLogger.
|
||||
@@ -174,6 +235,22 @@ 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) {
|
||||
|
||||
|
||||
@@ -4,27 +4,36 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/coreos/go-systemd/journal"
|
||||
"github.com/coreos/go-systemd/v22/journal"
|
||||
)
|
||||
|
||||
// Setup sets up/configures a SystemDLogger and prepares it for use.
|
||||
func (l *SystemDLogger) Setup() {
|
||||
/*
|
||||
Setup sets up/configures a SystemDLogger and prepares it for use.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *SystemDLogger) Setup() (err error) {
|
||||
|
||||
// NOOP
|
||||
_ = ""
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Shutdown cleanly shuts down a SystemDLogger.
|
||||
func (l *SystemDLogger) Shutdown() {
|
||||
/*
|
||||
Shutdown cleanly shuts down a SystemDLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *SystemDLogger) Shutdown() (err error) {
|
||||
|
||||
// NOOP
|
||||
_ = ""
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetPrefix returns the prefix used by this SystemDLogger.
|
||||
func (l *SystemDLogger) GetPrefix() (prefix string) {
|
||||
/*
|
||||
GetPrefix returns the prefix used by this SystemDLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *SystemDLogger) GetPrefix() (prefix string, err error) {
|
||||
|
||||
prefix = l.Prefix
|
||||
|
||||
@@ -32,16 +41,34 @@ func (l *SystemDLogger) GetPrefix() (prefix string) {
|
||||
}
|
||||
|
||||
/*
|
||||
DoDebug sets the debug state of this SystemDLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
DoDebug sets the debug state of this SystemDLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *SystemDLogger) DoDebug(d bool) {
|
||||
func (l *SystemDLogger) DoDebug(d bool) (err error) {
|
||||
|
||||
l.EnableDebug = d
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetPrefix sets the prefix for this SystemDLogger.
|
||||
func (l *SystemDLogger) SetPrefix(prefix string) {
|
||||
// GetDebug returns the debug status of this SystemDLogger.
|
||||
func (l *SystemDLogger) GetDebug() (d bool) {
|
||||
|
||||
d = l.EnableDebug
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SetPrefix sets the prefix for this SystemDLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *SystemDLogger) SetPrefix(prefix string) (err error) {
|
||||
|
||||
l.Prefix = prefix
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Alert writes an ALERT-level message to this SystemDLogger.
|
||||
@@ -196,3 +223,19 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,58 +1,85 @@
|
||||
//go:build !(windows || plan9 || wasip1 || js || ios)
|
||||
// +build !windows,!plan9,!wasip1,!js,!ios
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/syslog"
|
||||
|
||||
"r00t2.io/goutils/multierr"
|
||||
)
|
||||
|
||||
// Setup sets up/configures a SyslogLogger and prepares it for use.
|
||||
func (l *SyslogLogger) Setup() {
|
||||
func (l *SyslogLogger) Setup() (err error) {
|
||||
|
||||
var err error
|
||||
var errs *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
if l.alert, err = syslog.New(syslog.LOG_ALERT|syslogFacility, l.Prefix); err != nil {
|
||||
log.Panicln("could not open log for Alert")
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
if l.crit, err = syslog.New(syslog.LOG_CRIT|syslogFacility, l.Prefix); err != nil {
|
||||
log.Panicln("could not open log for Crit")
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
if l.debug, err = syslog.New(syslog.LOG_DEBUG|syslogFacility, l.Prefix); err != nil {
|
||||
log.Panicln("could not open log for Debug")
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
if l.emerg, err = syslog.New(syslog.LOG_EMERG|syslogFacility, l.Prefix); err != nil {
|
||||
log.Panicln("could not open log for Emerg")
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
if l.err, err = syslog.New(syslog.LOG_ERR|syslogFacility, l.Prefix); err != nil {
|
||||
log.Panicln("could not open log for Err")
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
if l.info, err = syslog.New(syslog.LOG_INFO|syslogFacility, l.Prefix); err != nil {
|
||||
log.Panicln("could not open log for Info")
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
if l.notice, err = syslog.New(syslog.LOG_NOTICE|syslogFacility, l.Prefix); err != nil {
|
||||
log.Panicln("could not open log for Notice")
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
if l.warning, err = syslog.New(syslog.LOG_WARNING|syslogFacility, l.Prefix); err != nil {
|
||||
log.Panicln("could not open log for Warning")
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
|
||||
if errs.Count() > 0 {
|
||||
err = errs
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Shutdown cleanly shuts down a SyslogLogger.
|
||||
func (l *SyslogLogger) Shutdown() {
|
||||
func (l *SyslogLogger) Shutdown() (err error) {
|
||||
|
||||
var err error
|
||||
var errs *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
|
||||
for _, i := range []*syslog.Writer{l.alert, l.crit, l.debug, l.emerg, l.err, l.info, l.notice, l.warning} {
|
||||
if err = i.Close(); err != nil {
|
||||
log.Panicf("could not close log %#v\n", i)
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if errs.Count() > 0 {
|
||||
err = errs
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetPrefix returns the prefix used by this SyslogLogger.
|
||||
func (l *SyslogLogger) GetPrefix() (prefix string) {
|
||||
/*
|
||||
GetPrefix returns the prefix used by this SyslogLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *SyslogLogger) GetPrefix() (prefix string, err error) {
|
||||
|
||||
prefix = l.Prefix
|
||||
|
||||
@@ -60,17 +87,40 @@ func (l *SyslogLogger) GetPrefix() (prefix string) {
|
||||
}
|
||||
|
||||
/*
|
||||
DoDebug sets the debug state of this SyslogLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
DoDebug sets the debug state of this SyslogLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *SyslogLogger) DoDebug(d bool) {
|
||||
func (l *SyslogLogger) DoDebug(d bool) (err error) {
|
||||
|
||||
l.EnableDebug = d
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetDebug returns the debug status of this SyslogLogger.
|
||||
func (l *SyslogLogger) GetDebug() (d bool) {
|
||||
|
||||
d = l.EnableDebug
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetPrefix sets the prefix for this SyslogLogger.
|
||||
func (l *SyslogLogger) SetPrefix(prefix string) {
|
||||
func (l *SyslogLogger) SetPrefix(prefix string) (err error) {
|
||||
|
||||
l.Prefix = prefix
|
||||
l.Setup()
|
||||
|
||||
// We need to close the current loggers first.
|
||||
if err = l.Shutdown(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = l.Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Alert writes an ALERT-level message to this SyslogLogger.
|
||||
@@ -219,3 +269,19 @@ 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
|
||||
}
|
||||
196
logging/funcs_test.go
Normal file
196
logging/funcs_test.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`os`
|
||||
`testing`
|
||||
)
|
||||
|
||||
/*
|
||||
TestStdLogger tests functionality for StdLogger.
|
||||
*/
|
||||
func TestStdLogger(t *testing.T) {
|
||||
|
||||
var l *StdLogger
|
||||
var ltype string = "StdLogger"
|
||||
var prefix string
|
||||
var err error
|
||||
|
||||
l = &StdLogger{
|
||||
EnableDebug: true,
|
||||
Prefix: TestLogPrefix,
|
||||
LogFlags: logFlags,
|
||||
EnableStdOut: false,
|
||||
EnableStdErr: true,
|
||||
}
|
||||
|
||||
if err = l.Setup(); err != nil {
|
||||
t.Fatalf("error when running Setup: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix != (TestLogPrefix + " ") { // StdLogger adds a space at the end.
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else if l.EnableDebug {
|
||||
t.Fatalf("did not properly set Debug filter state")
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
|
||||
/*
|
||||
TestFileLogger tests functionality for FileLogger.
|
||||
If the appropriate env var is set (see the EnvVarKeepLog constant), the temporary log file that is created will not be cleaned up.
|
||||
*/
|
||||
func TestFileLogger(t *testing.T) {
|
||||
|
||||
var l *FileLogger
|
||||
var ltype string = "FileLogger"
|
||||
var prefix string
|
||||
var tempfile *os.File
|
||||
var tempfilePath string
|
||||
var keepLog bool
|
||||
var err error
|
||||
|
||||
if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
|
||||
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
tempfilePath = tempfile.Name()
|
||||
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
|
||||
if err = tempfile.Close(); err != nil {
|
||||
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
|
||||
l = &FileLogger{
|
||||
StdLogger: StdLogger{
|
||||
EnableDebug: true,
|
||||
Prefix: TestLogPrefix,
|
||||
LogFlags: logFlags,
|
||||
},
|
||||
Path: tempfilePath,
|
||||
}
|
||||
|
||||
if err = l.Setup(); err != nil {
|
||||
t.Fatalf("error when running Setup: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix != (TestLogPrefix + " ") { // StdLogger (and thus FileLogger) adds a space at the end.
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else if l.EnableDebug {
|
||||
t.Fatalf("did not properly set Debug filter state")
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
_, keepLog = os.LookupEnv(EnvVarKeepLog)
|
||||
|
||||
if !keepLog {
|
||||
if err = os.Remove(tempfilePath); err != nil {
|
||||
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
@@ -2,39 +2,78 @@ package logging
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`path`
|
||||
`strings`
|
||||
|
||||
`r00t2.io/goutils/bitmask`
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
|
||||
/*
|
||||
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),
|
||||
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.
|
||||
|
||||
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).
|
||||
`logConfigFlags` is the corresponding flag(s) OR'd for [StdLogger.LogFlags] (and/or the [StdLogger.LogFlags] for [FileLogger])
|
||||
if either is selected. See [StdLogger.LogFlags] and [stdlib log's constants] for details.
|
||||
|
||||
Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
||||
Currently this will almost always return a WinLogger.
|
||||
`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).
|
||||
|
||||
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.
|
||||
Only the first `logPaths` entry that "works" will be used, later entries will be ignored.
|
||||
Currently this will almost always return a [WinLogger].
|
||||
|
||||
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),
|
||||
use [GetMultiLogger].
|
||||
|
||||
[stdlib log's constants]: https://pkg.go.dev/log#pkg-constants
|
||||
*/
|
||||
func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, 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 logFlags bitmask.MaskBit
|
||||
var exists bool
|
||||
var success bool
|
||||
var ckLogPaths []string
|
||||
var prefix string
|
||||
|
||||
if strings.TrimSpace(source) == "" {
|
||||
err = errors.New("invalid source for Windows logging")
|
||||
@@ -82,7 +121,7 @@ func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths .
|
||||
logger = &WinLogger{
|
||||
Prefix: source,
|
||||
EnableDebug: enableDebug,
|
||||
eids: eventIDs,
|
||||
EIDs: eventIDs,
|
||||
}
|
||||
} else {
|
||||
if logFlags.HasFlag(LogFile) {
|
||||
@@ -90,6 +129,7 @@ func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths .
|
||||
StdLogger: StdLogger{
|
||||
Prefix: source,
|
||||
EnableDebug: enableDebug,
|
||||
LogFlags: logConfigFlags,
|
||||
},
|
||||
Path: logPath,
|
||||
}
|
||||
@@ -97,13 +137,25 @@ func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logPaths .
|
||||
logger = &StdLogger{
|
||||
Prefix: source,
|
||||
EnableDebug: enableDebug,
|
||||
LogFlags: logConfigFlags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Setup()
|
||||
if err = logger.Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
if source != "\x00" {
|
||||
if err = logger.SetPrefix(source); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("logger initialized of type %T with source %v", logger, logger.GetPrefix())
|
||||
if prefix, err = logger.GetPrefix(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("logger initialized of type %T with source %v", logger, prefix)
|
||||
|
||||
return
|
||||
|
||||
|
||||
205
logging/funcs_windows_test.go
Normal file
205
logging/funcs_windows_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`os`
|
||||
`testing`
|
||||
)
|
||||
|
||||
/*
|
||||
TestWinLogger tests functionality for WinLogger.
|
||||
You will probably need to run it with an Administrator shell.
|
||||
*/
|
||||
func TestWinLogger(t *testing.T) {
|
||||
|
||||
var l *WinLogger
|
||||
var ltype string = "WinLogger"
|
||||
var prefix string
|
||||
var exists bool
|
||||
var err error
|
||||
|
||||
l = &WinLogger{
|
||||
EnableDebug: true,
|
||||
Prefix: TestLogPrefix,
|
||||
RemoveOnClose: true,
|
||||
EIDs: DefaultEventID,
|
||||
}
|
||||
|
||||
if exists, err = l.Exists(); err != nil {
|
||||
t.Fatalf("error when checking for existence of registered Event Log source '%v': %v", TestLogPrefix, err.Error())
|
||||
} else {
|
||||
t.Logf("Prefix (source) '%v' exists before setup: %v", TestLogPrefix, exists)
|
||||
}
|
||||
|
||||
if err = l.Setup(); err != nil {
|
||||
t.Fatalf("error when running Setup: %v", err.Error())
|
||||
}
|
||||
|
||||
if exists, err = l.Exists(); err != nil {
|
||||
t.Fatalf("error when checking for existence of registered Event Log source '%v': %v", TestLogPrefix, err.Error())
|
||||
} else {
|
||||
t.Logf("Prefix (source) '%v' exists after setup: %v", TestLogPrefix, exists)
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix != TestLogPrefix {
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else if l.EnableDebug {
|
||||
t.Fatalf("did not properly set Debug filter state")
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
|
||||
// TestDefaultLogger tests GetLogger.
|
||||
func TestDefaultLogger(t *testing.T) {
|
||||
|
||||
var l Logger
|
||||
var tempfile *os.File
|
||||
var tempfilePath string
|
||||
var keepLog bool
|
||||
var ltype string
|
||||
var prefix string
|
||||
var testPrefix string
|
||||
var err error
|
||||
|
||||
if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
|
||||
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
tempfilePath = tempfile.Name()
|
||||
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
|
||||
if err = tempfile.Close(); err != nil {
|
||||
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
|
||||
if l, err = GetLoggerWindows(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
|
||||
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
ltype = fmt.Sprintf("%T", l)
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if ltype == "StdLogger" || ltype == "FileLogger" { // StdLogger (and thus FileLogger) adds a space at the end.
|
||||
testPrefix = TestLogPrefix + " "
|
||||
} else {
|
||||
testPrefix = TestLogPrefix
|
||||
}
|
||||
|
||||
if prefix != testPrefix {
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
_, keepLog = os.LookupEnv(EnvVarKeepLog)
|
||||
|
||||
if !keepLog {
|
||||
if err = os.Remove(tempfilePath); err != nil {
|
||||
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
@@ -1,69 +1,138 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`errors`
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
`golang.org/x/sys/windows/svc/eventlog`
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"r00t2.io/sysutils/paths"
|
||||
)
|
||||
|
||||
// Setup sets up/configures a WinLogger and prepares it for use.
|
||||
func (l *WinLogger) Setup() {
|
||||
/*
|
||||
Setup sets up/configures a WinLogger and prepares it for use.
|
||||
This will fail with an Access Denied (the first time, at least) unless running with elevated permissions unless WinLogger.Prefix is
|
||||
a registered Event Log source.
|
||||
|
||||
var err error
|
||||
If a failure occurs while trying to open the log with the given WinLogger.Prefix ("source"), a new Event Log source will be registered.
|
||||
If WinLogger.Executable is not empty at the time of calling WinLogger.Setup (or WinLogger.ForceService is true),
|
||||
eventlog.Install will be used (with the WinLogger.ExpandKey field).
|
||||
Otherwise eventlog.InstallAsEventCreate will be used.
|
||||
*/
|
||||
func (l *WinLogger) Setup() (err error) {
|
||||
|
||||
/*
|
||||
First a sanity check on the EventIDs.
|
||||
A sanity check on the EventIDs.
|
||||
Since we use eventcreate, all Event IDs must be 1 <= eid <= 1000.
|
||||
*/
|
||||
for _, eid := range []uint32{
|
||||
l.eids.Alert,
|
||||
l.eids.Crit,
|
||||
l.eids.Debug,
|
||||
l.eids.Emerg,
|
||||
l.eids.Err,
|
||||
l.eids.Info,
|
||||
l.eids.Notice,
|
||||
l.eids.Warning,
|
||||
l.EIDs.Alert,
|
||||
l.EIDs.Crit,
|
||||
l.EIDs.Debug,
|
||||
l.EIDs.Emerg,
|
||||
l.EIDs.Err,
|
||||
l.EIDs.Info,
|
||||
l.EIDs.Notice,
|
||||
l.EIDs.Warning,
|
||||
} {
|
||||
if !(1 <= eid <= 1000) {
|
||||
err = errors.New("event IDs must be between 1 and 1000 inclusive")
|
||||
panic(err)
|
||||
if !((eid <= EIDMax) && (EIDMin <= eid)) {
|
||||
err = ErrBadEid
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
|
||||
if idx := ptrnSourceExists.FindStringIndex(err.Error()); idx == nil {
|
||||
// It's an error we want to panic on.
|
||||
panic(err)
|
||||
} else {
|
||||
// It already exists, so ignore the error.
|
||||
err = nil
|
||||
}
|
||||
if err = l.Install(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if l.elog, err = eventlog.Open(l.Prefix); err != nil {
|
||||
panic(err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Install installs/registers the WinLogger Event Log interface. You most likely do not need to run this directly.
|
||||
func (l *WinLogger) Install() (err error) {
|
||||
|
||||
var exists bool
|
||||
var doNotCreate bool
|
||||
var useEventCreate bool = true
|
||||
|
||||
if doNotCreate, err = l.Exists(); err != nil {
|
||||
return
|
||||
} else if !doNotCreate {
|
||||
if l.Executable != "" {
|
||||
if l.Executable, err = exec.LookPath(l.Executable); err != nil {
|
||||
return
|
||||
}
|
||||
useEventCreate = false
|
||||
} else if l.ForceService {
|
||||
if l.Executable, err = exec.LookPath(os.Args[0]); err != nil {
|
||||
return
|
||||
}
|
||||
useEventCreate = false
|
||||
}
|
||||
if !useEventCreate {
|
||||
if exists, err = paths.RealPathExists(&l.Executable); err != nil {
|
||||
return
|
||||
} else if !exists {
|
||||
err = ErrBadBinPath
|
||||
return
|
||||
}
|
||||
if err = eventlog.Install(l.Prefix, l.Executable, l.ExpandKey, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Shutdown cleanly shuts down a WinLogger.
|
||||
func (l *WinLogger) Shutdown() {
|
||||
|
||||
var err error
|
||||
|
||||
if err = l.elog.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Remove uninstalls a registered WinLogger source.
|
||||
func (l *WinLogger) Remove() (err error) {
|
||||
|
||||
if err = eventlog.Remove(l.Prefix); err != nil {
|
||||
panic(err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetPrefix returns the prefix used by this WinLogger.
|
||||
func (l *WinLogger) GetPrefix() (prefix string) {
|
||||
/*
|
||||
Shutdown cleanly shuts down a WinLogger but keep the source registered. Use WinLogger.Remove
|
||||
(or set WinLogger.RemoveOnClose to true before calling WinLogger.Shutdown) to remove the registered source.
|
||||
*/
|
||||
func (l *WinLogger) Shutdown() (err error) {
|
||||
|
||||
if err = l.elog.Close(); err != nil {
|
||||
// TODO: check for no access or file not exists syscall errors?
|
||||
return
|
||||
}
|
||||
|
||||
if l.RemoveOnClose {
|
||||
if err = l.Remove(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetPrefix returns the prefix used by this WinLogger.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *WinLogger) GetPrefix() (prefix string, err error) {
|
||||
|
||||
prefix = l.Prefix
|
||||
|
||||
@@ -71,45 +140,65 @@ func (l *WinLogger) GetPrefix() (prefix string) {
|
||||
}
|
||||
|
||||
/*
|
||||
DoDebug sets the debug state of this WinLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
DoDebug sets the debug state of this WinLogger.
|
||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||
err will always be nil; it's there for interface-compat.
|
||||
*/
|
||||
func (l *WinLogger) DoDebug(d bool) {
|
||||
func (l *WinLogger) DoDebug(d bool) (err error) {
|
||||
|
||||
l.EnableDebug = d
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetDebug returns the debug status of this WinLogger.
|
||||
func (l *WinLogger) GetDebug() (d bool) {
|
||||
|
||||
d = l.EnableDebug
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetPrefix sets the prefix for this WinLogger.
|
||||
func (l *WinLogger) SetPrefix(prefix string) {
|
||||
func (l *WinLogger) SetPrefix(prefix string) (err error) {
|
||||
|
||||
var err error
|
||||
// To properly change the prefix, we need to tear down the old event log and create a new one.
|
||||
if err = l.Shutdown(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.Prefix = prefix
|
||||
|
||||
// To properly change the prefix, we need to tear down the old event log and create a new one.
|
||||
if err = l.elog.Close(); err != nil {
|
||||
panic(err)
|
||||
if err = l.Setup(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = eventlog.Remove(l.Prefix); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = eventlog.InstallAsEventCreate(l.Prefix, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
|
||||
if idx := ptrnSourceExists.FindStringIndex(err.Error()); idx == nil {
|
||||
// It's an error we want to panic on.
|
||||
panic(err)
|
||||
} else {
|
||||
// It already exists, so ignore the error.
|
||||
// Exists indicates if the WinLogger.Prefix is a registered source or not.
|
||||
func (l *WinLogger) Exists() (e bool, err error) {
|
||||
|
||||
var regKey registry.Key
|
||||
var subKey registry.Key
|
||||
|
||||
if regKey, err = registry.OpenKey(registry.LOCAL_MACHINE, eventLogRegistryKey, registry.READ); err != nil {
|
||||
return
|
||||
}
|
||||
defer regKey.Close()
|
||||
|
||||
if subKey, err = registry.OpenKey(regKey, l.Prefix, registry.READ); err != nil {
|
||||
if errors.Is(err, syscall.ERROR_FILE_NOT_FOUND) {
|
||||
e = false
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
defer subKey.Close()
|
||||
|
||||
if l.elog, err = eventlog.Open(l.Prefix); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
e = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Alert writes an ALERT-level message to this WinLogger.
|
||||
@@ -124,7 +213,7 @@ func (l *WinLogger) Alert(s string, v ...interface{}) (err error) {
|
||||
}
|
||||
|
||||
// Treat ALERT as Warning
|
||||
err = l.elog.Warning(l.eids.Alert, msg)
|
||||
err = l.elog.Warning(l.EIDs.Alert, msg)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -141,7 +230,7 @@ func (l *WinLogger) Crit(s string, v ...interface{}) (err error) {
|
||||
}
|
||||
|
||||
// Treat CRIT as Error
|
||||
err = l.elog.Error(l.eids.Crit, msg)
|
||||
err = l.elog.Error(l.EIDs.Crit, msg)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -162,7 +251,7 @@ func (l *WinLogger) Debug(s string, v ...interface{}) (err error) {
|
||||
}
|
||||
|
||||
// Treat DEBUG as Info
|
||||
err = l.elog.Info(l.eids.Debug, msg)
|
||||
err = l.elog.Info(l.EIDs.Debug, msg)
|
||||
|
||||
return
|
||||
|
||||
@@ -180,7 +269,7 @@ func (l *WinLogger) Emerg(s string, v ...interface{}) (err error) {
|
||||
}
|
||||
|
||||
// Treat EMERG as Error
|
||||
err = l.elog.Error(l.eids.Emerg, msg)
|
||||
err = l.elog.Error(l.EIDs.Emerg, msg)
|
||||
|
||||
return
|
||||
|
||||
@@ -197,7 +286,7 @@ func (l *WinLogger) Err(s string, v ...interface{}) (err error) {
|
||||
msg = s
|
||||
}
|
||||
|
||||
err = l.elog.Error(l.eids.Error, msg)
|
||||
err = l.elog.Error(l.EIDs.Err, msg)
|
||||
|
||||
return
|
||||
|
||||
@@ -214,7 +303,7 @@ func (l *WinLogger) Info(s string, v ...interface{}) (err error) {
|
||||
msg = s
|
||||
}
|
||||
|
||||
err = l.elog.Info(l.eids.Info, msg)
|
||||
err = l.elog.Info(l.EIDs.Info, msg)
|
||||
|
||||
return
|
||||
|
||||
@@ -232,7 +321,7 @@ func (l *WinLogger) Notice(s string, v ...interface{}) (err error) {
|
||||
}
|
||||
|
||||
// Treat NOTICE as Info
|
||||
err = l.elog.Info(l.eids.Notice, msg)
|
||||
err = l.elog.Info(l.EIDs.Notice, msg)
|
||||
|
||||
return
|
||||
|
||||
@@ -249,8 +338,24 @@ func (l *WinLogger) Warning(s string, v ...interface{}) (err error) {
|
||||
msg = s
|
||||
}
|
||||
|
||||
err = l.elog.Warning(l.eids.Warning, msg)
|
||||
err = l.elog.Warning(l.EIDs.Warning, msg)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
121
logging/muiltilogger_linux_test.go
Normal file
121
logging/muiltilogger_linux_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`os`
|
||||
`testing`
|
||||
)
|
||||
|
||||
// TestMultiLogger tests GetMultiLogger and MultiLogger methods.
|
||||
func TestMultiLogger(t *testing.T) {
|
||||
|
||||
var l *MultiLogger
|
||||
var tempfile *os.File
|
||||
var tempfilePath string
|
||||
var keepLog bool
|
||||
var ltype string = "MultiLogger"
|
||||
var prefix string
|
||||
var testPrefix string
|
||||
var err error
|
||||
|
||||
if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
|
||||
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
tempfilePath = tempfile.Name()
|
||||
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
|
||||
if err = tempfile.Close(); err != nil {
|
||||
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
|
||||
l = GetMultiLogger(true, TestLogPrefix)
|
||||
|
||||
if err = l.AddStdLogger("StdLogger", false, true, logFlags); err != nil {
|
||||
t.Fatalf("error when adding StdLogger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
if err = l.AddFileLogger("FileLogger", logFlags, tempfilePath); err != nil {
|
||||
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.AddDefaultLogger("DefaultLogger", logFlags, tempfilePath); err != nil {
|
||||
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.AddSysdLogger("SystemDLogger"); err != nil {
|
||||
t.Fatalf("error when adding SystemDLogger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
if err = l.AddSyslogLogger("SyslogLogger"); err != nil {
|
||||
t.Fatalf("error when adding SyslogLogger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if ltype == "StdLogger" || ltype == "FileLogger" { // StdLogger (and thus FileLogger) adds a space at the end.
|
||||
testPrefix = TestLogPrefix + " "
|
||||
} else {
|
||||
testPrefix = TestLogPrefix
|
||||
}
|
||||
|
||||
if prefix != testPrefix {
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
_, keepLog = os.LookupEnv(EnvVarKeepLog)
|
||||
|
||||
if !keepLog {
|
||||
if err = os.Remove(tempfilePath); err != nil {
|
||||
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
118
logging/multilogger_windows_test.go
Normal file
118
logging/multilogger_windows_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
`os`
|
||||
`testing`
|
||||
)
|
||||
|
||||
// TestMultiLogger tests GetMultiLogger and MultiLogger methods.
|
||||
func TestMultiLogger(t *testing.T) {
|
||||
|
||||
var l *MultiLogger
|
||||
var tempfile *os.File
|
||||
var tempfilePath string
|
||||
var keepLog bool
|
||||
var ltype string = "MultiLogger"
|
||||
var prefix string
|
||||
var testPrefix string
|
||||
var err error
|
||||
|
||||
if tempfile, err = os.CreateTemp("", ".LOGGINGTEST_*"); err != nil {
|
||||
t.Fatalf("error when creating temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
tempfilePath = tempfile.Name()
|
||||
// We can close the handler immediately; we don't need it since the FileLogger opens its own.
|
||||
if err = tempfile.Close(); err != nil {
|
||||
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
|
||||
l = GetMultiLogger(true, TestLogPrefix)
|
||||
|
||||
if err = l.AddStdLogger("StdLogger", false, true, logFlags); err != nil {
|
||||
t.Fatalf("error when adding StdLogger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
if err = l.AddFileLogger("FileLogger", logFlags, tempfilePath); err != nil {
|
||||
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.AddDefaultLogger("DefaultLogger", logFlags, tempfilePath); err != nil {
|
||||
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.AddWinLogger("WinLogger", DefaultEventID); err != nil {
|
||||
t.Fatalf("error when adding WinLogger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed Setup. Logger: %#v", ltype, l)
|
||||
|
||||
if err = l.Alert(testAlert, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Crit(testCrit, ltype); err != nil {
|
||||
t.Fatalf("error for Crit: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Debug(testDebug, ltype); err != nil {
|
||||
t.Fatalf("error for Debug: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Emerg(testEmerg, ltype); err != nil {
|
||||
t.Fatalf("error for Emerg: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Err(testErr, ltype); err != nil {
|
||||
t.Fatalf("error for Err: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Info(testInfo, ltype); err != nil {
|
||||
t.Fatalf("error for Alert: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Notice(testNotice, ltype); err != nil {
|
||||
t.Fatalf("error for Notice: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.Warning(testWarning, ltype); err != nil {
|
||||
t.Fatalf("error for Warning: %v", err.Error())
|
||||
}
|
||||
|
||||
if prefix, err = l.GetPrefix(); err != nil {
|
||||
t.Fatalf("error when fetching prefix: %v", err.Error())
|
||||
}
|
||||
|
||||
if ltype == "StdLogger" || ltype == "FileLogger" { // StdLogger (and thus FileLogger) adds a space at the end.
|
||||
testPrefix = TestLogPrefix + " "
|
||||
} else {
|
||||
testPrefix = TestLogPrefix
|
||||
}
|
||||
|
||||
if prefix != testPrefix {
|
||||
t.Fatalf("true prefix ('%v') does not match TestLogPrefix ('%v')", prefix, TestLogPrefix)
|
||||
}
|
||||
if err = l.SetPrefix(TestLogAltPrefix); err != nil {
|
||||
t.Fatalf("error when setting prefix to %v: %v", TestLogAltPrefix, err.Error())
|
||||
} else {
|
||||
_ = l.SetPrefix(TestLogPrefix)
|
||||
}
|
||||
|
||||
if err = l.DoDebug(false); err != nil {
|
||||
t.Fatalf("error when changing debug to false: %v", err.Error())
|
||||
} else {
|
||||
_ = l.DoDebug(true)
|
||||
}
|
||||
|
||||
if err = l.Shutdown(); err != nil {
|
||||
t.Fatalf("Error when running Shutdown: %v", err.Error())
|
||||
}
|
||||
|
||||
_, keepLog = os.LookupEnv(EnvVarKeepLog)
|
||||
|
||||
if !keepLog {
|
||||
if err = os.Remove(tempfilePath); err != nil {
|
||||
t.Fatalf("error when removing temporary log file '%v': %v", tempfilePath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Logger %v passed all logging targets.", ltype)
|
||||
}
|
||||
@@ -2,11 +2,15 @@ package logging
|
||||
|
||||
import (
|
||||
"log"
|
||||
`os`
|
||||
"os"
|
||||
|
||||
"r00t2.io/goutils/bitmask"
|
||||
)
|
||||
|
||||
type logPrio bitmask.MaskBit
|
||||
|
||||
/*
|
||||
Logger is one of the various loggers offered by this module.
|
||||
Logger is one of the various loggers offered by this module.
|
||||
*/
|
||||
type Logger interface {
|
||||
Alert(s string, v ...interface{}) (err error)
|
||||
@@ -17,16 +21,19 @@ type Logger interface {
|
||||
Info(s string, v ...interface{}) (err error)
|
||||
Notice(s string, v ...interface{}) (err error)
|
||||
Warning(s string, v ...interface{}) (err error)
|
||||
DoDebug(d bool)
|
||||
SetPrefix(p string)
|
||||
GetPrefix() (p string)
|
||||
Setup()
|
||||
Shutdown()
|
||||
DoDebug(d bool) (err error)
|
||||
GetDebug() (d bool)
|
||||
SetPrefix(p string) (err error)
|
||||
GetPrefix() (p string, err error)
|
||||
Setup() (err error)
|
||||
Shutdown() (err error)
|
||||
ToLogger(prio logPrio) (stdLibLog *log.Logger)
|
||||
ToRaw(prio logPrio) (raw *logWriter)
|
||||
}
|
||||
|
||||
/*
|
||||
StdLogger uses the log package in stdlib to perform all logging. The default is to write to STDOUT.
|
||||
If you wish to modify the underling log.Logger object, you can access it directly via StdLogger.Logger.
|
||||
StdLogger uses the log package in stdlib to perform all logging. The default is to write to STDOUT.
|
||||
If you wish to modify the underling log.Logger object, you can access it directly via StdLogger.Logger.
|
||||
*/
|
||||
type StdLogger struct {
|
||||
// All log.Logger fields/methods are exposed.
|
||||
@@ -38,24 +45,57 @@ type StdLogger struct {
|
||||
EnableDebug bool
|
||||
// Prefix indicates the prefix for log entries; in shared logs, this helps differentiate the source.
|
||||
Prefix string
|
||||
/*
|
||||
LogFlags control some of the formatting options presented as an OR'd value.
|
||||
See https://pkg.go.dev/log#pkg-constants for flag details.
|
||||
e.g.:
|
||||
*StdLogger.LogFlags = log.Ldate | log.Lmicroseconds | log.Llongfile | log.LUTC // a very detailed log output
|
||||
*StdLogger.LogFlags = log.Ldate | log.Ltime // the flags used by log.Default() (also available as simply log.LstdFlags)
|
||||
The default is 0; no flags (no output except prefix if non-empty and message).
|
||||
You will need to run *StdLogger.Shutdown and then *StdLogger.Setup again if you wish to change this.
|
||||
*/
|
||||
LogFlags int
|
||||
/*
|
||||
EnableStdOut is true if the log will send to STDOUT.
|
||||
If false (default), no output will be written to STDOUT.
|
||||
You will need to run StdLogger.Shutdown and then StdLogger.Setup again if you wish to change this.
|
||||
|
||||
If EnableStdOut is false and EnableStdErr is false, no logging output will occur by default
|
||||
and StdLogger.Logger will be largely useless.
|
||||
It will be up to you to modify the underlying log.Logger to behave as you want.
|
||||
*/
|
||||
EnableStdOut bool
|
||||
/*
|
||||
EnableStdErr is true if the log will send to STDERR.
|
||||
If false (default), no output will be written to STDERR.
|
||||
You will need to run StdLogger.Shutdown and then StdLogger.Setup again if you wish to change this.
|
||||
|
||||
If EnableStdErr is false and EnableStdOut is false, no logging output will occur by default
|
||||
and StdLogger.Logger will be largely useless.
|
||||
It will be up to you to modify the underlying log.Logger to behave as you want.
|
||||
*/
|
||||
EnableStdErr bool
|
||||
}
|
||||
|
||||
// FileLogger uses a StdLogger with a file handle writer to write to the file given at Path.
|
||||
/*
|
||||
FileLogger uses a StdLogger with a file handle writer to write to the file given at Path.
|
||||
|
||||
NOTE: If you wish to change the FileLogger.StdLogger.LogFlags, do *not* run FileLogger.StdLogger.Setup after doing so as this
|
||||
will instead create a logger detached from the file handler. Instead, be sure to call FileLogger.Setup.
|
||||
(Alternatively, run FileLogger.Shutdown and replace your logger with a new FileLogger.)
|
||||
*/
|
||||
type FileLogger struct {
|
||||
// StdLogger is used for the log formation and handling. See StdLogger for more details.
|
||||
StdLogger
|
||||
// Path is the path to the logfile.
|
||||
Path string
|
||||
/*
|
||||
EnableStdOut is true if the log will send to STDOUT as well as the file.
|
||||
If false (default), it will only (silently) write to the log file.
|
||||
You will need to run *FileLogger.Shutdown and then *FileLogger.Setup again if you wish to change this.
|
||||
*/
|
||||
EnableStdOut bool
|
||||
// writer is used for the writing out of the log file.
|
||||
writer *os.File
|
||||
}
|
||||
|
||||
// NullLogger is used mainly for test implementations, mockup code, etc. It does absolutely nothing with all messages sent to it.
|
||||
type NullLogger struct{}
|
||||
|
||||
// MultiLogger is used to contain one or more Loggers and present them all as a single Logger.
|
||||
type MultiLogger struct {
|
||||
/*
|
||||
@@ -71,3 +111,12 @@ 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{}
|
||||
|
||||
@@ -1,23 +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.
|
||||
*/
|
||||
type SystemDLogger struct {
|
||||
EnableDebug bool
|
||||
Prefix string
|
||||
}
|
||||
|
||||
type SyslogLogger struct {
|
||||
EnableDebug bool
|
||||
Prefix string
|
||||
alert,
|
||||
crit,
|
||||
debug,
|
||||
emerg,
|
||||
err,
|
||||
info,
|
||||
notice,
|
||||
warning *syslog.Writer
|
||||
}
|
||||
|
||||
22
logging/types_nix.go
Normal file
22
logging/types_nix.go
Normal file
@@ -0,0 +1,22 @@
|
||||
//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
|
||||
}
|
||||
@@ -4,13 +4,60 @@ import (
|
||||
`golang.org/x/sys/windows/svc/eventlog`
|
||||
)
|
||||
|
||||
// WinLogger is used for logging to the Windows Event Log. These entries are viewable in the Event Viewer application, under "Windows Logs > Application".
|
||||
type WinLogger struct {
|
||||
/*
|
||||
EnableDebug indicates if the debug filter should be disabled (true) or if the filter should be enabled (false).
|
||||
This prevents potential data leak of sensitive information, as some loggers (e.g. FileLogger) will otherwise write all messages.
|
||||
*/
|
||||
EnableDebug bool
|
||||
/*
|
||||
Prefix is used as the Event Log "Source". It's named as Prefix to retain compatability with methods in the Logger interface.
|
||||
*/
|
||||
Prefix string
|
||||
/*
|
||||
Executable is used as the path for the executable implementing this logger.
|
||||
If non-empty, it enables the "service" mode of Event Log (intended for "installed" software that's expected
|
||||
to exist as a specific path reliably).
|
||||
It can be a file within the PATHs or an absolute/relative path; an attempt to resolve the actual path will be made. If this fails or the file
|
||||
does not exist, an error will be raised.
|
||||
*/
|
||||
Executable string
|
||||
/*
|
||||
ExpandKey is only used if Executable is non-empty and valid and/or ForceService is true.
|
||||
If true, the WinLogger will be installed/registered with the REG_EXPAND_SZ mode - otherwise it will be installed as REG_SZ.
|
||||
See the definition for the two at https://docs.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types for further details.
|
||||
If you're unsure which you want, it's probably REG_SZ (WinLogger.ExpandKey == false), which is the default.
|
||||
*/
|
||||
ExpandKey bool
|
||||
/*
|
||||
ForceService, if true, will enforce WinLogger to be used as if Executable is populated and valid (it will use os.Args[0] as the Executable path).
|
||||
If Executable is empty but ForceService is true and os.Args[0] is empty or invalid (not a real path, etc.), an error will be raised.
|
||||
*/
|
||||
ForceService bool
|
||||
// RemoveOnClose should be true if the logger should be removed/unregistered from the Registry upon calling WinLogger.Shutdown.
|
||||
RemoveOnClose bool
|
||||
// elog is the actual writer to the Event Log.
|
||||
elog *eventlog.Log
|
||||
eids *WinEventID
|
||||
// EIDs is used to look up what event ID to use when writing to a WinLogger.elog.
|
||||
EIDs *WinEventID
|
||||
}
|
||||
|
||||
/*
|
||||
WinEventID is a collection of Event IDs to use for a WinLogger.
|
||||
Because Event Log only supports three entry types (informational, warning, or error),
|
||||
these event IDs allow you to filter the messages in a slightly more granular way. They map to their corresponding method name/Logger level.
|
||||
However, this means that a WinLogger does not support custom event IDs (and thus you cannot assign individual event IDs to specific errors).
|
||||
This is the price of convenience.
|
||||
|
||||
An additional method set may be added in the future to support this, but this is currently an unplanned feature.
|
||||
|
||||
Event IDs *must* be between the constants EIDMin and EIDMax (inclusive) unless the WinLogger is used in "service" mode
|
||||
(see WinLogger.Executable and WinLogger.ForceService).
|
||||
|
||||
If you need recommended defaults, you may want to use the Event* constants (e.g. EventAlert, EventDebug, etc.)
|
||||
or even use the pre-populated DefaultEventID (which is assigned the above Event* constants).
|
||||
*/
|
||||
type WinEventID struct {
|
||||
Alert,
|
||||
Crit,
|
||||
|
||||
4
mapsx/doc.go
Normal file
4
mapsx/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package mapsx includes functions that probably should have been in [maps] but aren't.
|
||||
*/
|
||||
package mapsx
|
||||
9
mapsx/errs.go
Normal file
9
mapsx/errs.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package mapsx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("key not found")
|
||||
)
|
||||
43
mapsx/funcs.go
Normal file
43
mapsx/funcs.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package mapsx
|
||||
|
||||
/*
|
||||
Get mimics Python's [dict.get()] behavior, returning value `v` if key `k`
|
||||
is not found in map `m`.
|
||||
|
||||
See also [GetOk], [Must].
|
||||
|
||||
[dict.get()]: https://docs.python.org/3/library/stdtypes.html#dict.get
|
||||
*/
|
||||
func Get[Map ~map[K]V, K comparable, V any](m Map, k K, v V) (val V) {
|
||||
|
||||
val, _ = GetOk(m, k, v)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetOk is like [Get] but also explicitly indicates whether `k` was found or not. See also [Must].
|
||||
func GetOk[Map ~map[K]V, K comparable, V any](m Map, k K, v V) (val V, found bool) {
|
||||
|
||||
if val, found = m[k]; !found {
|
||||
val = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Must, unlike [Get] or [GetOk], requires that `k` be in map `m`.
|
||||
|
||||
A panic with error [ErrNotFound] will be raised if `k` is not present.
|
||||
Otherwise the found value will be returned.
|
||||
*/
|
||||
func Must[Map ~map[K]V, K comparable, V any](m Map, k K) (val V) {
|
||||
|
||||
var ok bool
|
||||
|
||||
if val, ok = m[k]; !ok {
|
||||
panic(ErrNotFound)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
21
multierr/TODO
Normal file
21
multierr/TODO
Normal file
@@ -0,0 +1,21 @@
|
||||
- add unwrapping
|
||||
https://go.dev/blog/go1.13-errors#the-unwrap-method
|
||||
- add As method, takes a ptr to a slice of []error to return the first matching error type (errors.As) for each?
|
||||
- add AsAll [][]error ptr param for multiple errors per type?
|
||||
- add Map, returns map[string][]error, where key is k:
|
||||
var sb strings.Builder
|
||||
t = reflect.TypeOf(err)
|
||||
if t.PkgPath() != "" {
|
||||
sb.WriteString(t.PkgPath())
|
||||
} else {
|
||||
sb.WriteString("<UNKNOWN>")
|
||||
}
|
||||
sb.WriteString(".")
|
||||
if t.Name() != "" {
|
||||
sb.WriteString(t.Name())
|
||||
} else {
|
||||
sb.WriteString("<UNKNOWN>")
|
||||
}
|
||||
k = sb.String()
|
||||
- support generics for similar to above?
|
||||
- this might allow for "error filtering"
|
||||
@@ -69,6 +69,9 @@ 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)
|
||||
@@ -87,6 +90,9 @@ func (e *MultiError) AddError(err error) {
|
||||
return
|
||||
}
|
||||
|
||||
e.lock.Lock()
|
||||
defer e.lock.Unlock()
|
||||
|
||||
e.Errors = append(e.Errors, err)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
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
|
||||
}
|
||||
|
||||
13
netx/consts_nix.go
Normal file
13
netx/consts_nix.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !windows
|
||||
|
||||
package netx
|
||||
|
||||
import (
|
||||
`golang.org/x/sys/unix`
|
||||
)
|
||||
|
||||
const (
|
||||
AFUnspec uint16 = unix.AF_UNSPEC
|
||||
AFInet uint16 = unix.AF_INET
|
||||
AFInet6 uint16 = unix.AF_INET6
|
||||
)
|
||||
13
netx/consts_windows.go
Normal file
13
netx/consts_windows.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build windows
|
||||
|
||||
package netx
|
||||
|
||||
import (
|
||||
`golang.org/x/sys/windows`
|
||||
)
|
||||
|
||||
const (
|
||||
AFUnspec uint16 = windows.AF_UNSPEC
|
||||
AFInet uint16 = windows.AF_INET
|
||||
AFInet6 uint16 = windows.AF_INET6
|
||||
)
|
||||
4
netx/docs.go
Normal file
4
netx/docs.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package netx includes extensions to the stdlib `net` module.
|
||||
*/
|
||||
package netx
|
||||
10
netx/errors.go
Normal file
10
netx/errors.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package netx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadMask4Str error = errors.New("netx: unknown/bad IPv4 netmask dotted quad")
|
||||
ErrBadNetFam error = errors.New("netx: unknown/bad IP network family")
|
||||
)
|
||||
410
netx/funcs.go
Normal file
410
netx/funcs.go
Normal file
@@ -0,0 +1,410 @@
|
||||
package netx
|
||||
|
||||
import (
|
||||
`math/bits`
|
||||
`net`
|
||||
`net/netip`
|
||||
`strconv`
|
||||
`strings`
|
||||
|
||||
`go4.org/netipx`
|
||||
)
|
||||
|
||||
/*
|
||||
AddrRfc returns an RFC-friendly string from an IP address ([net/netip.Addr]).
|
||||
|
||||
If addr is an IPv4 address, it will simply be the string representation (e.g. "203.0.113.1").
|
||||
|
||||
If addr is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]").
|
||||
|
||||
If the version can't be determined, rfcStr will be an empty string.
|
||||
*/
|
||||
func AddrRfc(addr netip.Addr) (rfcStr string) {
|
||||
|
||||
if addr.Is4() {
|
||||
rfcStr = addr.String()
|
||||
} else if addr.Is6() {
|
||||
rfcStr = "[" + addr.String() + "]"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Cidr4ToIPMask takes an IPv4 CIDR/bit size/prefix length and returns the [net.IPMask].
|
||||
It's (essentially) the inverse of [net.IPMask.Size].
|
||||
|
||||
See also:
|
||||
|
||||
* [Cidr4ToMask]
|
||||
* [Cidr4ToStr]
|
||||
|
||||
Inverse of [IPMask4ToCidr].
|
||||
*/
|
||||
func Cidr4ToIPMask(cidr uint8) (ipMask net.IPMask, err error) {
|
||||
|
||||
if cidr > 32 {
|
||||
err = ErrBadNetFam
|
||||
return
|
||||
}
|
||||
|
||||
ipMask = net.CIDRMask(int(cidr), 32)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Cidr4ToMask takes an IPv4 CIDR/bit size/prefix length and returns the netmask *in bitmask form*.
|
||||
|
||||
See also:
|
||||
|
||||
* [Cidr4ToIPMask]
|
||||
* [Cidr4ToStr]
|
||||
|
||||
Inverse of [Mask4ToCidr].
|
||||
*/
|
||||
func Cidr4ToMask(cidr uint8) (mask uint32, err error) {
|
||||
|
||||
if cidr > 32 {
|
||||
err = ErrBadNetFam
|
||||
return
|
||||
}
|
||||
|
||||
// COULD do (1 << 32) - (1 << (32 - ip.Bits())) instead but in EXTREME edge cases that could cause an overflow.
|
||||
// We're basically converting the CIDR size ("number of bits"/"number of ones") to an integer mask ("number AS bits")
|
||||
mask = uint32(0xffffffff) << uint32(32-cidr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Cidr4ToStr is a convenience wrapper around [IPMask4ToStr]([Cidr4ToMask](cidr)).
|
||||
|
||||
See also:
|
||||
|
||||
* [Cidr4ToIPMask]
|
||||
* [Cidr4ToMask]
|
||||
|
||||
Inverse of [Mask4StrToCidr].
|
||||
*/
|
||||
func Cidr4ToStr(cidr uint8) (maskStr string, err error) {
|
||||
|
||||
var ipMask net.IPMask
|
||||
|
||||
if ipMask, err = Cidr4ToIPMask(cidr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if maskStr, err = IPMask4ToStr(ipMask); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetAddrFamily returns the network family of a [net/netip.Addr].
|
||||
|
||||
See also [GetIpFamily].
|
||||
|
||||
If addr is not a "valid" IP address or the version can't be determined, family will be AFUnspec (usually 0x00/0).
|
||||
*/
|
||||
func GetAddrFamily(addr netip.Addr) (family uint16) {
|
||||
|
||||
family = AFUnspec
|
||||
|
||||
if !addr.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
if addr.Is4() {
|
||||
family = AFInet
|
||||
} else if addr.Is6() {
|
||||
family = AFInet6
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetIpFamily returns the network family of a [net.IP].
|
||||
|
||||
See also [GetAddrFamily].
|
||||
|
||||
If ip is not a "valid" IP address or the version can't be determined,
|
||||
family will be [golang.org/x/sys/unix.AF_UNSPEC] or [golang.org/x/sys/windows.AF_UNSPEC] depending on platform (usually 0x00/0).
|
||||
*/
|
||||
func GetIpFamily(ip net.IP) (family uint16) {
|
||||
|
||||
var ok bool
|
||||
var addr netip.Addr
|
||||
|
||||
if addr, ok = netipx.FromStdIP(ip); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
family = GetAddrFamily(addr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpRfc returns an RFC-friendly string from an IP address ([net.IP]).
|
||||
|
||||
If ip is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1").
|
||||
|
||||
If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]").
|
||||
|
||||
If the version can't be determined, rfcStr will be an empty string.
|
||||
*/
|
||||
func IpRfc(ip net.IP) (rfcStr string) {
|
||||
|
||||
if ip.To4() != nil {
|
||||
rfcStr = ip.To4().String()
|
||||
} else if ip.To16() != nil {
|
||||
rfcStr = "[" + ip.To16().String() + "]"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask].
|
||||
|
||||
See also:
|
||||
|
||||
* [IPMask4ToMask]
|
||||
* [IPMask4ToStr]
|
||||
|
||||
Inverse of [Cidr4ToIPMask].
|
||||
*/
|
||||
func IPMask4ToCidr(ipMask net.IPMask) (cidr uint8, err error) {
|
||||
|
||||
var ones int
|
||||
var total int
|
||||
|
||||
ones, total = ipMask.Size()
|
||||
|
||||
if total != 32 {
|
||||
err = ErrBadNetFam
|
||||
return
|
||||
}
|
||||
if ones > 32 {
|
||||
err = ErrBadNetFam
|
||||
return
|
||||
}
|
||||
|
||||
cidr = uint8(ones)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IPMask4ToMask returns the mask *in bitmask form* from a [net.IPMask].
|
||||
|
||||
See also:
|
||||
|
||||
* [IPMask4ToCidr]
|
||||
* [IPMask4ToStr]
|
||||
|
||||
Inverse of [Mask4ToIPMask].
|
||||
*/
|
||||
func IPMask4ToMask(ipMask net.IPMask) (mask uint32, err error) {
|
||||
|
||||
var cidr uint8
|
||||
|
||||
if cidr, err = IPMask4ToCidr(ipMask); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if mask, err = Cidr4ToMask(cidr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IPMask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a [net.IPMask].
|
||||
|
||||
See also:
|
||||
|
||||
* [IPMask4ToCidr]
|
||||
* [IPMask4ToMask]
|
||||
|
||||
Inverse of [Mask4StrToIPMask].
|
||||
*/
|
||||
func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) {
|
||||
|
||||
var idx int
|
||||
var b []byte
|
||||
var quads []string = make([]string, 4)
|
||||
|
||||
b = []byte(ipMask)
|
||||
if len(b) != 4 {
|
||||
err = ErrBadNetFam
|
||||
return
|
||||
}
|
||||
|
||||
for idx = 0; idx < len(b); idx++ {
|
||||
quads[idx] = strconv.Itoa(int(b[idx]))
|
||||
}
|
||||
|
||||
maskStr = strings.Join(quads, ".")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Mask4ToCidr converts an IPv4 netmask *in bitmask form* to a CIDR prefix size/bit size/bit length.
|
||||
|
||||
See also:
|
||||
|
||||
* [Mask4ToIPMask]
|
||||
* [Mask4ToStr]
|
||||
|
||||
Inverse of [Cidr4ToMask].
|
||||
*/
|
||||
func Mask4ToCidr(mask uint32) (cidr uint8, err error) {
|
||||
|
||||
cidr = 32 - uint8(bits.LeadingZeros32(mask))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Mask4ToIPMask returns mask *in bitmask form* as a [net.IPMask].
|
||||
|
||||
See also:
|
||||
|
||||
* [Mask4ToCidr]
|
||||
* [Mask4ToStr]
|
||||
|
||||
Inverse of [IPMask4ToMask].
|
||||
*/
|
||||
func Mask4ToIPMask(mask uint32) (ipMask net.IPMask, err error) {
|
||||
|
||||
var cidr uint8
|
||||
|
||||
if cidr, err = Mask4ToCidr(mask); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ipMask = net.CIDRMask(int(cidr), 32)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Mask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a netmask *in bitmask form*.
|
||||
|
||||
See also:
|
||||
|
||||
* [Mask4ToCidr]
|
||||
* [Mask4ToIPMask]
|
||||
|
||||
Inverse of [Mask4StrToMask].
|
||||
*/
|
||||
func Mask4ToStr(mask uint32) (maskStr string, err error) {
|
||||
|
||||
var ipMask net.IPMask
|
||||
|
||||
if ipMask, err = Mask4ToIPMask(mask); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if maskStr, err = IPMask4ToStr(ipMask); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Mask4StrToCidr parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns am IPv4 CIDR/bit size/prefix length.
|
||||
|
||||
See also:
|
||||
|
||||
* [Mask4StrToIPMask]
|
||||
* [Mask4StrToMask]
|
||||
|
||||
Inverse of [Cidr4ToMaskStr].
|
||||
*/
|
||||
func Mask4StrToCidr(maskStr string) (cidr uint8, err error) {
|
||||
|
||||
var ipMask net.IPMask
|
||||
|
||||
if ipMask, err = Mask4StrToIPMask(maskStr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if cidr, err = IPMask4ToCidr(ipMask); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Mask4StrToIPMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a [net.IPMask].
|
||||
|
||||
See also:
|
||||
|
||||
* [Mask4StrToCidr]
|
||||
* [Mask4StrToMask]
|
||||
|
||||
Inverse of [IPMask4ToStr].
|
||||
*/
|
||||
func Mask4StrToIPMask(maskStr string) (mask net.IPMask, err error) {
|
||||
|
||||
var idx int
|
||||
var s string
|
||||
var u64 uint64
|
||||
var b []byte = make([]byte, 4)
|
||||
var sl []string = strings.Split(maskStr, ".")
|
||||
|
||||
if len(sl) != 4 {
|
||||
err = ErrBadMask4Str
|
||||
return
|
||||
}
|
||||
|
||||
// A net.IPMask is just a []byte.
|
||||
for idx = 0; idx < len(sl); idx++ {
|
||||
s = sl[idx]
|
||||
if u64, err = strconv.ParseUint(s, 10, 8); err != nil {
|
||||
return
|
||||
}
|
||||
b[idx] = byte(u64)
|
||||
}
|
||||
|
||||
mask = net.IPMask(b)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Mask4StrToMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a netmask *in bitmask form*.
|
||||
|
||||
See also:
|
||||
|
||||
* [Mask4StrToCidr]
|
||||
* [Mask4StrToIPMask]
|
||||
|
||||
Inverse of [Mask4ToStr].
|
||||
*/
|
||||
func Mask4StrToMask(maskStr string) (mask uint32, err error) {
|
||||
|
||||
var ipMask net.IPMask
|
||||
|
||||
if ipMask, err = Mask4StrToIPMask(maskStr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if mask, err = IPMask4ToMask(ipMask); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
134
netx/funcs_test.go
Normal file
134
netx/funcs_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package netx
|
||||
|
||||
import (
|
||||
`math`
|
||||
`net`
|
||||
`net/netip`
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFuncsIP(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var ip net.IP
|
||||
var addr netip.Addr
|
||||
var ipFamily uint16
|
||||
var tgtFamily uint16
|
||||
var addrFamily uint16
|
||||
|
||||
// IPv4 on even indexes, IPv6 on odd.
|
||||
for idx, s := range []string{
|
||||
"203.0.113.10",
|
||||
"2001:db8::203:0:113:10",
|
||||
} {
|
||||
if ip = net.ParseIP(s); ip == nil {
|
||||
t.Fatalf("ip %s not valid", s)
|
||||
}
|
||||
if addr, err = netip.ParseAddr(s); err != nil {
|
||||
t.Fatalf("addr %s not valid", s)
|
||||
}
|
||||
ipFamily = GetIpFamily(ip)
|
||||
addrFamily = GetAddrFamily(addr)
|
||||
if ipFamily == AFUnspec {
|
||||
t.Fatalf("GetIpFamily: Failed on IP %s (unspecified family)", s)
|
||||
}
|
||||
if addrFamily == AFUnspec {
|
||||
t.Fatalf("GetAddrFamily: Failed on IP %s (unspecified family)", s)
|
||||
}
|
||||
switch idx%2 == 0 {
|
||||
case true:
|
||||
tgtFamily = AFInet
|
||||
case false:
|
||||
tgtFamily = AFInet6
|
||||
}
|
||||
if ipFamily != tgtFamily {
|
||||
t.Fatalf("GetIpFamily: Failed on IP %s (expected %d, got %d)", s, AFInet, tgtFamily)
|
||||
}
|
||||
if addrFamily != tgtFamily {
|
||||
t.Fatalf("GetAddrFamily: Failed on IP %s (expected %d, got %d)", s, AFInet, tgtFamily)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncsMask(t *testing.T) {
|
||||
|
||||
var err error
|
||||
|
||||
var cidr uint8
|
||||
var mask uint32
|
||||
var maskStr string
|
||||
var ipMask net.IPMask
|
||||
|
||||
var cidrTgt uint8 = 32
|
||||
var maskTgt uint32 = math.MaxUint32
|
||||
var maskStrTgt string = "255.255.255.255"
|
||||
var ipMaskTgt net.IPMask = net.IPMask{255, 255, 255, 255}
|
||||
|
||||
// To CIDR
|
||||
if cidr, err = Mask4ToCidr(maskTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cidr != cidrTgt {
|
||||
t.Fatalf("Mask4ToCidr: cidr %d != cidrTgt %d", cidr, cidrTgt)
|
||||
}
|
||||
if cidr, err = IPMask4ToCidr(ipMaskTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cidr != cidrTgt {
|
||||
t.Fatalf("IPMask4ToCidr: cidr %d != cidrTgt %d", cidr, cidrTgt)
|
||||
}
|
||||
if cidr, err = Mask4StrToCidr(maskStrTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cidr != cidrTgt {
|
||||
t.Fatalf("Mask4StrToCidr cidr %d != cidrTgt %d", cidr, cidrTgt)
|
||||
}
|
||||
|
||||
// To net.IPMask
|
||||
if ipMask, err = Cidr4ToIPMask(cidrTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if ipMaskTgt.String() != ipMask.String() {
|
||||
t.Fatalf("Cidr4ToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String())
|
||||
}
|
||||
if ipMask, err = Mask4ToIPMask(maskTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if ipMaskTgt.String() != ipMask.String() {
|
||||
t.Fatalf("Mask4ToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String())
|
||||
}
|
||||
if ipMask, err = Mask4StrToIPMask(maskStrTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if ipMaskTgt.String() != ipMask.String() {
|
||||
t.Fatalf("Mask4StrToIPMask ipMask %s != ipMaskTgt %s", ipMask.String(), ipMaskTgt.String())
|
||||
}
|
||||
|
||||
// To bitmask
|
||||
if mask, err = Cidr4ToMask(cidrTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if mask != maskTgt {
|
||||
t.Fatalf("Cidr4ToMask mask %d != maskTgt %d", mask, maskTgt)
|
||||
}
|
||||
if mask, err = IPMask4ToMask(ipMaskTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if mask != maskTgt {
|
||||
t.Fatalf("IPMask4ToMask mask %d != maskTgt %d", mask, maskTgt)
|
||||
}
|
||||
if mask, err = Mask4StrToMask(maskStrTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if mask != maskTgt {
|
||||
t.Fatalf("Mask4StrToMask mask %d != maskTgt %d", mask, maskTgt)
|
||||
}
|
||||
|
||||
// To string
|
||||
if maskStr, err = Cidr4ToStr(cidrTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if maskStr != maskStrTgt {
|
||||
t.Fatalf("Cidr4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt)
|
||||
}
|
||||
if maskStr, err = IPMask4ToStr(ipMaskTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if maskStr != maskStrTgt {
|
||||
t.Fatalf("IPMask4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt)
|
||||
}
|
||||
if maskStr, err = Mask4ToStr(maskTgt); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if maskStr != maskStrTgt {
|
||||
t.Fatalf("Mask4ToStr maskStr %s != maskStrTgt %s", maskStr, maskStrTgt)
|
||||
}
|
||||
}
|
||||
33
netx/inetcksum/consts.go
Normal file
33
netx/inetcksum/consts.go
Normal file
@@ -0,0 +1,33 @@
|
||||
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"
|
||||
(the lower 16 bits before folding carries).
|
||||
*/
|
||||
cksumMask uint32 = 0x0000ffff
|
||||
/*
|
||||
cksumShift is used in the "carried-ones folding";
|
||||
it's the number of bits to right-shift the carry-over.
|
||||
*/
|
||||
cksumShift uint32 = 0x00000010
|
||||
/*
|
||||
padShift is used to "pad out" a checksum for odd-length buffers by left-shifting.
|
||||
It positions the high-byte of a 16-byte "word" (big-endian, as per ord below).
|
||||
*/
|
||||
padShift uint32 = 0x00000008
|
||||
)
|
||||
|
||||
var (
|
||||
// ord is the byte order used by the Internet Checksum.
|
||||
ord binary.ByteOrder = binary.BigEndian
|
||||
)
|
||||
35
netx/inetcksum/docs.go
Normal file
35
netx/inetcksum/docs.go
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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]
|
||||
|
||||
If you don't need all these interfaces, a reasonable alternative may be
|
||||
to use gVisor's [gvisor.dev/gvisor/pkg/tcpip/checksum] instead.
|
||||
|
||||
[RFC 1071]: https://datatracker.ietf.org/doc/html/rfc1071
|
||||
[RFC 1141]: https://datatracker.ietf.org/doc/html/rfc1141
|
||||
[RFC 1624]: https://datatracker.ietf.org/doc/html/rfc1624
|
||||
*/
|
||||
package inetcksum
|
||||
119
netx/inetcksum/funcs.go
Normal file
119
netx/inetcksum/funcs.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package inetcksum
|
||||
|
||||
import (
|
||||
`io`
|
||||
)
|
||||
|
||||
// New returns a new initialized [InetChecksum]. It will never panic.
|
||||
func New() (i *InetChecksum) {
|
||||
|
||||
i = &InetChecksum{
|
||||
aligned: true,
|
||||
}
|
||||
|
||||
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
|
||||
var cptr *InetChecksum = &cksum
|
||||
|
||||
cksum.aligned = true
|
||||
|
||||
if b != nil && len(b) > 0 {
|
||||
if copied, err = cptr.Write(b); err != nil {
|
||||
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
|
||||
|
||||
cksum.aligned = true
|
||||
|
||||
if buf != nil {
|
||||
if copied, err = io.Copy(&cksum, buf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
i = &cksum
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewSimple returns a new initialized [InetChecksumSimple]. It will never panic.
|
||||
func NewSimple() (i *InetChecksumSimple) {
|
||||
|
||||
i = &InetChecksumSimple{
|
||||
aligned: true,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
NewSimpleFromBytes returns a new [InetChecksumSimple] initialized with explicit bytes.
|
||||
|
||||
b may be nil or 0-length; this will not cause an error.
|
||||
*/
|
||||
func NewSimpleFromBytes(b []byte) (i *InetChecksumSimple, copied int, err error) {
|
||||
|
||||
var cksum InetChecksumSimple
|
||||
var cptr *InetChecksumSimple = &cksum
|
||||
|
||||
cksum.aligned = true
|
||||
|
||||
if b != nil && len(b) > 0 {
|
||||
if copied, err = cptr.Write(b); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
i = &cksum
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
NewSimpleFromBuf returns an [InetChecksumSimple] from a specified [io.Reader].
|
||||
|
||||
buf may be nil. If it isn't, NewSimpleFromBuf will call [io.Copy] on buf.
|
||||
Note that this may exhaust your passed buf or advance its current seek position/offset,
|
||||
depending on its type.
|
||||
*/
|
||||
func NewSimpleFromBuf(buf io.Reader) (i *InetChecksumSimple, copied int64, err error) {
|
||||
|
||||
var cksum InetChecksumSimple
|
||||
|
||||
cksum.aligned = true
|
||||
|
||||
if buf != nil {
|
||||
if copied, err = io.Copy(&cksum, buf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
i = &cksum
|
||||
|
||||
return
|
||||
}
|
||||
351
netx/inetcksum/funcs_inetchecksum.go
Normal file
351
netx/inetcksum/funcs_inetchecksum.go
Normal file
@@ -0,0 +1,351 @@
|
||||
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 = true
|
||||
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.buf.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
|
||||
}
|
||||
172
netx/inetcksum/funcs_inetchecksumsimple.go
Normal file
172
netx/inetcksum/funcs_inetchecksumsimple.go
Normal file
@@ -0,0 +1,172 @@
|
||||
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.aligned = true
|
||||
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
68
netx/inetcksum/types.go
Normal file
68
netx/inetcksum/types.go
Normal file
@@ -0,0 +1,68 @@
|
||||
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
|
||||
}
|
||||
)
|
||||
12
remap/doc.go
Normal file
12
remap/doc.go
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Package remap provides convenience functions around regular expressions,
|
||||
primarily offering maps for named capture groups.
|
||||
|
||||
It offers convenience equivalents of the following:
|
||||
|
||||
* [regexp.Compile] ([Compile])
|
||||
* [regexp.CompilePOSIX] ([CompilePOSIX])
|
||||
* [regexp.MustCompile] ([MustCompile])
|
||||
* [regexp.MustCompilePOSIX] ([MustCompilePOSIX])
|
||||
*/
|
||||
package remap
|
||||
11
remap/errs.go
Normal file
11
remap/errs.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package remap
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidIdxPair error = errors.New("invalid index pair; [1] must be >= [0]")
|
||||
ErrNoStr error = errors.New("no string to slice/reslice/subslice")
|
||||
ErrShortStr error = errors.New("string too short to slice/reslice/subslice")
|
||||
)
|
||||
170
remap/funcs.go
Normal file
170
remap/funcs.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package remap
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
/*
|
||||
Compile is a convenience shorthand for:
|
||||
|
||||
var err error
|
||||
var r *remap.ReMap = new(remap.ReMap)
|
||||
|
||||
if r.Regexp, err = regexp.Compile(expr); err != nil {
|
||||
// ...
|
||||
}
|
||||
|
||||
It corresponds to [regexp.Compile].
|
||||
*/
|
||||
func Compile(expr string) (r *ReMap, err error) {
|
||||
|
||||
var p *regexp.Regexp
|
||||
|
||||
if p, err = regexp.Compile(expr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r = &ReMap{
|
||||
Regexp: p,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CompilePOSIX is a convenience shorthand for:
|
||||
|
||||
var err error
|
||||
var r *remap.ReMap = new(remap.ReMap)
|
||||
|
||||
if r.Regexp, err = regexp.CompilePOSIX(expr); err != nil {
|
||||
// ...
|
||||
}
|
||||
|
||||
It corresponds to [regexp.CompilePOSIX].
|
||||
*/
|
||||
func CompilePOSIX(expr string) (r *ReMap, err error) {
|
||||
|
||||
var p *regexp.Regexp
|
||||
|
||||
if p, err = regexp.CompilePOSIX(expr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r = &ReMap{
|
||||
Regexp: p,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
MustCompile is a convenience shorthand for:
|
||||
|
||||
var r *remap.ReMap = &remap.ReMap{
|
||||
Regexp: regexp.MustCompile(expr),
|
||||
}
|
||||
|
||||
It corresponds to [regexp.MustCompile].
|
||||
*/
|
||||
func MustCompile(expr string) (r *ReMap) {
|
||||
|
||||
var err error
|
||||
var p *regexp.Regexp
|
||||
|
||||
// We panic ourselves instead of wrapping regexp.MustCompile.
|
||||
// Makes debuggers a little more explicit.
|
||||
if p, err = regexp.Compile(expr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r = &ReMap{
|
||||
Regexp: p,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
MustCompilePOSIX is a convenience shorthand for:
|
||||
|
||||
var r *remap.ReMap = &remap.ReMap{
|
||||
Regexp: regexp.MustCompilePOSIX(expr),
|
||||
}
|
||||
|
||||
It corresponds to [regexp.MustCompilePOSIX].
|
||||
*/
|
||||
func MustCompilePOSIX(expr string) (r *ReMap) {
|
||||
|
||||
var err error
|
||||
var p *regexp.Regexp
|
||||
|
||||
// We panic ourselves instead of wrapping regexp.MustCompilePOSIX.
|
||||
// Makes debuggers a little more explicit.
|
||||
if p, err = regexp.CompilePOSIX(expr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r = &ReMap{
|
||||
Regexp: p,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
strIdxSlicer takes string s, and returns the substring marked by idxPair,
|
||||
where:
|
||||
|
||||
idxPair = [2]int{
|
||||
<substring START POSITION>,
|
||||
<substring END BOUNDARY>,
|
||||
}
|
||||
|
||||
That is, to get `oo` from `foobar`,
|
||||
|
||||
idxPair = [2]int{1, 3}
|
||||
# NOT:
|
||||
#idxPair = [2]int{1, 2}
|
||||
|
||||
subStr will be empty and matched will be false if:
|
||||
|
||||
* idxPair[0] < 0
|
||||
* idxPair[1] < 0
|
||||
|
||||
It will panic with [ErrShortStr] if:
|
||||
|
||||
* idxPair[0] > len(s)-1
|
||||
* idxPair[1] > len(s)
|
||||
|
||||
It will panic with [ErrInvalidIdxPair] if:
|
||||
|
||||
* idxPair[0] > idxPair[1]
|
||||
|
||||
It will properly handle single-character addresses (i.e. idxPair[0] == idxPair[1]).
|
||||
*/
|
||||
func strIdxSlicer(s string, idxPair [2]int) (subStr string, matched bool) {
|
||||
|
||||
if idxPair[0] < 0 || idxPair[1] < 0 {
|
||||
return
|
||||
}
|
||||
matched = true
|
||||
|
||||
if (idxPair[0] > (len(s) - 1)) ||
|
||||
(idxPair[1] > len(s)) {
|
||||
panic(ErrShortStr)
|
||||
}
|
||||
if idxPair[0] > idxPair[1] {
|
||||
panic(ErrInvalidIdxPair)
|
||||
}
|
||||
|
||||
if idxPair[0] == idxPair[1] {
|
||||
// single character
|
||||
subStr = string(s[idxPair[0]])
|
||||
} else {
|
||||
// multiple characters
|
||||
subStr = s[idxPair[0]:idxPair[1]]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
815
remap/funcs_remap.go
Normal file
815
remap/funcs_remap.go
Normal file
@@ -0,0 +1,815 @@
|
||||
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).
|
||||
|
||||
It will panic if the embedded [regexp.Regexp] is nil.
|
||||
|
||||
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.
|
||||
|
||||
This operates on only the first found match (like [regexp.Regexp.FindSubmatch]).
|
||||
To operate on *all* matches, use [ReMap.MapAll].
|
||||
|
||||
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.
|
||||
May be unpredictable if the same name is used multiple times for different capture groups across multiple patterns.
|
||||
|
||||
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 {
|
||||
if grpNm == "" {
|
||||
continue
|
||||
}
|
||||
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 matched 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 grpNm == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok = tmpMap[grpNm]; !ok {
|
||||
tmpMap[grpNm] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(tmpMap) > 0 {
|
||||
matches = tmpMap
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
MapAll behaves exactly like [ReMap.Map] but will "squash"/consolidate *all* found matches, not just the first occurrence,
|
||||
into the group name.
|
||||
|
||||
You likely want to use this instead of [ReMap.Map] for multiline patterns.
|
||||
*/
|
||||
func (r *ReMap) MapAll(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][][]byte) {
|
||||
|
||||
var ok bool
|
||||
var mIdx int
|
||||
var isEmpty bool
|
||||
var match []byte
|
||||
var grpNm string
|
||||
var names []string
|
||||
var mbGrp [][]byte
|
||||
var ptrnNms []string
|
||||
var matchBytes [][][]byte
|
||||
var tmpMap map[string][][]byte = make(map[string][][]byte)
|
||||
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
|
||||
names = r.Regexp.SubexpNames()[:]
|
||||
matchBytes = r.Regexp.FindAllSubmatch(b, -1)
|
||||
|
||||
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:]
|
||||
|
||||
tmpMap = make(map[string][][]byte)
|
||||
|
||||
// From here, it behaves (sort of) like ReMap.Map
|
||||
// except mbGrp is like matchBytes in Map.
|
||||
for _, mbGrp = range matchBytes {
|
||||
|
||||
// Unlike ReMap.Map, we have to do a little additional logic.
|
||||
isEmpty = false
|
||||
ptrnNms = make([]string, 0, len(names))
|
||||
|
||||
if mbGrp == nil {
|
||||
isEmpty = true
|
||||
}
|
||||
|
||||
if !isEmpty {
|
||||
if len(mbGrp) == 0 || len(mbGrp) == 1 {
|
||||
/*
|
||||
no submatches whatsoever.
|
||||
*/
|
||||
isEmpty = true
|
||||
} else {
|
||||
mbGrp = mbGrp[1:]
|
||||
|
||||
for mIdx, match = range mbGrp {
|
||||
if mIdx > len(names) {
|
||||
break
|
||||
}
|
||||
grpNm = names[mIdx]
|
||||
if grpNm == "" {
|
||||
continue
|
||||
}
|
||||
ptrnNms = append(ptrnNms, grpNm)
|
||||
|
||||
if match == nil {
|
||||
// This specific group didn't match, but it matched the whole pattern.
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// I can't recall why I capture this.
|
||||
_ = ptrnNms
|
||||
}
|
||||
|
||||
// *Theoretically* all of these should be populated with at least a nil.
|
||||
if inclNoMatch {
|
||||
for _, grpNm = range names {
|
||||
if grpNm == "" {
|
||||
continue
|
||||
}
|
||||
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 == "".)
|
||||
|
||||
It will panic if the embedded [regexp.Regexp] is nil.
|
||||
|
||||
This operates on only the first found match (like [regexp.Regexp.FindStringSubmatch]).
|
||||
To operate on *all* matches, use [ReMap.MapStringAll].
|
||||
|
||||
A small deviation and caveat, 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 empty string values explicitly matching the number of times the group name is specified
|
||||
in the pattern.
|
||||
May be unpredictable if the same name is used multiple times for different capture groups across multiple patterns.
|
||||
|
||||
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 strings
|
||||
(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 grpIdx int
|
||||
var grpNm string
|
||||
var names []string
|
||||
var matchStr string
|
||||
var si stringIndexer
|
||||
var matchIndices []int
|
||||
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 matched 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) == 0 || 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
|
||||
}
|
||||
names = names[1:]
|
||||
|
||||
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 == "" {
|
||||
continue
|
||||
}
|
||||
matches[grpNm] = nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
The reslice of `matchIndices` starts at 2 because they're in pairs:
|
||||
|
||||
[]int{<start>, <end>, <start>, <end>, ...}
|
||||
|
||||
and the first pair is the entire pattern match (un-resliced names[0],
|
||||
un-resliced matchIndices[0]).
|
||||
|
||||
Thus the len(matchIndices) == 2*len(names) (*should*, that is), *even* if you reslice.
|
||||
Keep in mind that since the first element of names is removed,
|
||||
we reslice matchIndices as well.
|
||||
*/
|
||||
matchIndices = matchIndices[2:]
|
||||
|
||||
tmpMap = make(map[string][]string)
|
||||
|
||||
// Note that the second index is the *upper boundary*, not a *position in the string*
|
||||
// so these indices are perfectly usable as-is as returned from the regexp methods.
|
||||
// http://golang.org/ref/spec#Slice_expressions
|
||||
for startIdx = 0; endIdx < len(matchIndices); startIdx += 2 {
|
||||
endIdx = startIdx + 2
|
||||
// This technically should never happen.
|
||||
if endIdx > len(matchIndices) {
|
||||
endIdx = len(matchIndices)
|
||||
}
|
||||
|
||||
if grpIdx >= len(names) {
|
||||
break
|
||||
}
|
||||
|
||||
si = stringIndexer{
|
||||
group: grpIdx,
|
||||
start: matchIndices[startIdx],
|
||||
end: matchIndices[endIdx-1],
|
||||
matched: true,
|
||||
nm: names[grpIdx],
|
||||
grpS: "",
|
||||
s: &matchStr,
|
||||
ptrn: r.Regexp,
|
||||
}
|
||||
grpIdx++
|
||||
|
||||
if si.nm == "" {
|
||||
// unnamed capture group
|
||||
continue
|
||||
}
|
||||
|
||||
// sets si.matched and si.grpS
|
||||
si.idxSlice(&s)
|
||||
|
||||
if !si.matched {
|
||||
if !inclNoMatch {
|
||||
continue
|
||||
}
|
||||
if _, ok = tmpMap[si.nm]; !ok {
|
||||
if !inclNoMatchStrict {
|
||||
tmpMap[si.nm] = nil
|
||||
} else {
|
||||
tmpMap[si.nm] = []string{""}
|
||||
}
|
||||
} else {
|
||||
if inclNoMatchStrict {
|
||||
tmpMap[si.nm] = append(tmpMap[si.nm], "")
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok = tmpMap[si.nm]; !ok {
|
||||
tmpMap[si.nm] = make([]string, 0)
|
||||
}
|
||||
tmpMap[si.nm] = append(tmpMap[si.nm], si.grpS)
|
||||
}
|
||||
|
||||
// This *technically* should be completely handled above.
|
||||
if inclNoMatch {
|
||||
for _, grpNm = range names {
|
||||
if grpNm == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok = tmpMap[grpNm]; !ok {
|
||||
tmpMap[grpNm] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(tmpMap) > 0 {
|
||||
matches = tmpMap
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
MapStringAll behaves exactly like [ReMap.MapString] but will "squash"/consolidate *all* found matches, not just the first occurrence,
|
||||
into the group name.
|
||||
|
||||
You likely want to use this instead of [ReMap.MapString] for multiline patterns.
|
||||
*/
|
||||
func (r *ReMap) MapStringAll(s string, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][]string) {
|
||||
|
||||
var ok bool
|
||||
var endIdx int
|
||||
var startIdx int
|
||||
var grpIdx int
|
||||
var grpNm string
|
||||
var names []string
|
||||
var matchStr string
|
||||
var si stringIndexer
|
||||
var matchIndices []int
|
||||
var allMatchIndices [][]int
|
||||
var tmpMap map[string][]string = make(map[string][]string)
|
||||
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
names = r.Regexp.SubexpNames()[:]
|
||||
allMatchIndices = r.Regexp.FindAllStringSubmatchIndex(s, -1)
|
||||
|
||||
if allMatchIndices == nil {
|
||||
// s does not match pattern at all.
|
||||
if !mustMatch {
|
||||
matches = make(map[string][]string)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if names == nil || len(names) == 0 || 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
|
||||
}
|
||||
names = names[1:]
|
||||
|
||||
if len(allMatchIndices) == 0 {
|
||||
// No matches (and thus submatches) whatsoever.
|
||||
// I think this is actually covered by the `if allMatchIndices == nil { ... }` above,
|
||||
// but this is still here for safety and efficiency - early return on no matches to iterate.
|
||||
matches = make(map[string][]string)
|
||||
if inclNoMatch {
|
||||
for _, grpNm = range names {
|
||||
if grpNm == "" {
|
||||
continue
|
||||
}
|
||||
matches[grpNm] = nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// Do *NOT* trim/reslice allMatchIndices!
|
||||
// The reslicing is done below, *inside* each matchIndices iteration!
|
||||
|
||||
tmpMap = make(map[string][]string)
|
||||
|
||||
// From here, it behaves (sort of) like ReMap.MapString.
|
||||
|
||||
// Build the strictly-paired chunk indexes and populate them.
|
||||
// We are iterating over *match sets*; matchIndices here should be analgous
|
||||
// to matchIndices in ReMap.MapString.
|
||||
for _, matchIndices = range allMatchIndices {
|
||||
|
||||
if matchIndices == nil {
|
||||
// I *think* the exception with the *All* variant here
|
||||
// is the *entire* return (allMatchIndices) is nil if there
|
||||
// aren't any matches; I can't imagine there'd be any feasible
|
||||
// way it'd insert a nil *element* for an index mapping group.
|
||||
// So just continuing here should be fine;
|
||||
// this continue SHOULD be unreachable.
|
||||
continue
|
||||
}
|
||||
|
||||
// Reslice *here*, on the particular match index group.
|
||||
// Grap the matchStr first; it's not currently *used* by anything but may in the future.
|
||||
matchStr, ok = strIdxSlicer(
|
||||
s,
|
||||
*(*[2]int)(matchIndices[0:2]),
|
||||
)
|
||||
if len(matchIndices) == 0 || len(matchIndices) == 1 {
|
||||
// No *sub*matches (capture groups) in this match, but it still matched the pattern.
|
||||
if inclNoMatch {
|
||||
for _, grpNm = range names {
|
||||
if grpNm == "" {
|
||||
continue
|
||||
}
|
||||
// We don't immediately return, though; we just stage out group names just in case.
|
||||
// That's why we use tmpMap and not matches.
|
||||
if _, ok = tmpMap[grpNm]; !ok {
|
||||
tmpMap[grpNm] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
matchIndices = matchIndices[2:]
|
||||
|
||||
// Reset from previous loop
|
||||
endIdx = 0
|
||||
grpIdx = 0
|
||||
|
||||
for startIdx = 0; endIdx < len(matchIndices); startIdx += 2 {
|
||||
endIdx = startIdx + 2
|
||||
if endIdx > len(matchIndices) {
|
||||
endIdx = len(matchIndices)
|
||||
}
|
||||
|
||||
if grpIdx >= len(names) {
|
||||
break
|
||||
}
|
||||
|
||||
si = stringIndexer{
|
||||
group: grpIdx,
|
||||
start: matchIndices[startIdx],
|
||||
end: matchIndices[endIdx-1],
|
||||
matched: true,
|
||||
nm: names[grpIdx],
|
||||
grpS: "",
|
||||
ptrn: r.Regexp,
|
||||
}
|
||||
grpIdx++
|
||||
// We do not include the entire match string here;
|
||||
// we don't need it for this. Waste of memory.
|
||||
_ = matchStr
|
||||
/*
|
||||
si.s = new(string)
|
||||
*si.s = matchStr
|
||||
*/
|
||||
|
||||
if si.nm == "" {
|
||||
// unnamed capture group
|
||||
continue
|
||||
}
|
||||
|
||||
// sets si.matched and si.grpS
|
||||
si.idxSlice(&s)
|
||||
|
||||
if !si.matched {
|
||||
if !inclNoMatch {
|
||||
continue
|
||||
}
|
||||
if _, ok = tmpMap[si.nm]; !ok {
|
||||
if !inclNoMatchStrict {
|
||||
tmpMap[si.nm] = nil
|
||||
} else {
|
||||
tmpMap[si.nm] = []string{""}
|
||||
}
|
||||
} else {
|
||||
if inclNoMatchStrict {
|
||||
tmpMap[si.nm] = append(tmpMap[si.nm], "")
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok = tmpMap[si.nm]; !ok {
|
||||
tmpMap[si.nm] = make([]string, 0)
|
||||
}
|
||||
tmpMap[si.nm] = append(tmpMap[si.nm], si.grpS)
|
||||
}
|
||||
}
|
||||
|
||||
if inclNoMatch {
|
||||
for _, grpNm = range names {
|
||||
if grpNm == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok = tmpMap[grpNm]; !ok {
|
||||
tmpMap[grpNm] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(tmpMap) > 0 {
|
||||
matches = tmpMap
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
344
remap/funcs_remap_test.go
Normal file
344
remap/funcs_remap_test.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package remap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type (
|
||||
testMatcher struct {
|
||||
Nm string
|
||||
S string
|
||||
M *ReMap
|
||||
All bool
|
||||
Expected map[string][][]byte
|
||||
ExpectedStr map[string][]string
|
||||
ParamInclNoMatch bool
|
||||
ParamInclNoMatchStrict bool
|
||||
ParamInclMustMatch bool
|
||||
}
|
||||
)
|
||||
|
||||
func TestRemap(t *testing.T) {
|
||||
|
||||
var matches map[string][][]byte
|
||||
|
||||
for midx, m := range []testMatcher{
|
||||
// 1
|
||||
testMatcher{
|
||||
Nm: "No matches",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
},
|
||||
// 2
|
||||
testMatcher{
|
||||
Nm: "Single mid match",
|
||||
S: "This contains a single match in the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match)\s+`)},
|
||||
Expected: map[string][][]byte{
|
||||
"g1": [][]byte{[]byte("match")},
|
||||
},
|
||||
},
|
||||
// 3
|
||||
testMatcher{
|
||||
Nm: "multi mid match",
|
||||
S: "This contains a single match and another match in the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another (?P<g1>match)\s+`)},
|
||||
Expected: map[string][][]byte{
|
||||
"g1": [][]byte{
|
||||
[]byte("match"),
|
||||
[]byte("match"),
|
||||
},
|
||||
},
|
||||
},
|
||||
// 4
|
||||
testMatcher{
|
||||
Nm: "line match",
|
||||
S: "This\ncontains a\nsingle\nmatch\non a dedicated line",
|
||||
M: &ReMap{regexp.MustCompile(`(?m)^(?P<g1>match)$`)},
|
||||
Expected: map[string][][]byte{
|
||||
"g1": [][]byte{
|
||||
[]byte("match"),
|
||||
},
|
||||
},
|
||||
},
|
||||
// 5
|
||||
testMatcher{
|
||||
Nm: "multiline match",
|
||||
S: "This\ncontains a\nsingle match and another\nmatch\nin the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another\s+(?P<g1>match)\s+`)},
|
||||
All: true,
|
||||
Expected: map[string][][]byte{
|
||||
"g1": [][]byte{
|
||||
[]byte("match"),
|
||||
[]byte("match"),
|
||||
},
|
||||
},
|
||||
},
|
||||
// 6
|
||||
// More closely mirrors something closer to real-life
|
||||
testMatcher{
|
||||
Nm: "mixed match",
|
||||
S: " # No longer log hits/reqs/resps to file.\n" +
|
||||
" #access_log /mnt/nginx_logs/vhost/tenant/site/access.log main;\n" +
|
||||
" #error_log /mnt/nginx_logs/vhost/tenant/site/error.log;\n" +
|
||||
" access_log off;\n" +
|
||||
" error_log /dev/null;\n\n" +
|
||||
" ssl_certificate /etc/nginx/tls/crt/tenant.pem;\n" +
|
||||
" ssl_certificate_key /etc/nginx/tls/key/tenant.pem;\n\n",
|
||||
M: &ReMap{regexp.MustCompile(`(?m)^\s*(?:error|access)_log\s+(?P<logpath>.+);\s*$`)},
|
||||
All: true,
|
||||
Expected: map[string][][]byte{
|
||||
"logpath": [][]byte{
|
||||
[]byte("off"),
|
||||
[]byte("/dev/null"),
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
if m.All {
|
||||
matches = m.M.MapAll([]byte(m.S), false, false, false)
|
||||
} else {
|
||||
matches = m.M.Map([]byte(m.S), false, false, false)
|
||||
}
|
||||
t.Logf(
|
||||
"#%d:\n\tsrc:\t'%s'\n\tptrn:\t'%s'\n\tmatch:\t%s\n",
|
||||
midx+1,
|
||||
m.S,
|
||||
m.M.Regexp.String(),
|
||||
testBmapToStrMap(matches),
|
||||
)
|
||||
if !reflect.DeepEqual(matches, m.Expected) {
|
||||
t.Fatalf("Case #%d (\"%s\"): expected '%#v' != received '%#v'", midx+1, m.Nm, m.Expected, matches)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRemapParams(t *testing.T) {
|
||||
|
||||
var matches map[string][][]byte
|
||||
|
||||
for midx, m := range []testMatcher{
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
ParamInclNoMatch: false,
|
||||
ParamInclNoMatchStrict: false,
|
||||
ParamInclMustMatch: false,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
ParamInclNoMatch: false,
|
||||
ParamInclNoMatchStrict: true,
|
||||
ParamInclMustMatch: false,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
ParamInclNoMatch: false,
|
||||
ParamInclNoMatchStrict: true,
|
||||
ParamInclMustMatch: true,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: nil,
|
||||
ParamInclNoMatch: false,
|
||||
ParamInclNoMatchStrict: false,
|
||||
ParamInclMustMatch: true,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: make(map[string][][]byte),
|
||||
ParamInclNoMatch: true,
|
||||
ParamInclNoMatchStrict: false,
|
||||
ParamInclMustMatch: false,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: make(map[string][][]byte),
|
||||
ParamInclNoMatch: true,
|
||||
ParamInclNoMatchStrict: true,
|
||||
ParamInclMustMatch: false,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: make(map[string][][]byte),
|
||||
ParamInclNoMatch: true,
|
||||
ParamInclNoMatchStrict: true,
|
||||
ParamInclMustMatch: true,
|
||||
},
|
||||
testMatcher{
|
||||
Nm: "",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
Expected: make(map[string][][]byte),
|
||||
ParamInclNoMatch: true,
|
||||
ParamInclNoMatchStrict: false,
|
||||
ParamInclMustMatch: true,
|
||||
},
|
||||
} {
|
||||
if m.All {
|
||||
matches = m.M.MapAll([]byte(m.S), m.ParamInclNoMatch, m.ParamInclNoMatchStrict, m.ParamInclMustMatch)
|
||||
} else {
|
||||
matches = m.M.Map([]byte(m.S), m.ParamInclNoMatch, m.ParamInclNoMatchStrict, m.ParamInclMustMatch)
|
||||
}
|
||||
t.Logf(
|
||||
"%d: %v/%v/%v: %#v\n",
|
||||
midx+1, m.ParamInclNoMatch, m.ParamInclNoMatchStrict, m.ParamInclMustMatch, matches,
|
||||
)
|
||||
if !reflect.DeepEqual(matches, m.Expected) {
|
||||
t.Fatalf("Case #%d (\"%s\"): '%#v' != '%#v'", midx+1, m.Nm, m.ExpectedStr, matches)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRemapString(t *testing.T) {
|
||||
|
||||
var matches map[string][]string
|
||||
|
||||
for midx, m := range []testMatcher{
|
||||
// 1
|
||||
testMatcher{
|
||||
Nm: "No matches",
|
||||
S: "this is a test",
|
||||
M: &ReMap{regexp.MustCompile(``)},
|
||||
ExpectedStr: nil,
|
||||
},
|
||||
// 2
|
||||
testMatcher{
|
||||
Nm: "Single mid match",
|
||||
S: "This contains a single match in the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match)\s+`)},
|
||||
ExpectedStr: map[string][]string{
|
||||
"g1": []string{"match"},
|
||||
},
|
||||
},
|
||||
// 3
|
||||
testMatcher{
|
||||
Nm: "multi mid match",
|
||||
S: "This contains a single match and another match in the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another (?P<g1>match)\s+`)},
|
||||
ExpectedStr: map[string][]string{
|
||||
"g1": []string{
|
||||
"match",
|
||||
"match",
|
||||
},
|
||||
},
|
||||
},
|
||||
// 4
|
||||
testMatcher{
|
||||
Nm: "line match",
|
||||
S: "This\ncontains a\nsingle\nmatch\non a dedicated line",
|
||||
M: &ReMap{regexp.MustCompile(`(?m)^(?P<g1>match)$`)},
|
||||
ExpectedStr: map[string][]string{
|
||||
"g1": []string{
|
||||
"match",
|
||||
},
|
||||
},
|
||||
},
|
||||
// 5
|
||||
testMatcher{
|
||||
Nm: "multiline match",
|
||||
S: "This\ncontains a\nsingle match and another\nmatch\nin the middle of a string",
|
||||
M: &ReMap{regexp.MustCompile(`\s+(?P<g1>match) and another\s+(?P<g1>match)\s+`)},
|
||||
All: true,
|
||||
ExpectedStr: map[string][]string{
|
||||
"g1": []string{
|
||||
"match",
|
||||
"match",
|
||||
},
|
||||
},
|
||||
},
|
||||
// 6
|
||||
// More closely mirrors something closer to real-life
|
||||
testMatcher{
|
||||
Nm: "mixed match",
|
||||
S: " # No longer log hits/reqs/resps to file.\n" +
|
||||
" #access_log /mnt/nginx_logs/vhost/tenant/site/access.log main;\n" +
|
||||
" #error_log /mnt/nginx_logs/vhost/tenant/site/error.log;\n" +
|
||||
" access_log off;\n" +
|
||||
" error_log /dev/null;\n\n" +
|
||||
" ssl_certificate /etc/nginx/tls/crt/tenant.pem;\n" +
|
||||
" ssl_certificate_key /etc/nginx/tls/key/tenant.pem;\n\n",
|
||||
M: &ReMap{regexp.MustCompile(`(?m)^\s*(?:error|access)_log\s+(?P<logpath>.+);\s*$`)},
|
||||
All: true,
|
||||
ExpectedStr: map[string][]string{
|
||||
"logpath": []string{
|
||||
"off",
|
||||
"/dev/null",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
if m.All {
|
||||
matches = m.M.MapStringAll(m.S, false, false, false)
|
||||
} else {
|
||||
matches = m.M.MapString(m.S, false, false, false)
|
||||
}
|
||||
t.Logf(
|
||||
"#%d:\n\tsrc:\t'%s'\n\tptrn:\t'%s'\n\tmatch:\t%s\n",
|
||||
midx+1,
|
||||
m.S,
|
||||
m.M.Regexp.String(),
|
||||
testSmapToStrMap(matches),
|
||||
)
|
||||
if !reflect.DeepEqual(matches, m.ExpectedStr) {
|
||||
t.Fatalf("Case #%d (\"%s\"): '%#v' != '%#v'", midx+1, m.Nm, m.ExpectedStr, matches)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testBmapToStrMap(bmap map[string][][]byte) (s string) {
|
||||
|
||||
if bmap == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = "\n"
|
||||
for k, v := range bmap {
|
||||
s += fmt.Sprintf("\t%s\n", k)
|
||||
for _, i := range v {
|
||||
s += fmt.Sprintf("\t\t%s\n", string(i))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func testSmapToStrMap(smap map[string][]string) (s string) {
|
||||
|
||||
if smap == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s = "\n"
|
||||
for k, v := range smap {
|
||||
s += fmt.Sprintf("\t%s\n", k)
|
||||
for _, i := range v {
|
||||
s += fmt.Sprintf("\t\t%s\n", i)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
34
remap/funcs_stringindexer.go
Normal file
34
remap/funcs_stringindexer.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package remap
|
||||
|
||||
// idx returns []int{s.start, s.end}.
|
||||
func (s *stringIndexer) idx() (i []int) {
|
||||
return []int{s.start, s.end}
|
||||
}
|
||||
|
||||
// idxStrict returns [2]int{s.start, s.end}.
|
||||
func (s *stringIndexer) idxStrict() (i [2]int) {
|
||||
return [2]int{s.start, s.end}
|
||||
}
|
||||
|
||||
/*
|
||||
idxSlice populates s.grpS using s.start and s.end.
|
||||
|
||||
If str is nil, it will use s.s.
|
||||
If str is nil and s.s is nil, it will panic with [ErrNoStr].
|
||||
|
||||
If the pattern does not match (s.start < 0 or s.end < 0),
|
||||
s.matched will be set to false (otherwise true).
|
||||
*/
|
||||
func (s *stringIndexer) idxSlice(str *string) {
|
||||
|
||||
if str == nil {
|
||||
if s.s == nil {
|
||||
panic(ErrNoStr)
|
||||
}
|
||||
str = s.s
|
||||
}
|
||||
|
||||
s.grpS, s.matched = strIdxSlicer(*str, s.idxStrict())
|
||||
|
||||
return
|
||||
}
|
||||
68
remap/types.go
Normal file
68
remap/types.go
Normal file
@@ -0,0 +1,68 @@
|
||||
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
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
stringIndexer struct {
|
||||
// group is the capture group index for this match.
|
||||
group int
|
||||
// start is the string index (from the original string) where the matched group starts
|
||||
start int
|
||||
// end is the string index where the matched group ends
|
||||
end int
|
||||
/*
|
||||
matched indicates if explicitly no match was found.
|
||||
(This is normally indeterminate with string regex returns,
|
||||
as e.g. `(?P<mygrp>\s*)`, `(?P<mygrp>(?:somestring)?)`, etc. all can be a *matched* "".)
|
||||
|
||||
If grpS == "" and matched == true, it DID match an empty string.
|
||||
If grpS == "" and matched == false, it DID NOT MATCH the pattern.
|
||||
If grpS != "", matched can be completely disregarded.
|
||||
*/
|
||||
matched bool
|
||||
// nm is the match group name.
|
||||
nm string
|
||||
/*
|
||||
grpS is the actual group-matched *substring*.
|
||||
|
||||
It will ALWAYS be either:
|
||||
|
||||
* the entirety of s
|
||||
* a substring of s
|
||||
* an empty string
|
||||
|
||||
it will never, and cannot be, a SUPERset of s.
|
||||
it may not always be included/populated to save on memory.
|
||||
*/
|
||||
grpS string
|
||||
/*
|
||||
s is the *entire* MATCHED (sub)string.
|
||||
It may not always be populated if not needed to save memory.
|
||||
*/
|
||||
s *string
|
||||
// ptrn is the pattern applied to s.
|
||||
ptrn *regexp.Regexp
|
||||
}
|
||||
)
|
||||
5
stringsx/TODO
Normal file
5
stringsx/TODO
Normal file
@@ -0,0 +1,5 @@
|
||||
- Banner struct, with .Format(s string) method
|
||||
-- draw border around multiline s
|
||||
-- i have a version in python somewhere that does this, should dig that up
|
||||
|
||||
- create bytesx package that duplicates the functions here?
|
||||
6
stringsx/consts.go
Normal file
6
stringsx/consts.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package stringsx
|
||||
|
||||
const (
|
||||
// DefMaskStr is the string used as the default maskStr if left empty in [Redact].
|
||||
DefMaskStr string = "***"
|
||||
)
|
||||
17
stringsx/doc.go
Normal file
17
stringsx/doc.go
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Package stringsx aims to extend functionality of the stdlib [strings] module.
|
||||
|
||||
Note that if you need a way of mimicking Bash's shell quoting rules, [desertbit/shlex] or [buildkite/shellwords]
|
||||
would be better options than [google/shlex] but this package does not attempt to reproduce
|
||||
any of that functionality.
|
||||
|
||||
For line splitting, one should use [muesli/reflow/wordwrap].
|
||||
Likewise for indentation, one should use [muesli/reflow/indent].
|
||||
|
||||
[desertbit/shlex]: https://pkg.go.dev/github.com/desertbit/go-shlex
|
||||
[buildkite/shellwords]: https://pkg.go.dev/github.com/buildkite/shellwords
|
||||
[google/shlex]: https://pkg.go.dev/github.com/google/shlex
|
||||
[muesli/reflow/wordwrap]: https://pkg.go.dev/github.com/muesli/reflow/wordwrap
|
||||
[muesli/reflow/indent]: https://pkg.go.dev/github.com/muesli/reflow/indent
|
||||
*/
|
||||
package stringsx
|
||||
326
stringsx/funcs.go
Normal file
326
stringsx/funcs.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package stringsx
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`strings`
|
||||
`unicode`
|
||||
)
|
||||
|
||||
/*
|
||||
LenSplit formats string `s` to break at, at most, every `width` characters.
|
||||
|
||||
Any existing newlines (e.g. \r\n) will be removed during a string/
|
||||
substring/line's length calculation. (e.g. `foobarbaz\n` and `foobarbaz\r\n` are
|
||||
both considered to be lines of length 9, not 10 and 11 respectively).
|
||||
|
||||
This also means that any newlines (\n or \r\n) are inherently removed from
|
||||
`out` (even if included in `wordWrap`; see below).
|
||||
|
||||
Note that if `s` is multiline (already contains newlines), they will be respected
|
||||
as-is - that is, if a line ends with less than `width` chars and then has a newline,
|
||||
it will be preserved as an empty element. That is to say:
|
||||
|
||||
"foo\nbar\n\n" → []string{"foo", "bar", ""}
|
||||
"foo\n\nbar\n" → []string{"foo", "", "bar"}
|
||||
|
||||
This splitter is particularly simple. If you need wordwrapping, it should be done
|
||||
with e.g. [github.com/muesli/reflow/wordwrap].
|
||||
*/
|
||||
func LenSplit(s string, width uint) (out []string) {
|
||||
|
||||
var end int
|
||||
var line string
|
||||
var lineRunes []rune
|
||||
|
||||
if width == 0 {
|
||||
out = []string{s}
|
||||
return
|
||||
}
|
||||
|
||||
for line = range strings.Lines(s) {
|
||||
line = strings.TrimRight(line, "\n")
|
||||
line = strings.TrimRight(line, "\r")
|
||||
|
||||
lineRunes = []rune(line)
|
||||
|
||||
if uint(len(lineRunes)) <= width {
|
||||
out = append(out, line)
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < len(lineRunes); i += int(width) {
|
||||
end = i + int(width)
|
||||
if end > len(lineRunes) {
|
||||
end = len(lineRunes)
|
||||
}
|
||||
out = append(out, string(lineRunes[i:end]))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
LenSplitStr wraps [LenSplit] but recombines into a new string with newlines.
|
||||
|
||||
It's mostly just a convenience wrapper.
|
||||
|
||||
All arguments remain the same as in [LenSplit] with an additional one,
|
||||
`winNewLine`, which if true will use \r\n as the newline instead of \n.
|
||||
*/
|
||||
func LenSplitStr(s string, width uint, winNewline bool) (out string) {
|
||||
|
||||
var outSl []string = LenSplit(s, width)
|
||||
|
||||
if winNewline {
|
||||
out = strings.Join(outSl, "\r\n")
|
||||
} else {
|
||||
out = strings.Join(outSl, "\n")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Pad pads each element in `s` to length `width` using `pad`.
|
||||
If `pad` is empty, a single space (0x20) will be assumed.
|
||||
Note that `width` operates on rune size, not byte size.
|
||||
(In ASCII, they will be the same size.)
|
||||
|
||||
If a line in `s` is greater than or equal to `width`,
|
||||
no padding will be performed.
|
||||
|
||||
If `leftPad` is true, padding will be applied to the "left" (beginning")
|
||||
of each element instead of the "right" ("end").
|
||||
*/
|
||||
func Pad(s []string, width uint, pad string, leftPad bool) (out []string) {
|
||||
|
||||
var idx int
|
||||
var padIdx int
|
||||
var runeIdx int
|
||||
var padLen uint
|
||||
var elem string
|
||||
var unpadLen uint
|
||||
var tmpPadLen int
|
||||
var padRunes []rune
|
||||
var tmpPad []rune
|
||||
|
||||
if width == 0 {
|
||||
out = s
|
||||
return
|
||||
}
|
||||
|
||||
out = make([]string, len(s))
|
||||
|
||||
// Easy; supported directly in fmt.
|
||||
if pad == "" {
|
||||
for idx, elem = range s {
|
||||
if leftPad {
|
||||
out[idx] = fmt.Sprintf("%*s", width, elem)
|
||||
} else {
|
||||
out[idx] = fmt.Sprintf("%-*s", width, elem)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// This gets a little more tricky.
|
||||
padRunes = []rune(pad)
|
||||
padLen = uint(len(padRunes))
|
||||
for idx, elem = range s {
|
||||
// First we need to know the number of runes in elem.
|
||||
unpadLen = uint(len([]rune(elem)))
|
||||
// If it's more than/equal to width, as-is.
|
||||
if unpadLen >= width {
|
||||
out[idx] = elem
|
||||
} else {
|
||||
// Otherwise, we need to construct/calculate a pad.
|
||||
if (width-unpadLen)%padLen == 0 {
|
||||
// Also easy enough.
|
||||
if leftPad {
|
||||
out[idx] = fmt.Sprintf("%s%s", strings.Repeat(pad, int((width-unpadLen)/padLen)), elem)
|
||||
} else {
|
||||
out[idx] = fmt.Sprintf("%s%s", elem, strings.Repeat(pad, int((width-unpadLen)/padLen)))
|
||||
}
|
||||
} else {
|
||||
// This is where it gets a little hairy.
|
||||
tmpPad = []rune{}
|
||||
tmpPadLen = int(width - unpadLen)
|
||||
idx = 0
|
||||
padIdx = 0
|
||||
for runeIdx = range tmpPadLen {
|
||||
tmpPad[runeIdx] = padRunes[padIdx]
|
||||
if uint(padIdx) >= padLen {
|
||||
padIdx = 0
|
||||
} else {
|
||||
padIdx++
|
||||
}
|
||||
runeIdx++
|
||||
}
|
||||
if leftPad {
|
||||
out[idx] = fmt.Sprintf("%s%s", string(tmpPad), elem)
|
||||
} else {
|
||||
out[idx] = fmt.Sprintf("%s%s", elem, string(tmpPad))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Redact provides a "masked" version of string s (e.g. `my_terrible_password` -> `my****************rd`).
|
||||
|
||||
maskStr is the character or sequence of characters
|
||||
to repeat for every masked character of s.
|
||||
If an empty string, the default [DefMaskStr] will be used.
|
||||
(maskStr does not need to be a single character.
|
||||
It is recommended to use a multi-char mask to help obfuscate a string's length.)
|
||||
|
||||
leading specifies the number of leading characters of s to leave *unmasked*.
|
||||
If 0, no leading characters will be unmasked.
|
||||
|
||||
trailing specifies the number of trailing characters of s to leave *unmasked*.
|
||||
if 0, no trailing characters will be unmasked.
|
||||
|
||||
newlines, if true, will preserve newline characters - otherwise
|
||||
they will be treated as regular characters.
|
||||
|
||||
As a safety precaution, if:
|
||||
|
||||
len(s) <= (leading + trailing)
|
||||
|
||||
then the entire string will be *masked* and no unmasking will be performed.
|
||||
|
||||
Note that this DOES NOT do a string *replace*, it provides a masked version of `s` itself.
|
||||
Wrap Redact with [strings.ReplaceAll] if you want to replace a certain value with a masked one.
|
||||
*/
|
||||
func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted string) {
|
||||
|
||||
var nl string
|
||||
var numMasked int
|
||||
var sb strings.Builder
|
||||
var endIdx int = int(leading)
|
||||
|
||||
// This condition functionally won't do anything, so just return the input as-is.
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if maskStr == "" {
|
||||
maskStr = DefMaskStr
|
||||
}
|
||||
|
||||
if newlines {
|
||||
for line := range strings.Lines(s) {
|
||||
nl = getNewLine(line)
|
||||
sb.WriteString(
|
||||
Redact(
|
||||
strings.TrimSuffix(line, nl), maskStr, leading, trailing, false,
|
||||
),
|
||||
)
|
||||
sb.WriteString(nl)
|
||||
}
|
||||
} else {
|
||||
if len(s) <= int(leading+trailing) {
|
||||
redacted = strings.Repeat(maskStr, len(s))
|
||||
return
|
||||
}
|
||||
|
||||
if leading == 0 && trailing == 0 {
|
||||
redacted = strings.Repeat(maskStr, len(s))
|
||||
return
|
||||
}
|
||||
|
||||
numMasked = len(s) - int(leading+trailing)
|
||||
endIdx = endIdx + numMasked
|
||||
|
||||
if leading > 0 {
|
||||
sb.WriteString(s[:int(leading)])
|
||||
}
|
||||
|
||||
sb.WriteString(strings.Repeat(maskStr, numMasked))
|
||||
|
||||
if trailing > 0 {
|
||||
sb.WriteString(s[endIdx:])
|
||||
}
|
||||
}
|
||||
|
||||
redacted = sb.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
|
||||
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
|
||||
The first encountered linebreak (`\n` vs. `\r\n`) are assumed to be
|
||||
the canonical linebreak for the rest of s.
|
||||
|
||||
left, if true, performs a [TrimSpaceLeft] on each line (retaining the newline).
|
||||
|
||||
right, if true, performs a [TrimSpaceRight] on each line (retaining the newline).
|
||||
*/
|
||||
func TrimLines(s string, left, right bool) (trimmed string) {
|
||||
|
||||
var sl string
|
||||
var nl string
|
||||
var sb strings.Builder
|
||||
|
||||
// These conditions functionally won't do anything, so just return the input as-is.
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
if !left && !right {
|
||||
trimmed = s
|
||||
return
|
||||
}
|
||||
|
||||
for line := range strings.Lines(s) {
|
||||
nl = getNewLine(line)
|
||||
sl = strings.TrimSuffix(line, nl)
|
||||
if left && right {
|
||||
sl = strings.TrimSpace(sl)
|
||||
} else if left {
|
||||
sl = TrimSpaceLeft(sl)
|
||||
} else if right {
|
||||
sl = TrimSpaceRight(sl)
|
||||
}
|
||||
sb.WriteString(sl + nl)
|
||||
}
|
||||
|
||||
trimmed = sb.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TrimSpaceLeft is like [strings.TrimSpace] but only removes leading whitespace from string `s`.
|
||||
func TrimSpaceLeft(s string) (trimmed string) {
|
||||
|
||||
trimmed = strings.TrimLeftFunc(s, unicode.IsSpace)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TrimSpaceRight is like [strings.TrimSpace] but only removes trailing whitespace from string s.
|
||||
*/
|
||||
func TrimSpaceRight(s string) (trimmed string) {
|
||||
|
||||
trimmed = strings.TrimRightFunc(s, unicode.IsSpace)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getNewLine is too unpredictable/nuanced to be used as part of a public API promise so it isn't exported.
|
||||
func getNewLine(s string) (nl string) {
|
||||
|
||||
if strings.HasSuffix(s, "\r\n") {
|
||||
nl = "\r\n"
|
||||
} else if strings.HasSuffix(s, "\n") {
|
||||
nl = "\n"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
344
stringsx/funcs_test.go
Normal file
344
stringsx/funcs_test.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package stringsx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type (
|
||||
testIndentSet struct {
|
||||
name string
|
||||
orig string
|
||||
indent string
|
||||
lvl uint
|
||||
ws bool
|
||||
empty bool
|
||||
tgt string
|
||||
}
|
||||
testRedactSet struct {
|
||||
name string
|
||||
orig string
|
||||
leading uint
|
||||
trailing uint
|
||||
tgt string
|
||||
newline bool
|
||||
mask string // defaults to DefMaskStr.
|
||||
}
|
||||
testTrimLinesSet struct {
|
||||
name string
|
||||
orig string
|
||||
left bool
|
||||
right bool
|
||||
tgt string
|
||||
}
|
||||
testTrimSet struct {
|
||||
name string
|
||||
orig string
|
||||
tgt string
|
||||
}
|
||||
)
|
||||
|
||||
func TestRedact(t *testing.T) {
|
||||
|
||||
var out string
|
||||
var tests []testRedactSet = []testRedactSet{
|
||||
testRedactSet{
|
||||
name: "empty in, empty out",
|
||||
orig: "",
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
tgt: "",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "standard",
|
||||
orig: "password",
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
tgt: "************************",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "standard with newline",
|
||||
orig: "pass\nword",
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
tgt: "************\n************",
|
||||
newline: true,
|
||||
},
|
||||
testRedactSet{
|
||||
name: "standard with Windows newline",
|
||||
orig: "pass\r\nword",
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
tgt: "************\r\n************",
|
||||
newline: true,
|
||||
},
|
||||
testRedactSet{
|
||||
name: "standard with newline without newlines",
|
||||
orig: "pass\nword",
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
tgt: "***************************",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "single leading",
|
||||
orig: "password",
|
||||
leading: 1,
|
||||
trailing: 0,
|
||||
tgt: "p*********************",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "single trailing",
|
||||
orig: "password",
|
||||
leading: 0,
|
||||
trailing: 1,
|
||||
tgt: "*********************d",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "three leading",
|
||||
orig: "password",
|
||||
leading: 3,
|
||||
trailing: 0,
|
||||
tgt: "pas***************",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "three trailing",
|
||||
orig: "password",
|
||||
leading: 0,
|
||||
trailing: 3,
|
||||
tgt: "***************ord",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "three leading and trailing",
|
||||
orig: "password",
|
||||
leading: 3,
|
||||
trailing: 3,
|
||||
tgt: "pas******ord",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "unmask overflow leading",
|
||||
orig: "password",
|
||||
leading: 5,
|
||||
trailing: 4,
|
||||
tgt: "************************",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "unmask overflow trailing",
|
||||
orig: "password",
|
||||
leading: 4,
|
||||
trailing: 5,
|
||||
tgt: "************************",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "single mask",
|
||||
orig: "password",
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
tgt: "********",
|
||||
mask: "*",
|
||||
},
|
||||
testRedactSet{
|
||||
name: "standard trailing newline with newlines",
|
||||
orig: "password\n",
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
tgt: "************************\n",
|
||||
newline: true,
|
||||
},
|
||||
testRedactSet{
|
||||
name: "standard trailing newline without newlines",
|
||||
orig: "password\n",
|
||||
leading: 0,
|
||||
trailing: 0,
|
||||
tgt: "***************************",
|
||||
},
|
||||
}
|
||||
|
||||
for idx, ts := range tests {
|
||||
out = Redact(ts.orig, ts.mask, ts.leading, ts.trailing, ts.newline)
|
||||
if out == ts.tgt {
|
||||
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
|
||||
} else {
|
||||
t.Errorf(
|
||||
"[%d] FAIL (%s): %#v (len %d):\n"+
|
||||
"\t\t\texpected (len %d): %#v\n"+
|
||||
"\t\t\tgot (len %d): %#v\n"+
|
||||
"\t\t%#v",
|
||||
idx, ts.name, ts.orig, len(ts.orig),
|
||||
len(ts.tgt), ts.tgt,
|
||||
len(out), out,
|
||||
ts,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimLines(t *testing.T) {
|
||||
|
||||
var out string
|
||||
var tests []testTrimLinesSet = []testTrimLinesSet{
|
||||
testTrimLinesSet{
|
||||
name: "none",
|
||||
orig: " foo \n bar \n baz ",
|
||||
left: false,
|
||||
right: false,
|
||||
tgt: " foo \n bar \n baz ",
|
||||
},
|
||||
testTrimLinesSet{
|
||||
name: "standard",
|
||||
orig: " foo \n bar \n baz ",
|
||||
left: true,
|
||||
right: true,
|
||||
tgt: "foo\nbar\nbaz",
|
||||
},
|
||||
testTrimLinesSet{
|
||||
name: "left only",
|
||||
orig: " foo \n bar \n baz ",
|
||||
left: true,
|
||||
right: false,
|
||||
tgt: "foo \nbar \nbaz ",
|
||||
},
|
||||
testTrimLinesSet{
|
||||
name: "right only",
|
||||
orig: " foo \n bar \n baz ",
|
||||
left: false,
|
||||
right: true,
|
||||
tgt: " foo\n bar\n baz",
|
||||
},
|
||||
testTrimLinesSet{
|
||||
name: "standard, trailing newline",
|
||||
orig: " foo \n bar \n baz \n",
|
||||
left: true,
|
||||
right: true,
|
||||
tgt: "foo\nbar\nbaz\n",
|
||||
},
|
||||
testTrimLinesSet{
|
||||
name: "left only, trailing newline",
|
||||
orig: " foo \n bar \n baz \n",
|
||||
left: true,
|
||||
right: false,
|
||||
tgt: "foo \nbar \nbaz \n",
|
||||
},
|
||||
testTrimLinesSet{
|
||||
name: "right only, trailing newline",
|
||||
orig: " foo \n bar \n baz \n",
|
||||
left: false,
|
||||
right: true,
|
||||
tgt: " foo\n bar\n baz\n",
|
||||
},
|
||||
// Since there's no "non-space" boundary, both of these condition tests do the same thing.
|
||||
testTrimLinesSet{
|
||||
name: "left only, trailing newline and ws",
|
||||
orig: " foo \n bar \n baz \n ",
|
||||
left: true,
|
||||
right: false,
|
||||
tgt: "foo \nbar \nbaz \n",
|
||||
},
|
||||
testTrimLinesSet{
|
||||
name: "right only, trailing newline and ws",
|
||||
orig: " foo \n bar \n baz \n ",
|
||||
left: false,
|
||||
right: true,
|
||||
tgt: " foo\n bar\n baz\n",
|
||||
},
|
||||
}
|
||||
|
||||
for idx, ts := range tests {
|
||||
out = TrimLines(ts.orig, ts.left, ts.right)
|
||||
if out == ts.tgt {
|
||||
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
|
||||
} else {
|
||||
t.Errorf(
|
||||
"[%d] FAIL (%s): %#v (len %d):\n"+
|
||||
"\t\t\texpected (len %d): %#v\n"+
|
||||
"\t\t\tgot (len %d): %#v\n"+
|
||||
"\t\t%#v",
|
||||
idx, ts.name, ts.orig, len(ts.orig),
|
||||
len(ts.tgt), ts.tgt,
|
||||
len(out), out,
|
||||
ts,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTrimSpaceLeft(t *testing.T) {
|
||||
|
||||
var out string
|
||||
var tests []testTrimSet = []testTrimSet{
|
||||
testTrimSet{
|
||||
name: "standard",
|
||||
orig: " foo ",
|
||||
tgt: "foo ",
|
||||
},
|
||||
testTrimSet{
|
||||
name: "tabs",
|
||||
orig: "\t\tfoo\t\t",
|
||||
tgt: "foo\t\t",
|
||||
},
|
||||
testTrimSet{
|
||||
name: "newlines",
|
||||
orig: "\n\nfoo\n\n",
|
||||
tgt: "foo\n\n",
|
||||
},
|
||||
}
|
||||
|
||||
for idx, ts := range tests {
|
||||
out = TrimSpaceLeft(ts.orig)
|
||||
if out == ts.tgt {
|
||||
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
|
||||
} else {
|
||||
t.Errorf(
|
||||
"[%d] FAIL (%s): %#v (len %d):\n"+
|
||||
"\t\t\texpected (len %d): %#v\n"+
|
||||
"\t\t\tgot (len %d): %#v\n"+
|
||||
"\t\t%#v",
|
||||
idx, ts.name, ts.orig, len(ts.orig),
|
||||
len(ts.tgt), ts.tgt,
|
||||
len(out), out,
|
||||
ts,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTrimSpaceRight(t *testing.T) {
|
||||
|
||||
var out string
|
||||
var tests []testTrimSet = []testTrimSet{
|
||||
testTrimSet{
|
||||
name: "standard",
|
||||
orig: " foo ",
|
||||
tgt: " foo",
|
||||
},
|
||||
testTrimSet{
|
||||
name: "tabs",
|
||||
orig: "\t\tfoo\t\t",
|
||||
tgt: "\t\tfoo",
|
||||
},
|
||||
testTrimSet{
|
||||
name: "newlines",
|
||||
orig: "\n\nfoo\n\n",
|
||||
tgt: "\n\nfoo",
|
||||
},
|
||||
}
|
||||
|
||||
for idx, ts := range tests {
|
||||
out = TrimSpaceRight(ts.orig)
|
||||
if out == ts.tgt {
|
||||
t.Logf("[%d] OK (%s): %#v: got %#v", idx, ts.name, ts.orig, out)
|
||||
} else {
|
||||
t.Errorf(
|
||||
"[%d] FAIL (%s): %#v (len %d):\n"+
|
||||
"\t\t\texpected (len %d): %#v\n"+
|
||||
"\t\t\tgot (len %d): %#v\n"+
|
||||
"\t\t%#v",
|
||||
idx, ts.name, ts.orig, len(ts.orig),
|
||||
len(ts.tgt), ts.tgt,
|
||||
len(out), out,
|
||||
ts,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
5
structutils/consts.go
Normal file
5
structutils/consts.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package structutils
|
||||
|
||||
const (
|
||||
TagMapTrim tagMapOpt = iota
|
||||
)
|
||||
362
structutils/funcs.go
Normal file
362
structutils/funcs.go
Normal file
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
GoUtils - a library to assist with various Golang-related functions
|
||||
Copyright (C) 2020 Brent Saner
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package structutils
|
||||
|
||||
import (
|
||||
`reflect`
|
||||
`strings`
|
||||
)
|
||||
|
||||
/*
|
||||
TagToBoolMap takes struct field `field` and tag name `tagName`,
|
||||
optionally with options `opts`, and returns a map of the tag values.
|
||||
The tag value string is assumed to be in the form of:
|
||||
option[,option,option...]
|
||||
and returns a map[string]bool (map[option]true).
|
||||
|
||||
If field does not have tag tagName, m will be nil.
|
||||
|
||||
See the TagMap* constants for opts.
|
||||
*/
|
||||
func TagToBoolMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (m map[string]bool) {
|
||||
|
||||
var s string
|
||||
var optSplit []string
|
||||
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
|
||||
|
||||
s = field.Tag.Get(tagName)
|
||||
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
optSplit = strings.Split(s, ",")
|
||||
if optSplit == nil || len(optSplit) == 0 {
|
||||
return
|
||||
}
|
||||
m = make(map[string]bool)
|
||||
for _, o := range optSplit {
|
||||
if tagOpts[TagMapTrim] {
|
||||
o = strings.TrimSpace(o)
|
||||
}
|
||||
m[o] = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TagToBoolMapWithValue is like TagToBoolMap but additionally assumes the first value is an "identifier".
|
||||
The tag value string is assumed to be in the form of:
|
||||
value,option[,option,option...]
|
||||
and returns a map[string]bool (map[option]true) with the value.
|
||||
*/
|
||||
func TagToBoolMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, m map[string]bool) {
|
||||
|
||||
var s string
|
||||
var optSplit []string
|
||||
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
|
||||
|
||||
s = field.Tag.Get(tagName)
|
||||
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
optSplit = strings.Split(s, ",")
|
||||
if optSplit == nil || len(optSplit) == 0 {
|
||||
return
|
||||
}
|
||||
m = make(map[string]bool)
|
||||
for idx, o := range optSplit {
|
||||
if idx == 0 {
|
||||
if tagOpts[TagMapTrim] {
|
||||
o = strings.TrimSpace(o)
|
||||
}
|
||||
value = o
|
||||
continue
|
||||
}
|
||||
if tagOpts[TagMapTrim] {
|
||||
o = strings.TrimSpace(o)
|
||||
}
|
||||
m[o] = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TagToMixedMap combines TagToBoolMap and TagToStringMap.
|
||||
It takes struct field `field` and tag name `tagName`,
|
||||
and returns all single-value options in mapBool, and all key/value options in mapString.
|
||||
|
||||
If field does not have tag tagName, m will be nil.
|
||||
|
||||
See the TagMap* constants for opts.
|
||||
*/
|
||||
func TagToMixedMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (mapBool map[string]bool, mapString map[string]string) {
|
||||
|
||||
var s string
|
||||
var valStr string
|
||||
var split []string
|
||||
var kvSplit []string
|
||||
var valSplit []string
|
||||
var k string
|
||||
var v string
|
||||
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
|
||||
|
||||
s = field.Tag.Get(tagName)
|
||||
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
split = strings.Split(s, ",")
|
||||
if split == nil || len(split) == 0 {
|
||||
return
|
||||
}
|
||||
mapBool = make(map[string]bool)
|
||||
mapString = make(map[string]string)
|
||||
for _, valStr = range split {
|
||||
if strings.Contains(valStr, "=") {
|
||||
kvSplit = strings.SplitN(valStr, "=", 2)
|
||||
if kvSplit == nil || len(kvSplit) == 0 {
|
||||
continue
|
||||
}
|
||||
k = valSplit[0]
|
||||
switch len(valSplit) {
|
||||
case 1:
|
||||
v = ""
|
||||
case 2:
|
||||
v = kvSplit[1]
|
||||
}
|
||||
if tagOpts[TagMapTrim] {
|
||||
k = strings.TrimSpace(k)
|
||||
v = strings.TrimSpace(v)
|
||||
}
|
||||
mapString[k] = v
|
||||
} else {
|
||||
if tagOpts[TagMapTrim] {
|
||||
valStr = strings.TrimSpace(valStr)
|
||||
}
|
||||
mapBool[valStr] = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TagToMixedMapWithValue combines TagToBoolMapWithValue and TagToStringMapWithValue.
|
||||
It takes struct field `field` and tag name `tagName`,
|
||||
and returns all single-value options in mapBool, and all key/value options in mapString
|
||||
along with the first single-value option as value..
|
||||
|
||||
If field does not have tag tagName, m will be nil.
|
||||
|
||||
See the TagMap* constants for opts.
|
||||
*/
|
||||
func TagToMixedMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, mapBool map[string]bool, mapString map[string]string) {
|
||||
|
||||
var s string
|
||||
var idx int
|
||||
var valStr string
|
||||
var split []string
|
||||
var kvSplit []string
|
||||
var valSplit []string
|
||||
var k string
|
||||
var v string
|
||||
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
|
||||
|
||||
s = field.Tag.Get(tagName)
|
||||
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
split = strings.Split(s, ",")
|
||||
if split == nil || len(split) == 0 {
|
||||
return
|
||||
}
|
||||
mapBool = make(map[string]bool)
|
||||
mapString = make(map[string]string)
|
||||
for idx, valStr = range split {
|
||||
if idx == 0 {
|
||||
if tagOpts[TagMapTrim] {
|
||||
valStr = strings.TrimSpace(valStr)
|
||||
}
|
||||
value = valStr
|
||||
continue
|
||||
}
|
||||
if strings.Contains(valStr, "=") {
|
||||
kvSplit = strings.SplitN(valStr, "=", 2)
|
||||
if kvSplit == nil || len(kvSplit) == 0 {
|
||||
continue
|
||||
}
|
||||
k = valSplit[0]
|
||||
switch len(valSplit) {
|
||||
case 1:
|
||||
v = ""
|
||||
case 2:
|
||||
v = kvSplit[1]
|
||||
}
|
||||
if tagOpts[TagMapTrim] {
|
||||
k = strings.TrimSpace(k)
|
||||
v = strings.TrimSpace(v)
|
||||
}
|
||||
mapString[k] = v
|
||||
} else {
|
||||
if tagOpts[TagMapTrim] {
|
||||
valStr = strings.TrimSpace(valStr)
|
||||
}
|
||||
mapBool[valStr] = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TagToStringMap takes struct field `field` and tag name `tagName`,
|
||||
optionally with options `opts`, and returns a map of the tag values.
|
||||
The tag value string is assumed to be in the form of:
|
||||
key=value[,key=value,key=value...]
|
||||
and returns a map[string]string (map[key]value).
|
||||
It is proccessed in order; later duplicate keys overwrite previous ones.
|
||||
|
||||
If field does not have tag tagName, m will be nil.
|
||||
|
||||
If only a key is provided with no value, the value in the map will be an empty string.
|
||||
(e.g. "foo,bar=baz" => map[string]string{"foo": "", "bar: "baz"}
|
||||
|
||||
See the TagMap* constants for opts.
|
||||
*/
|
||||
func TagToStringMap(field reflect.StructField, tagName string, opts ...tagMapOpt) (m map[string]string) {
|
||||
|
||||
var s string
|
||||
var kvSplit []string
|
||||
var valSplit []string
|
||||
var k string
|
||||
var v string
|
||||
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
|
||||
|
||||
s = field.Tag.Get(tagName)
|
||||
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
kvSplit = strings.Split(s, ",")
|
||||
if kvSplit == nil || len(kvSplit) == 0 {
|
||||
return
|
||||
}
|
||||
for _, kv := range kvSplit {
|
||||
valSplit = strings.SplitN(kv, "=", 2)
|
||||
if valSplit == nil || len(valSplit) == 0 {
|
||||
continue
|
||||
}
|
||||
k = valSplit[0]
|
||||
switch len(valSplit) {
|
||||
case 1:
|
||||
v = ""
|
||||
case 2:
|
||||
v = valSplit[1]
|
||||
// It's not possible to have more than 2.
|
||||
}
|
||||
if m == nil {
|
||||
m = make(map[string]string)
|
||||
}
|
||||
if tagOpts[TagMapTrim] {
|
||||
k = strings.TrimSpace(k)
|
||||
v = strings.TrimSpace(v)
|
||||
}
|
||||
m[k] = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TagToStringMapWithValue is like TagToStringMap but additionally assumes the first value is an "identifier".
|
||||
The tag value string is assumed to be in the form of:
|
||||
value,key=value[,key=value,key=value...]
|
||||
and returns a map[string]string (map[key]value) with the value.
|
||||
*/
|
||||
func TagToStringMapWithValue(field reflect.StructField, tagName string, opts ...tagMapOpt) (value string, m map[string]string) {
|
||||
|
||||
var s string
|
||||
var kvSplit []string
|
||||
var valSplit []string
|
||||
var k string
|
||||
var v string
|
||||
var tagOpts map[tagMapOpt]bool = getTagMapOpts(opts)
|
||||
|
||||
s = field.Tag.Get(tagName)
|
||||
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
kvSplit = strings.Split(s, ",")
|
||||
if kvSplit == nil || len(kvSplit) == 0 {
|
||||
return
|
||||
}
|
||||
for idx, kv := range kvSplit {
|
||||
if idx == 0 {
|
||||
if tagOpts[TagMapTrim] {
|
||||
kv = strings.TrimSpace(kv)
|
||||
}
|
||||
value = kv
|
||||
continue
|
||||
}
|
||||
valSplit = strings.SplitN(kv, "=", 2)
|
||||
if valSplit == nil || len(valSplit) == 0 {
|
||||
continue
|
||||
}
|
||||
k = valSplit[0]
|
||||
switch len(valSplit) {
|
||||
case 1:
|
||||
v = ""
|
||||
case 2:
|
||||
v = valSplit[1]
|
||||
// It's not possible to have more than 2.
|
||||
}
|
||||
if m == nil {
|
||||
m = make(map[string]string)
|
||||
}
|
||||
if tagOpts[TagMapTrim] {
|
||||
k = strings.TrimSpace(k)
|
||||
v = strings.TrimSpace(v)
|
||||
}
|
||||
m[k] = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getTagMapOpts(opts []tagMapOpt) (optMap map[tagMapOpt]bool) {
|
||||
|
||||
optMap = make(map[tagMapOpt]bool)
|
||||
|
||||
if opts == nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
5
structutils/types.go
Normal file
5
structutils/types.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package structutils
|
||||
|
||||
type (
|
||||
tagMapOpt uint8
|
||||
)
|
||||
4
timex/doc.go
Normal file
4
timex/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package timex provides some handy [time]-related functions.
|
||||
*/
|
||||
package timex
|
||||
35
timex/funcs.go
Normal file
35
timex/funcs.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package timex
|
||||
|
||||
import (
|
||||
`time`
|
||||
)
|
||||
|
||||
/*
|
||||
F64Seconds returns [time.Time] `t` as a 64-bit float of <seconds>.<nanoseconds>
|
||||
(where <nanoseconds> is the number of nanoseconds since <seconds>,
|
||||
and <seconds> is the number of seconds since the UNIX epoch).
|
||||
|
||||
This can be used to represent a UNIX Epoch timestamp as seconds but with nanosecond precision.
|
||||
*/
|
||||
func F64Seconds(t time.Time) (f64 float64) {
|
||||
return F64Nanoseconds(t) / float64(time.Second)
|
||||
}
|
||||
|
||||
/*
|
||||
F64Milliseconds is like [F64Seconds] but with a millisecond integer.
|
||||
*/
|
||||
func F64Milliseconds(t time.Time) (f64 float64) {
|
||||
return F64Nanoseconds(t) / float64(time.Millisecond)
|
||||
}
|
||||
|
||||
/*
|
||||
F64Microseconds is like [F64Seconds] but with a microsecond integer.
|
||||
*/
|
||||
func F64Microseconds(t time.Time) (f64 float64) {
|
||||
return F64Nanoseconds(t) / float64(time.Microsecond)
|
||||
}
|
||||
|
||||
// F64Nanoseconds returns [time.Time.UnixNano] as a float64.
|
||||
func F64Nanoseconds(t time.Time) (f64 float64) {
|
||||
return float64(t.UnixNano())
|
||||
}
|
||||
30
timex/funcs_test.go
Normal file
30
timex/funcs_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package timex
|
||||
|
||||
import (
|
||||
"testing"
|
||||
`time`
|
||||
)
|
||||
|
||||
func TestF64(t *testing.T) {
|
||||
|
||||
var tmNano float64 = 1766533329999999999
|
||||
var tmSeconds float64 = 1766533329.999999999
|
||||
var tmMilli float64 = 1766533329999.999999
|
||||
var tmMicro float64 = 1766533329999999.999
|
||||
// 2025-12-23 23:42:09.999999999 +0000 UTC
|
||||
var tm time.Time = time.Unix(1766533329, int64(time.Second-1))
|
||||
|
||||
if F64Seconds(tm) != tmSeconds {
|
||||
t.Fatalf("Failed seconds: %f != %f", F64Seconds(tm), tmSeconds)
|
||||
}
|
||||
if F64Milliseconds(tm) != tmMilli {
|
||||
t.Fatalf("Failed milliseconds: %f != %f", F64Milliseconds(tm), tmMilli)
|
||||
}
|
||||
if F64Microseconds(tm) != tmMicro {
|
||||
t.Fatalf("Failed microseconds: %f != %f", F64Microseconds(tm), tmMicro)
|
||||
}
|
||||
if F64Nanoseconds(tm) != tmNano {
|
||||
t.Fatalf("Failed nanoseconds: %f != %f", F64Nanoseconds(tm), tmNano)
|
||||
}
|
||||
|
||||
}
|
||||
6
tplx/consts.go
Normal file
6
tplx/consts.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package tplx
|
||||
|
||||
const (
|
||||
TplTypeText tplType = iota
|
||||
TplTypeHtml
|
||||
)
|
||||
4
tplx/doc.go
Normal file
4
tplx/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package tplx provides some "shortcuts" to [text/template] and [html/template] rendering.
|
||||
*/
|
||||
package tplx
|
||||
9
tplx/errs.go
Normal file
9
tplx/errs.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidTplType = errors.New("unknown/invalid template type")
|
||||
)
|
||||
235
tplx/funcs.go
Normal file
235
tplx/funcs.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
htmlTpl `html/template`
|
||||
txtTpl `text/template`
|
||||
)
|
||||
|
||||
// MustTplStrToStr wraps [TplStrToStr] but will panic on a non-nil error instead of returning it.
|
||||
func MustTplStrToStr(tplStr string, typ tplType, obj any) (s string) {
|
||||
|
||||
var err error
|
||||
|
||||
if s, err = TplStrToStr(tplStr, typ, obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MustTplToStr wraps [TplToStr] but will panic on error instead of returning it.
|
||||
func MustTplToStr[T Template](tpl T, obj any) (s string) {
|
||||
|
||||
var err error
|
||||
|
||||
if s, err = TplToStr(tpl, obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MustTplToStrWith wraps [TplToStrWith] but will panic on error instead of returning it.
|
||||
func MustTplToStrWith[T Template](tpl T, tplNm string, obj any) (s string) {
|
||||
|
||||
var err error
|
||||
|
||||
if s, err = TplToStrWith(tpl, tplNm, obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TplStrToStr takes in a template string, a template type (see i.e. [TplTypeText], [TplTypeHtml]),
|
||||
and an object and renders to a string.
|
||||
|
||||
This is obviously quite inflexible - there's no way to provide a [text/template.FuncMap]/[html/template.FuncMap],
|
||||
for instance, but if more advanced template features aren't needed then this might just do the trick.
|
||||
|
||||
If you need something more flexible, see [TplToStr] instead.
|
||||
*/
|
||||
func TplStrToStr(tplStr string, typ tplType, obj any) (out string, err error) {
|
||||
|
||||
var ttpl *txtTpl.Template
|
||||
var htpl *htmlTpl.Template
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
switch typ {
|
||||
case TplTypeText:
|
||||
if ttpl, err = txtTpl.New("").Parse(tplStr); err != nil {
|
||||
return
|
||||
}
|
||||
if err = ttpl.Execute(buf, obj); err != nil {
|
||||
return
|
||||
}
|
||||
case TplTypeHtml:
|
||||
if htpl, err = htmlTpl.New("").Parse(tplStr); err != nil {
|
||||
return
|
||||
}
|
||||
if err = htpl.Execute(buf, obj); err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = ErrInvalidTplType
|
||||
return
|
||||
}
|
||||
|
||||
out = buf.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TplToStr takes in an [html/template] or [text/template] and an object and executes it.
|
||||
|
||||
PLEASE NOTE that it is expected that `tpl` has already had at least one template string `.Parse()`'d in.
|
||||
|
||||
If you haven't used generics in Golang yet, this function would be used via something like the following complete example
|
||||
for both a [text/template.Template] (import-aliased as `txtT.Template`) and
|
||||
an [html/template.Template] (import-aliased as `htmlT.Template`).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
txtT "text/template"
|
||||
htmlT "html/template"
|
||||
|
||||
`r00t2.io/goutils/tplx`
|
||||
)
|
||||
|
||||
type (
|
||||
S struct {
|
||||
Name string
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
tTpl *txtT.Template
|
||||
hTpl *htmlT.Template
|
||||
)
|
||||
|
||||
const tTplStr string = "Greetings, {{ .Name }}!\n"
|
||||
const hTplStr string = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hello, {{ .Name }}!</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello, {{ .Name }}. Good to see you.</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
var s string
|
||||
var o *S
|
||||
|
||||
o = &S{
|
||||
Name: "Bob",
|
||||
}
|
||||
|
||||
// A text template.
|
||||
if tTpl, err = txtT.
|
||||
New("my_txt_template").
|
||||
Parse(tTplStr); err != nil {
|
||||
log.Panicf("Failed to parse text template string '%s': %v\n", tTplStr, err)
|
||||
}
|
||||
if s, err = tplx.TplToStr[*txtT.Template](tTpl, o); err != nil {
|
||||
log.Panicf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
fmt.Println(s)
|
||||
|
||||
// An HTML template.
|
||||
if hTpl, err = htmlT.
|
||||
New("index.html").
|
||||
Parse(hTplStr); err != nil {
|
||||
log.Panicf("Failed to parse HTML template string '%s': %v\n", hTplStr, err)
|
||||
}
|
||||
if s, err = tplx.TplToStr[*htmlT.Template](hTpl, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
Additionally, because this function uses a union type [Template],
|
||||
you can even leave the type indicator off.
|
||||
For example:
|
||||
|
||||
// ...
|
||||
if s, err = tplx.TplToStr(tTpl, o); err != nil {
|
||||
log.Panicf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
// ...
|
||||
if s, err = tplx.TplToStr(hTpl, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
// ...
|
||||
|
||||
However, this is not recommended for readability purposes - including
|
||||
the type indicator indicates (heh heh) to others reading your code
|
||||
what type `tTpl` and `hTpl` are without needing to cross-reference
|
||||
their declaration/assignment/definition.
|
||||
|
||||
For more information on generics in Golang, see:
|
||||
|
||||
* The introductory [blog post]
|
||||
* The official [tutorial]
|
||||
* The syntax [reference doc]
|
||||
* The (community-maintained/unofficial) [Go by Example: Generics]
|
||||
|
||||
[blog post]: https://go.dev/blog/intro-generics
|
||||
[tutorial]: https://go.dev/doc/tutorial/generics
|
||||
[reference doc]: https://go.dev/ref/spec#Instantiations
|
||||
[Go by Example: Generics]: https://gobyexample.com/generics
|
||||
*/
|
||||
func TplToStr[T Template](tpl T, obj any) (out string, err error) {
|
||||
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
if err = tpl.Execute(buf, obj); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
out = buf.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TplToStrWith functions the exact same as [TplToStr] but allows you to specify the
|
||||
template entry point (template name) named `nm`.
|
||||
|
||||
For example (see [TplToStr] for a full example):
|
||||
|
||||
// ...
|
||||
var tplNm string = "index.html"
|
||||
|
||||
if s, err = tplx.TplToStrWith(tTpl, tplNm, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template '%s' to string: %v\n", tplNm, err)
|
||||
}
|
||||
// ...
|
||||
|
||||
would call the equivalent of:
|
||||
|
||||
// ...
|
||||
if err = tpl.ExecuteTemplate(<internal buffer>, tplNm, o); err != nil {
|
||||
// ...
|
||||
}
|
||||
*/
|
||||
func TplToStrWith[T Template](tpl T, tplNm string, obj any) (out string, err error) {
|
||||
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
if err = tpl.ExecuteTemplate(buf, tplNm, obj); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
out = buf.String()
|
||||
|
||||
return
|
||||
}
|
||||
103
tplx/funcs_test.go
Normal file
103
tplx/funcs_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
htmlT `html/template`
|
||||
`log`
|
||||
"testing"
|
||||
txtT `text/template`
|
||||
)
|
||||
|
||||
const (
|
||||
txtTplNm string = "my_txt_template"
|
||||
htmlTplNm string = "index.html"
|
||||
tgtTxt string = "Greetings, Bob!\n"
|
||||
tgtHtml string = "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>Hello, Bob!</title>\n\t</head>\n\t<body>\n\t\t<p>Hello, Bob. Good to see you.</p>\n\t</body>\n</html>\n"
|
||||
tTplStr string = "Greetings, {{ .Name }}!\n"
|
||||
hTplStr string = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hello, {{ .Name }}!</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello, {{ .Name }}. Good to see you.</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
tTpl *txtT.Template = txtT.Must(txtT.New(txtTplNm).Parse(tTplStr))
|
||||
hTpl *htmlT.Template = htmlT.Must(htmlT.New(htmlTplNm).Parse(hTplStr))
|
||||
o struct{ Name string } = struct{ Name string }{
|
||||
Name: "Bob",
|
||||
}
|
||||
)
|
||||
|
||||
func TestTpl(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
|
||||
// if s, err = TplToStr[*txtT.Template](tTpl, o); err != nil {
|
||||
if s, err = TplToStr(tTpl, o); err != nil {
|
||||
t.Fatalf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("Text template (%#v): '%s'", s, s)
|
||||
if s != tgtTxt {
|
||||
t.Fatalf("Mismatch on text template '%s'", s)
|
||||
}
|
||||
|
||||
// if s, err = TplToStr[*htmlT.Template](hTpl, o); err != nil {
|
||||
if s, err = TplToStr(hTpl, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("HTML template (%#v):\n%s", s, s)
|
||||
if s != tgtHtml {
|
||||
t.Fatalf("Mismatch on HTML template '%s'", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTplStr(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
|
||||
if s, err = TplStrToStr(tTplStr, TplTypeText, o); err != nil {
|
||||
t.Fatalf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("Text template (%#v): '%s'", s, s)
|
||||
if s != tgtTxt {
|
||||
t.Fatalf("Mismatch on text template '%s'", s)
|
||||
}
|
||||
|
||||
if s, err = TplStrToStr(hTplStr, TplTypeHtml, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("HTML template (%#v):\n%s", s, s)
|
||||
if s != tgtHtml {
|
||||
t.Fatalf("Mismatch on HTML template '%s'", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTplWith(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
|
||||
if s, err = TplToStrWith(tTpl, txtTplNm, o); err != nil {
|
||||
t.Fatalf("Failed to render text template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("Text template (%#v): '%s'", s, s)
|
||||
if s != tgtTxt {
|
||||
t.Fatalf("Mismatch on text template '%s'", s)
|
||||
}
|
||||
|
||||
if s, err = TplToStrWith(hTpl, htmlTplNm, o); err != nil {
|
||||
log.Panicf("Failed to render HTML template to string: %v\n", err)
|
||||
}
|
||||
t.Logf("HTML template (%#v):\n%s", s, s)
|
||||
if s != tgtHtml {
|
||||
t.Fatalf("Mismatch on HTML template '%s'", s)
|
||||
}
|
||||
}
|
||||
631
tplx/sprigx/README.adoc
Normal file
631
tplx/sprigx/README.adoc
Normal file
@@ -0,0 +1,631 @@
|
||||
= SprigX
|
||||
Brent Saner <bts@square-r00t.net>
|
||||
Last rendered {localdatetime}
|
||||
:doctype: book
|
||||
:docinfo: shared
|
||||
:data-uri:
|
||||
:imagesdir: images
|
||||
:sectlinks:
|
||||
:sectnums:
|
||||
:sectnumlevels: 7
|
||||
:toc: preamble
|
||||
:toc2: left
|
||||
:idprefix:
|
||||
:toclevels: 7
|
||||
:source-highlighter: rouge
|
||||
:docinfo: shared
|
||||
|
||||
[id="wat"]
|
||||
== What is SprigX?
|
||||
SprigX are extensions to https://masterminds.github.io/sprig/[the `sprig` library^] (https://pkg.go.dev/github.com/Masterminds/sprig/v3[Go docs^]).
|
||||
|
||||
They provide functions that offer more enriched use cases and domain-specific data.
|
||||
|
||||
[id="use"]
|
||||
== How do I Use SprigX?
|
||||
|
||||
[%collapsible]
|
||||
.The same way you would `sprig`!
|
||||
====
|
||||
[source,go]
|
||||
----
|
||||
package main
|
||||
|
||||
import (
|
||||
htmlTplLib "html/template"
|
||||
txtTplLib "text/template"
|
||||
|
||||
"r00t2.io/goutils/tplx/sprigx"
|
||||
)
|
||||
|
||||
var (
|
||||
txtTpl *txtTplLib.Template = txtTplLib.
|
||||
New("").
|
||||
Funcs(
|
||||
sprigx.TxtFuncMap(),
|
||||
)
|
||||
htmlTpl *htmlTplLib.Template = htmlTplLib.
|
||||
New("").
|
||||
Funcs(
|
||||
sprigx.HtmlFuncMap(),
|
||||
)
|
||||
)
|
||||
----
|
||||
====
|
||||
|
||||
[%collapsible]
|
||||
.They can even be combined/used together.
|
||||
====
|
||||
[source,go]
|
||||
----
|
||||
package main
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"r00t2.io/goutils/tplx/sprigx"
|
||||
)
|
||||
|
||||
var txtTpl *template.Template = template.
|
||||
New("").
|
||||
Funcs(
|
||||
sprigx.TxtFuncMap(),
|
||||
).
|
||||
Funcs(
|
||||
sprig.TxtFuncMap(),
|
||||
)
|
||||
// Or:
|
||||
/*
|
||||
var txtTpl *template.Template = template.
|
||||
New("").
|
||||
Funcs(
|
||||
sprig.TxtFuncMap(),
|
||||
).
|
||||
Funcs(
|
||||
sprigx.TxtFuncMap(),
|
||||
)
|
||||
*/
|
||||
----
|
||||
====
|
||||
|
||||
If a `<template>.FuncMap` is added via `.Funcs()` *after* template parsing, it will override any functions of the same name of a `<template>.FuncMap` *before* parsing.
|
||||
|
||||
For example, if both `sprig` and `sprigx` provide a function `foo`:
|
||||
|
||||
[%collapsible]
|
||||
.this will use `foo` from `sprigx`
|
||||
====
|
||||
[source,go]
|
||||
----
|
||||
package main
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"r00t2.io/goutils/tplx/sprigx"
|
||||
)
|
||||
|
||||
const (
|
||||
myTpl string = `{{ "This is an example template string." | foo }}`
|
||||
)
|
||||
|
||||
var (
|
||||
tpl *template.Template = template.Must(
|
||||
template.
|
||||
New("").
|
||||
Funcs(sprig.TxtFuncMap()).
|
||||
Parse(myTpl),
|
||||
).
|
||||
Funcs(sprigx.TxtFuncMap())
|
||||
)
|
||||
----
|
||||
====
|
||||
|
||||
whereas
|
||||
|
||||
[%collapsible]
|
||||
.this will use `foo` from `sprig`
|
||||
====
|
||||
[source,go]
|
||||
----
|
||||
package main
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"r00t2.io/goutils/tplx/sprigx"
|
||||
)
|
||||
|
||||
const (
|
||||
myTpl string = `{{ "This is an example template string." | foo }}`
|
||||
)
|
||||
|
||||
var (
|
||||
tpl *template.Template = template.Must(
|
||||
template.
|
||||
New("").
|
||||
Funcs(sprigx.TxtFuncMap()).
|
||||
Parse(myTpl),
|
||||
).
|
||||
Funcs(sprig.TxtFuncMap())
|
||||
)
|
||||
----
|
||||
====
|
||||
|
||||
and a function can even be
|
||||
|
||||
[%collapsible]
|
||||
.explicitly overridden.
|
||||
====
|
||||
This would override a function `foo` and `foo2` in `sprigx` from `foo` and `foo2` from `sprig`, but leave all other `sprig` functions untouched.
|
||||
|
||||
[source,go]
|
||||
----
|
||||
package main
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"r00t2.io/goutils/tplx/sprigx"
|
||||
)
|
||||
|
||||
const (
|
||||
myTpl string = `{{ "This is an example template string." | foo }}`
|
||||
)
|
||||
|
||||
var (
|
||||
overrideFuncs template.FuncMap = sprig.TxtFuncMap()
|
||||
tpl *template.Template = template.Must(
|
||||
template.
|
||||
New("").
|
||||
Funcs(sprigx.TxtFuncMap()).
|
||||
Parse(myTpl),
|
||||
).
|
||||
Funcs(
|
||||
template.FuncMap(
|
||||
map[string]any{
|
||||
"foo": overrideFuncs["foo"],
|
||||
"foo2": overrideFuncs["foo2"],
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
----
|
||||
====
|
||||
|
||||
[id="fn"]
|
||||
== Functions
|
||||
Expect this list to grow over time, and potentially more frequently than the `sprigx` functions.
|
||||
|
||||
[id="fn_sys"]
|
||||
=== System/OS/Platform
|
||||
|
||||
[id="fn_sys_arch"]
|
||||
==== `sysArch`
|
||||
Returns the https://pkg.go.dev/runtime#GOARCH[`runtime.GOARCH`^] constant.
|
||||
|
||||
[id="fn_sys_numcpu"]
|
||||
==== `sysNumCpu`
|
||||
Returns the value from https://pkg.go.dev/runtime#NumCPU[`runtime.NumCPU`^].
|
||||
|
||||
[id="fn_sys_os"]
|
||||
==== `sysOsName`
|
||||
Returns the https://pkg.go.dev/runtime#GOOS[`runtime.GOOS`^] constant.
|
||||
|
||||
[id="fn_sys_rntm"]
|
||||
==== `sysRuntime`
|
||||
This function returns a `map[string]string` of various information from the https://pkg.go.dev/runtime[`runtime` stdlib library^].
|
||||
|
||||
Specifically, the following are returned.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
The value type is a direct link to the `runtime` documentation providing more detail about the associated value.
|
||||
|
||||
Because all values are mapped as strings, they can be converted back to their native type via e.g. the https://masterminds.github.io/sprig/conversion.html[Sprig conversion functions^] if necessary.
|
||||
====
|
||||
|
||||
.`sysRuntime` Values
|
||||
[cols="^.^3m,^.^3",options="header"]
|
||||
|===
|
||||
| Key | Value Type
|
||||
|
||||
| compiler | https://pkg.go.dev/runtime#Compiler[string^]
|
||||
| arch | https://pkg.go.dev/runtime#GOARCH[string^]
|
||||
| os | https://pkg.go.dev/runtime#GOOS[string^]
|
||||
| maxprocs | https://pkg.go.dev/runtime#GOMAXPROCS[int^] footnote:[For safety concerns, `sprigx` does not allow *setting* `GOMAXPROCS`, this value only contains the *current* `GOMAXPROCS` value.]
|
||||
| cpu_cnt | https://pkg.go.dev/runtime#NumCPU[int^]
|
||||
| num_cgo | https://pkg.go.dev/runtime#NumCgoCall[int^]
|
||||
| num_go | https://pkg.go.dev/runtime#NumGoroutine[int^]
|
||||
| go_ver | https://pkg.go.dev/runtime#Version[string^]
|
||||
|===
|
||||
|
||||
As a convenience, some of these values also have their own dedicated functions as well:
|
||||
|
||||
* <<fn_sys_arch>>
|
||||
* <<fn_sys_numcpu>>
|
||||
* <<fn_sys_os>>
|
||||
|
||||
[id="fn_path"]
|
||||
=== Paths
|
||||
|
||||
[id="fn_path_gnrc"]
|
||||
==== Generic
|
||||
These operate similar to https://pkg.go.dev/path[the `path` stdlib library^] and use a fixed `/` path separator.
|
||||
|
||||
[id="fn_path_gnrc_pj"]
|
||||
===== `pathJoin`
|
||||
`pathJoin` operates *exactly* like https://pkg.go.dev/path#Join[`path.Join`^] in stdlib.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
If you are joining paths in a pipeline, you almost assuredly want <<fn_path_gnrc_ppj>> or <<fn_path_gnrc_pspj>> instead unless you are explicitly *appending* a pipeline result to a path.
|
||||
====
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- pathJoin "a" "b" "c" }}
|
||||
{{- pathJoin "/" "a" "b" "c" }}
|
||||
{{- pathJoin "/a/b" "c" }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[source,text]
|
||||
----
|
||||
a/b/c
|
||||
/a/b/c
|
||||
/a/b/c
|
||||
----
|
||||
|
||||
[id="fn_path_gnrc_ppj"]
|
||||
===== `pathPipeJoin`
|
||||
`pathPipeJoin` operates like <<fn_path_gnrc_pj>> with one deviation: the root/base path is expected to be *last* in the arguments.
|
||||
|
||||
This makes it much more suitable for use in template pipelines, as the previous value in a pipeline is passed in as the last element to the next pipe function.
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- $myBase := "/a" -}}
|
||||
{{- pathPipeJoin "b" "c" "a" }}
|
||||
{{- pathPipeJoin "a" "b" "c" "/" }}
|
||||
{{- $myBase | pathPipeJoin "b" "c" }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[source,text]
|
||||
----
|
||||
a/b/c
|
||||
/a/b/c
|
||||
/a/b/c
|
||||
----
|
||||
|
||||
[id="fn_path_gnrc_psj"]
|
||||
===== `pathSliceJoin`
|
||||
`pathSliceJoin` joins a slice of path segment strings (`[]string`) instead of a variadic sequence of strings.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
The `splitList` function shown below is from the https://masterminds.github.io/sprig/string_slice.html[`sprig` string slice functions^].
|
||||
====
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- $myList := "a,b,c" | splitList "," -}}
|
||||
{{- $myList | pathSliceJoin }}
|
||||
{{- ("a,b,c" | splitList ",") | pathSliceJoin }}
|
||||
{{- ("/,a,b,c" | splitList ",") | pathSliceJoin }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[source,text]
|
||||
----
|
||||
a/b/c
|
||||
a/b/c
|
||||
/a/b/c
|
||||
----
|
||||
|
||||
[id="fn_path_gnrc_pspj"]
|
||||
===== `pathSlicePipeJoin`
|
||||
`pathSlicePipeJoin` operates like <<fn_path_gnrc_ppj>> in that it is suitable for pipeline use in which the root/base path is passed in from the pipeline, but it is like <<fn_path_gnrc_psj>> in that it then also accepts a slice of path segments (`[]string`) to append to that base path.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
The `splitList` function shown below is from the https://masterminds.github.io/sprig/string_slice.html[`sprig` string slice functions^].
|
||||
====
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- $myBase := "/a" -}}
|
||||
{{- $myList := "b,c,d" | splitList "." -}}
|
||||
{{- pathSlicePipeJoin $myList $myBase }}
|
||||
{{- $myBase | pathSlicePipeJoin $myList }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[source,text]
|
||||
----
|
||||
/a/b/c
|
||||
/a/b/c
|
||||
----
|
||||
|
||||
[id="fn_path_gnrc_psubj"]
|
||||
===== `pathSubJoin`
|
||||
`pathSubJoin` operates like <<fn_path_gnrc_pj>> but it expects an explicit root/base path.
|
||||
|
||||
The pipeline-friendly equivalent of this is <<fn_path_gnrc_ppj>>.
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- pathSubJoin "/a/b" "c" }}
|
||||
{{- pathSubJoin "/" "a" "b" "c" }}
|
||||
{{- "c" | pathSubJoin "/" "a" "b" }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[source,text]
|
||||
----
|
||||
/a/b/c
|
||||
/a/b/c
|
||||
/a/b/c
|
||||
----
|
||||
|
||||
[id="fn_path_os"]
|
||||
==== OS/Platform-Tailored
|
||||
These operate similar to https://pkg.go.dev/path/filepath[the `path/filepath` stdlib library^], and use the OS-specific https://pkg.go.dev/os#PathSeparator[`os.PathSeparator`^].
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Take special note of the oddness around specifying Windows paths and drive letters in e.g. <<fn_path_os_pj>>!
|
||||
|
||||
It is recommended to make use of <<fn_sys_os>> to conditionally format path bases/roots if needed.
|
||||
====
|
||||
|
||||
[id="fn_path_os_pj"]
|
||||
===== `osPathJoin`
|
||||
`osPathJoin` operates *exactly* like https://pkg.go.dev/path/filepath#Join[`path/filepath.Join`^] in stdlib.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
If you are joining paths in a pipeline, you almost assuredly want <<fn_path_os_ppj>> or <<fn_path_os_pspj>> instead unless you are explicitly *appending* a pipeline result to a path.
|
||||
====
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- osPathJoin "a" "b" "c" }}
|
||||
{{- osPathJoin "/" "a" "b" "c" }}
|
||||
{{- osPathJoin "C:\\" "a" "b" "c" }}
|
||||
{{- osPathJoin "C:" "a" "b" "c" }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[cols="^.^2,.^4a",options="header"]
|
||||
|===
|
||||
| OS ^| Result
|
||||
|
||||
| Windows | [source,text]
|
||||
----
|
||||
a\b\c
|
||||
\a\b\c
|
||||
\a\b\c
|
||||
C:\a\b\c
|
||||
C:a\b\c
|
||||
----
|
||||
| Others (e.g. Linux, macOS) | [source,text]
|
||||
----
|
||||
a/b/c
|
||||
/a/b/c
|
||||
C:\/a/b/c
|
||||
C:/a/b/c
|
||||
----
|
||||
|===
|
||||
|
||||
[id="fn_path_os_ppj"]
|
||||
===== `osPathPipeJoin`
|
||||
`osPathPipeJoin` operates like <<fn_path_gnrc_ppj>> (except using OS-specific path separators).
|
||||
|
||||
This makes it much more suitable for use in template pipelines, as the previous value in a pipeline is passed in as the last element to the next pipe function.
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- $myBase := "/a" -}}
|
||||
{{- osPathPipeJoin "b" "c" "a" }}
|
||||
{{- osPathPipeJoin "a" "b" "c" "/" }}
|
||||
{{- $myBase | osPathPipeJoin "b" "c" }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[cols="^.^2,.^4a",options="header"]
|
||||
|===
|
||||
| OS ^| Result
|
||||
|
||||
| Windows | [source,text]
|
||||
----
|
||||
a\b\c
|
||||
\a\b\c
|
||||
\a\b\c
|
||||
----
|
||||
| Others (e.g. Linux, macOS) | [source,text]
|
||||
----
|
||||
a/b/c
|
||||
/a/b/c
|
||||
/a/b/c
|
||||
----
|
||||
|===
|
||||
|
||||
[id="fn_path_ossep"]
|
||||
===== `osPathSep`
|
||||
`osPathSep` returns the https://pkg.go.dev/os#PathSeparator[`os.PathSeparator`^] for this OS.
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- osPathSep }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[cols="^.^2,.^4a",options="header"]
|
||||
|===
|
||||
| OS ^| Result
|
||||
|
||||
| Windows | [source,text]
|
||||
----
|
||||
\
|
||||
----
|
||||
| Others (e.g. Linux, macOS) | [source,text]
|
||||
----
|
||||
/
|
||||
----
|
||||
|===
|
||||
|
||||
[id="fn_path_os_psj"]
|
||||
===== `osPathSliceJoin`
|
||||
`osPathSliceJoin` operates like <<fn_path_gnrc_psj>> but with OS-specific path separators.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
The `splitList` function shown below is from the https://masterminds.github.io/sprig/string_slice.html[`sprig` string slice functions^].
|
||||
====
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- $myList := "a,b,c" | splitList "," -}}
|
||||
{{- $myList | osPathSliceJoin }}
|
||||
{{- ("a,b,c" | splitList ",") | osPathSliceJoin }}
|
||||
{{- ("/,a,b,c" | splitList ",") | osPathSliceJoin }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[cols="^.^2,.^4a",options="header"]
|
||||
|===
|
||||
| OS ^| Result
|
||||
|
||||
| Windows | [source,text]
|
||||
----
|
||||
a\b\c
|
||||
a\b\c
|
||||
\a\b\c
|
||||
----
|
||||
| Others (e.g. Linux, macOS) | [source,text]
|
||||
----
|
||||
a/b/c
|
||||
a/b/c
|
||||
/a/b/c
|
||||
----
|
||||
|===
|
||||
|
||||
[id="fn_path_os_pspj"]
|
||||
===== `osPathSlicePipeJoin`
|
||||
`osPathSlicePipeJoin` operates like <<fn_path_gnrc_pspj>> but with OS-specific separators.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
The `splitList` function shown below is from the https://masterminds.github.io/sprig/string_slice.html[`sprig` string slice functions^].
|
||||
====
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- $myBase := "/a" -}}
|
||||
{{- $myList := "b,c,d" | splitList "." -}}
|
||||
{{- osPathSlicePipeJoin $myList $myBase }}
|
||||
{{- $myBase | osPathSlicePipeJoin $myList }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[cols="^.^2,.^4a",options="header"]
|
||||
|===
|
||||
| OS ^| Result
|
||||
|
||||
| Windows | [source,text]
|
||||
----
|
||||
\a\b\c\d
|
||||
\a\b\c\d
|
||||
----
|
||||
| Others (e.g. Linux, macOS) | [source,text]
|
||||
----
|
||||
/a/b/c/d
|
||||
/a/b/c/d
|
||||
----
|
||||
|===
|
||||
|
||||
[id="fn_path_os_psubj"]
|
||||
===== `osPathSubJoin`
|
||||
`osPathSubJoin` operates like <<fn_path_gnrc_psubj>> but with OS-specific separators.
|
||||
|
||||
The pipeline-friendly equivalent of this is <<fn_path_os_ppj>>.
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{- osPathSubJoin "/a/b" "c" }}
|
||||
{{- osPathSubJoin "/" "a" "b" "c" }}
|
||||
{{- "c" | osPathSubJoin "/" "a" "b" }}
|
||||
----
|
||||
|
||||
renders as:
|
||||
|
||||
[cols="^.^2,.^4a",options="header"]
|
||||
|===
|
||||
| OS ^| Result
|
||||
|
||||
| Windows | [source,text]
|
||||
----
|
||||
\a\b\c
|
||||
\a\b\c
|
||||
\a\b\c
|
||||
----
|
||||
| Others (e.g. Linux, macOS) | [source,text]
|
||||
----
|
||||
/a/b/c
|
||||
/a/b/c
|
||||
/a/b/c
|
||||
----
|
||||
|===
|
||||
|
||||
[id="fn_str"]
|
||||
=== Strings
|
||||
|
||||
[id="fn_str_extindent"]
|
||||
==== `extIndent`
|
||||
`extIndent` allows for a MUCH more flexible indenter than the `sprig` `indent` function.
|
||||
|
||||
It works with both Windows (`\r\n`) and POSIX (`\n`) linebreaks.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
If `<indentString>` is set to `\n` and `<levels>` is always set to `1`, this function can even be used to doubelspace text!
|
||||
====
|
||||
|
||||
It has quite a few arguments, however:
|
||||
|
||||
[source,gotemplate]
|
||||
----
|
||||
{{ extIndent <levels> <skipFirst> <skipEmpty> <skipWhitespace> <indentString> <input> }}
|
||||
----
|
||||
|
||||
Where:
|
||||
|
||||
* `<levels>`: The level of indentation for the text. If less than or equal to `0`, `extIndent` just returns `<input>` as-is and NO-OPs otherwise.
|
||||
* `<skipFirst>`: If true, skip indenting the first line. This is particularly handy if you like to visually align your function calls in your templates.
|
||||
* `<skipEmpty>`: If true, do not add an indent to *empty* lines (where an "empty line" means "only has a linebreak").
|
||||
* `<skipWhitespace>`: If true, do not add an indent to lines that *only* consist of whitespace (spaces, tabs, etc.) and a linebreak.
|
||||
* `<indentString>`: The string to use as the "indent character". This can be any string, such as `" "`, `"\t"`, `"."`, `"|"`, `"=="` etc.
|
||||
* `<input>`: The text to be indented. Because it is the last argument, `extIndent` works with pipelined text as well.
|
||||
|
||||
[id="fn_dbg"]
|
||||
=== Debugging
|
||||
|
||||
[id="fn_dbg_dump"]
|
||||
==== `dump`
|
||||
The `dump` function calls https://pkg.go.dev/github.com/davecgh/go-spew/spew#Sdump[the `Sdump` function^] from https://github.com/davecgh/go-spew[`go-spew`] (https://pkg.go.dev/github.com/davecgh/go-spew/spew[`github.com/davecgh/go-spew/spew`^]) for whatever object(s) is/are passed to it.
|
||||
1532
tplx/sprigx/README.html
Normal file
1532
tplx/sprigx/README.html
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user