Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
58556d7281
|
|||
|
c6fc692f5e
|
|||
|
4770052b52
|
|||
|
1eea0c2672
|
|||
|
67c7faf449
|
|||
|
82c69ec542
|
|||
|
07e0e587fa
|
|||
|
1bd6e1256c
|
|||
|
64a7648fbc
|
|||
|
9cce861b2e
|
|||
|
927ad08057
|
|||
|
2edbc9306d
|
|||
|
bb71be187f
|
|||
|
834395c050
|
|||
|
ef56898d6b
|
|||
|
006cf39fa1
|
|||
|
145c32268e
|
|||
|
6ddfcdb416
|
|||
|
79f10b7611
|
|||
|
01adbfc605
|
|||
|
b1d8ea34a6
|
|||
|
e101758187
|
|||
|
3c49a5b70a
|
|||
|
965657d1b2
|
|||
|
970acd0ee4
|
|||
|
2222cea7fb
|
|||
|
688abd0874
|
|||
|
a1f87d6b51
|
|||
|
07951f1f03
|
|||
|
bae0abe960
|
|||
|
368ae0cb8e
|
@@ -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
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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
|
||||
)
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/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}"
|
||||
|
||||
# Render HTML, include in commit
|
||||
newf="${pfx}.html"
|
||||
asciidoctor -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
|
||||
echo "Generated ${newf} from ${f}"
|
||||
git add "${newf}"
|
||||
|
||||
# If asciidoctor-pdf is installed, render as PDF for local use
|
||||
# (Does not get added to commit, and *.pdf is in .gitignore for a reason)
|
||||
if command -v asciidoctor-pdf &> /dev/null;
|
||||
then
|
||||
newf="${pfx}.pdf"
|
||||
|
||||
asciidoctor-pdf -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
|
||||
fi
|
||||
|
||||
# If pandoc is installed, render to "GitHub-flavored Markdown" for better rendering on forks/mirrors
|
||||
# and marginally better rendering on https://pkg.go.dev/ and add to commit.
|
||||
#
|
||||
# <rant>
|
||||
# There is no such thing as "Markdown".
|
||||
# The closest thing you have to any sort of standard is https://daringfireball.net/projects/markdown/
|
||||
# but everybody and their mother adds their own "extensions"/"flavor", and sometimes even
|
||||
# change how formatting works compared to the Daring Fireball/John Gruber spec (the original creator of the "syntax").
|
||||
# Ergo "Markdown" inherently has no meaning.
|
||||
# It's one of the worst formatting languages out there - just because it's popular doesn't mean it's good.
|
||||
#
|
||||
# If you're writing docs, you should stick to one of these which have defined, canonical, standardized
|
||||
# syntax:
|
||||
# * AsciiDoc/AsciiDoctor
|
||||
# * Supports much more extensive formatting than any Markdown flavor I've seen
|
||||
# * Source/raw/unrendered still *quite* readable by human eyes
|
||||
# * Somewhat limited parsers/renderers
|
||||
# * https://asciidoc.org/
|
||||
# * https://asciidoctor.org/
|
||||
# * DocBook
|
||||
# * Supports even more extensive and flexible but exact formatting
|
||||
# * Great for publishing, though - especially if you need control over formatting/layout
|
||||
# * XML-based
|
||||
# * Harder to read in plaintext, but fairly doable (XML lends to decent mental rendering)
|
||||
# * Very wide support for parsing/rendering
|
||||
# * https://docbook.org/
|
||||
# * LaTex
|
||||
# * Allows for *very* extensive domain-specific ligature/representation (very common in mathematic/scientific literature)
|
||||
# * But nigh unreadable by human eyes unless you've rather familiar with it
|
||||
# * Parsing/rendering support about on-par with DocBook
|
||||
# * https://www.latex-project.org/
|
||||
# </rant>
|
||||
if command -v pandoc &> /dev/null;
|
||||
then
|
||||
newf="${pfx}.md"
|
||||
|
||||
set +e
|
||||
# --wrap=preserve or --wrap=none is required to avoid it reflowing the section titles to newlines.
|
||||
#asciidoctor -a ROOTDIR="${orig}/" -b docbook -o - "${f}" | pandoc -f docbook -t markdown_strict -o "${newf}" --wrap=none
|
||||
asciidoctor -a ROOTDIR="${orig}/" -b html -o - "${f}" | pandoc -f html -t gfm -o "${newf}" --wrap=none
|
||||
if [ $? -eq 0 ];
|
||||
then
|
||||
echo "Generated ${newf} from ${f}"
|
||||
git add "${newf}"
|
||||
else
|
||||
echo "Failed to generate ${newf} from ${f}"
|
||||
git rm "${newf}" 2>/dev/null
|
||||
fi
|
||||
set -e
|
||||
fi
|
||||
cd ${orig}
|
||||
done
|
||||
echo "Regenerated docs"
|
||||
+2
-1
@@ -19,12 +19,13 @@
|
||||
.idea/
|
||||
|
||||
# https://github.com/github/gitignore/blob/master/Go.gitignore
|
||||
# Binaries for programs and plugins
|
||||
# Binaries for programs and plugins and other data
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.pdf
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
- validx: validator functions for https://pkg.go.dev/github.com/go-playground/validator/v10
|
||||
|
||||
- hashx?
|
||||
+48
-13
@@ -11,11 +11,11 @@ import (
|
||||
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.
|
||||
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.
|
||||
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) {
|
||||
|
||||
@@ -34,17 +34,53 @@ func NewMaskBitExplicit(value uint) (m *MaskBit) {
|
||||
return
|
||||
}
|
||||
|
||||
// HasFlag is true if m has MaskBit flag set/enabled.
|
||||
/*
|
||||
HasFlag is true if m has MaskBit flag set/enabled.
|
||||
|
||||
See the "Composite (OR'd) Flags" section in this module's
|
||||
documentation for important caveats.
|
||||
*/
|
||||
func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {
|
||||
|
||||
var b MaskBit = *m
|
||||
|
||||
if b&flag != 0 {
|
||||
if b&flag == flag {
|
||||
r = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
HasOneOf is like a "looser" form of [MaskBit.HasFlag]
|
||||
in that it allows for testing composite membership.
|
||||
|
||||
See [MaskBit.HasFlag] and the "Composite (OR'd) Flags"
|
||||
section in this module's documentation for more details.
|
||||
|
||||
If composite is *not* an OR'd MaskBit (i.e.
|
||||
it falls directly on a boundary -- 0, 1, 2, 4, 8, 16, etc.),
|
||||
then HasOneOf will behave exactly like [MaskBit.HasFlag].
|
||||
|
||||
If m is a composite [MaskBit] (it usually is) and composite is ALSO a composite MaskBit,
|
||||
HasOneOf will return true if ANY of the flags set in composite is set in m.
|
||||
*/
|
||||
func (m *MaskBit) HasOneOf(composite MaskBit) (r bool) {
|
||||
|
||||
var b MaskBit = *m
|
||||
|
||||
if b&composite != 0 {
|
||||
r = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsOneOf is the old name for [MaskBit.HasOneOf].
|
||||
|
||||
DEPRECATED: This method will be removed sometime in the future. Use [MaskBit.HasOneOf] instead.
|
||||
*/
|
||||
func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) { return m.HasOneOf(composite) }
|
||||
|
||||
// AddFlag adds MaskBit flag to m.
|
||||
func (m *MaskBit) AddFlag(flag MaskBit) {
|
||||
|
||||
@@ -70,15 +106,14 @@ func (m *MaskBit) ToggleFlag(flag MaskBit) {
|
||||
}
|
||||
|
||||
/*
|
||||
Bytes returns the current value of a MasBit as a byte slice (big-endian).
|
||||
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.
|
||||
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) {
|
||||
|
||||
|
||||
+196
-14
@@ -1,13 +1,104 @@
|
||||
/*
|
||||
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 - compared to a *single boolean*. But bitmasking lets you store a "boolean" AT EACH BIT boundary - it operates on
|
||||
whether a bit in a byte/set of bytes at a given position is 0 or 1.
|
||||
|
||||
In other words:
|
||||
|
||||
type (
|
||||
OptStruct struct {
|
||||
A bool
|
||||
B bool
|
||||
C bool
|
||||
D bool
|
||||
E bool
|
||||
F bool
|
||||
G bool
|
||||
H bool
|
||||
}
|
||||
)
|
||||
|
||||
One instance of OptStruct takes up *8 bytes* (*64 bits*).
|
||||
|
||||
As a uint8 bitmask, however, it takes up exactly *one byte/8 bits*:
|
||||
|
||||
type (
|
||||
Opt uint8
|
||||
)
|
||||
|
||||
const OptNone Opt = 0
|
||||
const (
|
||||
A Opt = 1 << iota // 1
|
||||
B // 2
|
||||
C // 4
|
||||
D // 8
|
||||
E // 16
|
||||
F // 32
|
||||
G // 64
|
||||
H // 128
|
||||
// Would overflow to 0:
|
||||
// I
|
||||
)
|
||||
|
||||
As shown, the size of an unsigned integer determines the number of bit-boundaried flags for that bitmask.
|
||||
|
||||
Which means on 32-bit platforms, a [MaskBit] can have up to 32 different flags but only occupies 4 bytes of memory.
|
||||
|
||||
On 64-bit platforms, a [MaskBit] can have up to 64 different flags but only occupies 8 bytes of memory.
|
||||
|
||||
If you tried to do that with a struct of booleans instead, that'd occupy 32 *bytes* and 64 *bytes* respectively.
|
||||
|
||||
In summary, a bitmask set occupies 1/8th the amount of memory as a set of equal number of booleans in Go.
|
||||
|
||||
You can, of course, extend this even further by defining meaningful "composites",
|
||||
or OR'd-combination of flags (see the "Composite Flags" section below):
|
||||
|
||||
type Perms uint8
|
||||
const NoPerms Perms = 0
|
||||
const (
|
||||
PermsRead Perms = 1 << iota
|
||||
PermsList
|
||||
PermsUpdate
|
||||
PermsCreate
|
||||
PermsDelete
|
||||
PermsUpdateBulk
|
||||
PermsCreateBulk
|
||||
PermsDeleteBulk
|
||||
|
||||
// This makes it easy to add multiple permissions at once.
|
||||
PermsCRUD Perms = PermsCreate | PermsRead | PermsUpdate | PermsDelete
|
||||
PermsCRUDL Perms = PermsCRUD | PermsList
|
||||
// And so forth.
|
||||
)
|
||||
|
||||
"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 ZDoom used bitmasking for the [dmflags] value in its server configs,
|
||||
and why *NIX platforms uses them for permission/type modes.
|
||||
|
||||
# Usage
|
||||
|
||||
To use this library, set constants like thus:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"r00t2.io/goutils/bitmask"
|
||||
)
|
||||
|
||||
@@ -19,35 +110,126 @@ To use this, set constants like thus:
|
||||
// ...
|
||||
)
|
||||
|
||||
var MyMask *bitmask.MaskBit
|
||||
var (
|
||||
MyMask *bitmask.MaskBit = bitmask.NewMaskBit()
|
||||
)
|
||||
|
||||
func main() {
|
||||
MyMask = bitmask.NewMaskBit()
|
||||
|
||||
MyMask.AddFlag(OPT1)
|
||||
MyMask.AddFlag(OPT3)
|
||||
|
||||
_ = MyMask
|
||||
// This would print true.
|
||||
fmt.Println(MyMask.HasFlag(OPT1))
|
||||
|
||||
// As would this:
|
||||
fmt.Println(MyMask.HasFlag(OPT3))
|
||||
|
||||
// But this would print false:
|
||||
fmt.Println(MyMask.HasFlag(OPT2))
|
||||
}
|
||||
|
||||
This would return true:
|
||||
# Technical Caveats
|
||||
|
||||
MyMask.HasFlag(OPT1)
|
||||
Targeting
|
||||
|
||||
As would this:
|
||||
When implementing, you should always set a "source" mask (e.g. MyMask, from Usage section above)
|
||||
as the actual value.
|
||||
|
||||
MyMask.HasFlag(OPT3)
|
||||
For example, if you are checking a permissions set for a user that has the value, say, 6
|
||||
|
||||
But this would return false:
|
||||
var userPerms uint = 6 // 0x0000000000000006
|
||||
|
||||
MyMask.HasFlag(OPT2)
|
||||
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)
|
||||
userPermMask.HasFlag(bitmask.MaskBit(userPerms))
|
||||
|
||||
NOR:
|
||||
userPermMask = PermsRead
|
||||
if userPermMask.HasFlag(bitmask.MaskBit(userPerms)) {
|
||||
// ...
|
||||
}
|
||||
|
||||
This will be terribly, horribly wrong, cause incredibly unexpected results,
|
||||
and quite possibly cause massive security issues. Don't do it.
|
||||
|
||||
Remember, [MaskBit.HasFlag] (and other methods) are for comparing/modifying an
|
||||
*authoritative/concrete value* with one or more *attributes*, not the other way around.
|
||||
|
||||
Composite Flags
|
||||
|
||||
If you want to define a set of flags that are a combination
|
||||
(a "composite", or OR'd set) of other flags, your inclination
|
||||
would be to bitwise-OR them together:
|
||||
|
||||
const (
|
||||
flagA bitmask.MaskBit = 1 << iota // 1
|
||||
flagB // 2
|
||||
flagC // 4
|
||||
// ...
|
||||
)
|
||||
|
||||
const (
|
||||
flagAB bitmask.MaskBit = flagA | flagB // 3
|
||||
)
|
||||
|
||||
Which is fine, and the correct approach.
|
||||
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 would only return true if you did:
|
||||
|
||||
// ...
|
||||
myMask.AddFlag(flagAB)
|
||||
|
||||
or:
|
||||
|
||||
// ...
|
||||
myMask.AddFlag(flagA)
|
||||
myMask.AddFlag(flagB)
|
||||
|
||||
Instead, if you want to see if a mask has *at least one flag of* a composite flag,
|
||||
you can use [MaskBit.HasOneOf].
|
||||
|
||||
# Other Options
|
||||
|
||||
If you need something with more flexibility (as always, at the cost of complexity),
|
||||
you may be interested in one of the following libraries:
|
||||
|
||||
. github.com/alvaroloes/enumer
|
||||
. github.com/abice/go-enum
|
||||
. github.com/jeffreyrichter/enum/enum
|
||||
* [github.com/alvaroloes/enumer]
|
||||
* [github.com/abice/go-enum]
|
||||
* [github.com/jeffreyrichter/enum/enum]
|
||||
|
||||
[dmflags]: https://doomwiki.org/wiki/DMFlags
|
||||
*/
|
||||
package bitmask
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
Package bytesx aims to extend functionality of the stdlib [bytes] module.
|
||||
|
||||
TODO.
|
||||
*/
|
||||
package bytesx
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# go tool dist list for all valid GOOS/GOARCH targets.
|
||||
|
||||
for tgt in $(go tool dist list);
|
||||
do
|
||||
o="$(echo ${tgt} | cut -f1 -d '/')"
|
||||
a="$(echo ${tgt} | cut -f2 -d '/')"
|
||||
out="$(env GOOS=${o} GOARCH=${a} go build ./... 2>&1)"
|
||||
ret=${?}
|
||||
if [ $ret -ne 0 ];
|
||||
then
|
||||
echo "OS: ${o}"
|
||||
echo "ARCH: ${a}"
|
||||
echo "${out}"
|
||||
echo
|
||||
echo
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package binaryx aims to extend functionality of the stdlib [encoding/binary] module.
|
||||
*/
|
||||
package binaryx
|
||||
@@ -0,0 +1,122 @@
|
||||
package binaryx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
/*
|
||||
Marshal provides a Golang convention marshaling function entry point
|
||||
(e.g. like [encoding/json.Marshal], [encoding/xml.Marshal]) for arbitrary
|
||||
binary data.
|
||||
|
||||
It simply wraps [OrderedMarshal] with [encoding/binary.NativeEndian]
|
||||
(which can differ from platform to platform) as the ord parameter to [OrderedMarshal].
|
||||
|
||||
If you need to use a different or explicit [encoding/binary.ByteOrder],
|
||||
use [OrderedMarshal] directly instead.
|
||||
*/
|
||||
func Marshal(v any) (data []byte, err error) {
|
||||
|
||||
if data, err = OrderedMarshal(v, binary.NativeEndian); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
OrderedMarshal provides a slightly more flexible Golang convention marshaling
|
||||
function entry point (e.g. similar to [encoding/json.MarshalIndent],
|
||||
[encoding/xml.MarshalIndent]) for arbitrary binary data.
|
||||
|
||||
If v conforms to [encoding.BinaryMarshaler], then the marshal method will be called.
|
||||
|
||||
Otherwise, it wraps [encoding/binary.Write] with an internal buffer using ord
|
||||
as the [encoding/binary.ByteOrder] (endianness), so refer to that function
|
||||
for all caveats and other details.
|
||||
Note that [encoding/binary.Write] is *much* more strict than marshaling,
|
||||
and requires very basic typing.
|
||||
|
||||
If you have no need for a specific byte order/endianness or want to explicitly use this
|
||||
system's native byte order/endianness,
|
||||
use [Marshal] instead.
|
||||
*/
|
||||
func OrderedMarshal(v any, ord binary.ByteOrder) (data []byte, err error) {
|
||||
|
||||
var ok bool
|
||||
var b binary.Marshaler
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
if b, ok = v.(encoding.BinaryMarshaler); ok {
|
||||
if data, err = b.MarshalBinary(); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = binary.Write(buf, ord, v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data = buf.Bytes()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Unmarshal provides a Golang convention unmarshaling function entry point
|
||||
(e.g. like [encoding/json.Unmarshal], [encoding/xml.Unmarshal]) for arbitrary
|
||||
binary data.
|
||||
|
||||
It simply wraps [OrderedUnmarshal] with [encoding/binary.NativeEndian]
|
||||
(which can differ from platform to platform) as the ord parameter to [OrderedUnmarshal].
|
||||
|
||||
If you need to use a different or explicit [encoding/binary.ByteOrder],
|
||||
use [OrderedUnmarshal] directly instead.
|
||||
*/
|
||||
func Unmarshal(data []byte, v any) (err error) {
|
||||
|
||||
if err = OrderedUnmarshal(data, v, binary.NativeEndian); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
OrderedUnmarshal provides a slightly more flexible Golang convention unmarshaling
|
||||
for arbitrary binary data.
|
||||
|
||||
If v conforms to [encoding.BinaryUnmarshaler], then the unmarshal method will be called.
|
||||
|
||||
Otherwise, it wraps [encoding/binary.Read] with an internal buffer using ord
|
||||
as the [encoding/binary.ByteOrder] (endianness), so refer to that function
|
||||
for all caveats and other details.
|
||||
Note that [encoding/binary.Read] is *much* more strict than unmarshaling,
|
||||
and requires very basic typing.
|
||||
|
||||
If you have no need for a specific byte order/endianness or want to explicitly use this
|
||||
system's native byte order/endianness,
|
||||
use [Unmarshal] instead.
|
||||
*/
|
||||
func OrderedUnmarshal(data []byte, v any, ord binary.ByteOrder) (err error) {
|
||||
|
||||
var ok bool
|
||||
var b encoding.BinaryUnmarshaler
|
||||
var buf *bytes.Reader = bytes.NewReader(data)
|
||||
|
||||
if b, ok = v.(encoding.BinaryUnmarshaler); ok {
|
||||
if err = b.UnmarshalBinary(data); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = binary.Read(buf, ord, v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package encodingx aims to extend functionality of the stdlib [encoding] module.
|
||||
*/
|
||||
package encodingx
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package hexx aims to extend [encoding/hex] functionality.
|
||||
*/
|
||||
package hexx
|
||||
@@ -0,0 +1,30 @@
|
||||
package hexx
|
||||
|
||||
import (
|
||||
`io`
|
||||
)
|
||||
|
||||
/*
|
||||
HexString returns a custom-formatted hex representation of b.
|
||||
*/
|
||||
func HexString(b []byte, opts ...hexStrFmtrOpt) (out string) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
HexStringStream encodes a stream read from r and written into w.
|
||||
|
||||
HexStringStream will return cleanly if r returns no more bytes or if an [io.EOF] is encountered (the [io.EOF] will not be returned).
|
||||
Any other error will immediately halt reading/writing and will b returned in err.
|
||||
|
||||
If w is of a fixed capacity, it must be at least 2x the size of r's capacity.
|
||||
*/
|
||||
func HexStringStream(r io.Reader, w io.Writer, opts ...hexStrFmtrOpt) (read, wrtn uint64, err error) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package hexx
|
||||
|
||||
/*
|
||||
HexStrWithPrefix sets whether a prefix ("0x" by default, see [HexStrWithPrefixStr])
|
||||
*/
|
||||
func HexStrWithPrefix(pfx bool) (f hexStrFmtrOpt) {
|
||||
|
||||
f = func(h *hexStrFmtr) {
|
||||
if h.pfxStr == "" {
|
||||
h.pfxStr = "0x"
|
||||
}
|
||||
h.pfx = pfx
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
HexStrWithPrefixStr sets the prefix used if a prefix is to be included (see [HexStrWithPrefix]).
|
||||
|
||||
The default is "0x".
|
||||
*/
|
||||
func HexStrWithPrefixStr(pfx string) (f hexStrFmtrOpt) { return func(h *hexStrFmtr) { h.pfxStr = pfx } }
|
||||
|
||||
/*
|
||||
HexStrWithLower uses lowercase or uppercase hex character representation. The default is lowercase.
|
||||
|
||||
To use uppercase, specify this option with lower == false.
|
||||
*/
|
||||
func HexStrWithLower(lower bool) (f hexStrFmtrOpt) { return func(h *hexStrFmtr) { h.lower = lower } }
|
||||
|
||||
/*
|
||||
HexStrWithLeftPad specifies if left zero-padding should be used.
|
||||
|
||||
The default is true.
|
||||
*/
|
||||
func HexStrWithLeftPad(leftPad bool) (f hexStrFmtrOpt) {
|
||||
return func(h *hexStrFmtr) { h.noLeftPad = !leftPad }
|
||||
}
|
||||
|
||||
/*
|
||||
HexStrWithSegLeftPad specifies if left zero-padding should be used for each segment (see [HexStrWithSplit]).
|
||||
|
||||
The default is true.
|
||||
*/
|
||||
func HexStrWithSegLeftPad(segLeftPad bool) (f hexStrFmtrOpt) {
|
||||
return func(h *hexStrFmtr) { h.noSegmentLeftPad = !segLeftPad }
|
||||
}
|
||||
|
||||
/*
|
||||
HexStrWithSplit will split the hex string into "chunks" of numBytes bytes.
|
||||
|
||||
For example:
|
||||
numBytes == 0:
|
||||
0x0123456789abcdef
|
||||
|
||||
numBytes == 1:
|
||||
0x01 0x23 0x45 0x67 0x89 0xab 0xcd 0xef
|
||||
|
||||
numBytes == 2:
|
||||
0x0123 0x4567 0x89ab 0xcdef
|
||||
*/
|
||||
func HexStrWithSplit(numBytes uint) (f hexStrFmtrOpt) {
|
||||
return func(h *hexStrFmtr) { h.segmentBytes = numBytes }
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -0,0 +1,16 @@
|
||||
package hexx
|
||||
|
||||
type (
|
||||
hexStrFmtrOpt func(h *hexStrFmtr)
|
||||
|
||||
hexStrFmtr struct {
|
||||
pfx bool
|
||||
pfxStr string
|
||||
lower bool
|
||||
noLeftPad bool
|
||||
noSegmentLeftPad bool
|
||||
segmentBytes uint
|
||||
segmentSep string
|
||||
newlineBytes uint
|
||||
}
|
||||
)
|
||||
@@ -1,15 +1,47 @@
|
||||
module r00t2.io/goutils
|
||||
|
||||
go 1.24.5
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
golang.org/x/sys v0.34.0
|
||||
r00t2.io/sysutils v1.14.0
|
||||
github.com/olekukonko/tablewriter v1.1.4
|
||||
github.com/shirou/gopsutil/v4 v4.26.5
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/sys v0.46.0
|
||||
r00t2.io/sysutils v1.16.2
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
github.com/ebitengine/purego v0.10.1 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.3.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,13 +1,125 @@
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
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.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
|
||||
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
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/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
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/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
|
||||
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
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/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=
|
||||
github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
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.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
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/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
r00t2.io/sysutils v1.14.0/go.mod h1:ZJ7gZxFVQ7QIokQ5fPZr7wl0XO5Iu+LqtE8j3ciRINw=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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.16.2 h1:wI01UwZ/bXn/lzBiCpqDmzZCOWiK87kz04SB4xRw+W0=
|
||||
r00t2.io/sysutils v1.16.2/go.mod h1:iXK+ALOwIdRKjAJIE5USlkZ669SVDHBNNuYhunsznH8=
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/*
|
||||
Package iox includes extensions to the stdlib `io` module.
|
||||
|
||||
Not everything in here is considered fully stabilized yet,
|
||||
but it should be usable.
|
||||
*/
|
||||
package iox
|
||||
|
||||
+9
-1
@@ -5,5 +5,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
|
||||
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
|
||||
ErrChunkTooBig error = errors.New("chunk too big for method")
|
||||
ErrChunkTooSmall error = errors.New("chunk too small for buffer")
|
||||
ErrInvalidChunkSize error = errors.New("an invalid chunk size was passed")
|
||||
ErrNilCtx error = errors.New("a nil context was passed")
|
||||
ErrNilReader error = errors.New("a nil reader was passed")
|
||||
ErrNilWriter error = errors.New("a nil writer was passed")
|
||||
ErrShortRead error = errors.New("a read was cut short with no EOF")
|
||||
ErrShortWrite error = errors.New("a write was cut short with no error")
|
||||
)
|
||||
|
||||
+214
-8
@@ -1,20 +1,21 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
`context`
|
||||
`io`
|
||||
)
|
||||
|
||||
/*
|
||||
CopyBufN is a mix between io.CopyN and io.CopyBuffer.
|
||||
CopyBufN is a mix between [io.CopyN] and [io.CopyBuffer].
|
||||
|
||||
Despite what the docs may suggest, io.CopyN does NOT *read* n bytes from src AND write n bytes to dst.
|
||||
Despite what the docs may suggest, [io.CopyN] does NOT *read* n bytes from src AND write n bytes to dst.
|
||||
Instead, it always reads 32 KiB from src, and writes n bytes to dst.
|
||||
|
||||
There are, of course, cases where this is deadfully undesired.
|
||||
There are cases where this is dreadfully undesired.
|
||||
|
||||
One can, of course, use io.CopyBuffer, but this is a bit annoying since you then have to provide a buffer yourself.
|
||||
One can, of course, use [io.CopyBuffer], but this is a bit annoying since you then have to provide a buffer yourself.
|
||||
|
||||
This convenience-wraps io.CopyBuffer to have a similar signature to io.CopyN but properly uses n for both reading and writing.
|
||||
This convenience-wraps [io.CopyBuffer] to have a similar signature to [io.CopyN] but properly uses n for both reading and writing.
|
||||
*/
|
||||
func CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||
|
||||
@@ -32,10 +33,215 @@ func CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
|
||||
return
|
||||
}
|
||||
|
||||
// CopyBufWith allows for specifying a buffer allocator function, otherwise acts as CopyBufN.
|
||||
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||
// CopyCtxBufN copies from `src` to `dst`, `n` bytes at a time, interruptible by `ctx`.
|
||||
func CopyCtxBufN(ctx context.Context, dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||
|
||||
written, err = io.CopyBuffer(dst, src, bufFunc())
|
||||
var nr int
|
||||
var nw int
|
||||
var end bool
|
||||
var buf []byte
|
||||
|
||||
if ctx == nil {
|
||||
err = ErrNilCtx
|
||||
return
|
||||
}
|
||||
if n <= 0 {
|
||||
err = ErrBufTooSmall
|
||||
return
|
||||
}
|
||||
|
||||
endCopy:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
return
|
||||
default:
|
||||
buf = make([]byte, n)
|
||||
nr, err = src.Read(buf)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
end = true
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
buf = buf[:nr]
|
||||
|
||||
if nw, err = dst.Write(buf); err != nil {
|
||||
written += int64(nw)
|
||||
return
|
||||
}
|
||||
written += int64(nw)
|
||||
if len(buf) != nw {
|
||||
err = io.ErrShortWrite
|
||||
return
|
||||
}
|
||||
if end {
|
||||
break endCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CopyBufWith allows for specifying a buffer allocator function, otherwise acts as [CopyBufN].
|
||||
|
||||
bufFunc *MUST NOT* return a nil or len == 0 buffer. [ErrBufTooSmall] will be returned if it does.
|
||||
|
||||
This uses a fixed buffer size from a single call to `bufFunc`.
|
||||
If you need something with dynamic buffer sizing according to some state, use [CopyBufWithDynamic] instead.
|
||||
(Note that CopyBufWithDynamic is generally a little slower, but it should only be noticeable on very large amounts of data.)
|
||||
*/
|
||||
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||
|
||||
var buf []byte = bufFunc()
|
||||
|
||||
if buf == nil || len(buf) == 0 {
|
||||
err = ErrBufTooSmall
|
||||
return
|
||||
}
|
||||
|
||||
written, err = io.CopyBuffer(dst, src, buf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CopyBufWithDynamic is like [CopyBufWith] except it will call bufFunc after each previous buffer is written.
|
||||
|
||||
That is to say (using a particularly contrived example):
|
||||
|
||||
import time
|
||||
|
||||
func dynBuf() (b []byte) {
|
||||
|
||||
var t time.Time = time.Now()
|
||||
|
||||
b = make([]byte, t.Seconds())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Then:
|
||||
|
||||
CopyBufWithDynamic(w, r, dynBuf)
|
||||
|
||||
will use a buffer sized to the seconds of the time it reads in/writes out the next buffer, whereas with [CopyBufWith]:
|
||||
|
||||
CopyBufWith(w, r, dynBuf)
|
||||
|
||||
would use a *fixed* buffer size of whatever the seconds was equal to at the time of the *first call* to dynBuf.
|
||||
|
||||
`src` MUST return an [io.EOF] when its end is reached, but (as per e.g. [io.CopyBuffer]) the io.EOF error will not
|
||||
be returned from CopyBufWithDynamic. (Any/all other errors encountered will be returned, however, and copying will
|
||||
immediately cease.)
|
||||
*/
|
||||
func CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||
|
||||
var nr int
|
||||
var nw int
|
||||
var end bool
|
||||
var buf []byte
|
||||
|
||||
for {
|
||||
buf = bufFunc()
|
||||
if buf == nil || len(buf) == 0 {
|
||||
err = ErrBufTooSmall
|
||||
return
|
||||
}
|
||||
nr, err = src.Read(buf)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
end = true
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
buf = buf[:nr]
|
||||
|
||||
if nw, err = dst.Write(buf); err != nil {
|
||||
written += int64(nw)
|
||||
return
|
||||
}
|
||||
written += int64(nw)
|
||||
if len(buf) != nw {
|
||||
err = ErrShortWrite
|
||||
return
|
||||
}
|
||||
if end {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewChunker returns a [ChunkLocker] ready to use.
|
||||
func NewChunker(chunkSize uint) (c *ChunkLocker, err error) {
|
||||
|
||||
c = &ChunkLocker{}
|
||||
err = c.SetChunkLen(chunkSize)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewCtxIO returns a [CtxIO].
|
||||
func NewCtxIO(ctx context.Context, r io.Reader, w io.Writer, chunkSize uint) (c *CtxIO, err error) {
|
||||
|
||||
if r == nil {
|
||||
err = ErrNilReader
|
||||
return
|
||||
}
|
||||
if w == nil {
|
||||
err = ErrNilWriter
|
||||
return
|
||||
}
|
||||
|
||||
if chunkSize == 0 {
|
||||
err = ErrInvalidChunkSize
|
||||
return
|
||||
}
|
||||
|
||||
if ctx == nil {
|
||||
err = ErrNilCtx
|
||||
return
|
||||
}
|
||||
|
||||
c = &CtxIO{
|
||||
r: r,
|
||||
w: w,
|
||||
l: ChunkLocker{
|
||||
chunkLen: chunkSize,
|
||||
},
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
NewXIO returns a nil [XIO].
|
||||
|
||||
A weird "feature" of Golang is that a nil XIO is perfectly fine to use;
|
||||
it's completely stateless and only has pointer receivers that only work with passed in
|
||||
values so `new(XIO)` is completely unnecessary (as is NewXCopier).
|
||||
In other words, this works fine:
|
||||
|
||||
var xc *iox.XIO
|
||||
|
||||
if n, err = xc.Copy(w, r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
This function is just to maintain cleaner-looking code if you should so need it,
|
||||
or want an XIO without declaring one:
|
||||
|
||||
if n, err = iox.NewXCopier().Copy(w, r); err != nil {
|
||||
return
|
||||
}
|
||||
*/
|
||||
func NewXIO() (x *XIO) {
|
||||
// No-op lel
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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) Len() (unread int) {
|
||||
return c.buf.Len()
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+214
-1
@@ -1,8 +1,221 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`context`
|
||||
`io`
|
||||
`sync`
|
||||
)
|
||||
|
||||
type (
|
||||
// RuneWriter matches the behavior of *(bytes.Buffer).WriteRune and *(bufio.Writer).WriteRune
|
||||
/*
|
||||
RuneWriter matches the behavior of [bytes.Buffer.WriteRune] and [bufio.Writer.WriteRune].
|
||||
|
||||
(Note that this package does not have a "RuneReader"; see [io.RuneReader] instead.)
|
||||
*/
|
||||
RuneWriter interface {
|
||||
WriteRune(r rune) (n int, err error)
|
||||
}
|
||||
|
||||
// Copier matches the signature/behavior of [io.Copy]. Implemented by [XIO].
|
||||
Copier interface {
|
||||
Copy(dst io.Writer, src io.Reader) (written int64, err error)
|
||||
}
|
||||
|
||||
// CopyBufferer matches the signature/behavior of [io.CopyBuffer]. Implemented by [XIO].
|
||||
CopyBufferer interface {
|
||||
CopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error)
|
||||
}
|
||||
|
||||
// SizedCopier matches the signature/behavior of [io.CopyN]. Implemented by [XIO].
|
||||
SizedCopier interface {
|
||||
CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
|
||||
}
|
||||
|
||||
// SizedCopyBufferer matches the signature/behavior of [CopyBufN]. Implemented by [XIO].
|
||||
SizedCopyBufferer interface {
|
||||
CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
|
||||
}
|
||||
|
||||
// SizedCopyBufferInvoker matches the signature/behavior of [CopyBufWith]. Implemented by [XIO].
|
||||
SizedCopyBufferInvoker interface {
|
||||
CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error)
|
||||
}
|
||||
|
||||
// DynamicSizedCopyBufferInvoker matches the signature/behavior of [CopyBufWithDynamic]. Implemented by [XIO].
|
||||
DynamicSizedCopyBufferInvoker interface {
|
||||
CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error)
|
||||
}
|
||||
|
||||
/*
|
||||
Chunker is used by both [ContextReader] and [ContextWriter] to set/get the current chunk size.
|
||||
Chunking is inherently required to be specified in order to interrupt reads/writes/copies with a [context.Context].
|
||||
|
||||
Implementations *must* use a [sync.RWMutex] to get (RLock) and set (Lock) the chunk size.
|
||||
The chunk size *must not* be directly accessible to maintain concurrency safety assumptions.
|
||||
*/
|
||||
Chunker interface {
|
||||
// GetChunkLen returns the current chunk size/length in bytes.
|
||||
GetChunkLen() (size uint)
|
||||
// SetChunkLen sets the current chunk size/length in bytes.
|
||||
SetChunkLen(size uint) (err error)
|
||||
}
|
||||
|
||||
/*
|
||||
ChunkReader implements a chunking reader.
|
||||
Third-party implementations *must* respect the chunk size locking (see [Chunker]).
|
||||
|
||||
The Read method should read in chunks of the internal chunk size.
|
||||
*/
|
||||
ChunkReader interface {
|
||||
io.Reader
|
||||
Chunker
|
||||
}
|
||||
|
||||
/*
|
||||
ChunkWriter implements a chunking writer.
|
||||
Third-party implementations *must* respect the chunk size locking (see [Chunker]).
|
||||
|
||||
The Write method should write out in chunks of the internal chunk size.
|
||||
*/
|
||||
ChunkWriter interface {
|
||||
io.Writer
|
||||
Chunker
|
||||
}
|
||||
|
||||
// ChunkReadWriter implements a chunking reader/writer.
|
||||
ChunkReadWriter interface {
|
||||
ChunkReader
|
||||
ChunkWriter
|
||||
}
|
||||
|
||||
/*
|
||||
ContextSetter allows one to set an internal context.
|
||||
|
||||
A nil context should return an error.
|
||||
*/
|
||||
ContextSetter interface {
|
||||
SetContext(context context.Context) (err error)
|
||||
}
|
||||
|
||||
/*
|
||||
ContextCopier is defined to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||
|
||||
The Copy method should use an internal context and chunk size
|
||||
(and thus wrap [CopyCtxBufN] internally on an external call to Copy, etc.).
|
||||
*/
|
||||
ContextCopier interface {
|
||||
Copier
|
||||
Chunker
|
||||
ContextSetter
|
||||
SizedCopyBufferer
|
||||
}
|
||||
|
||||
/*
|
||||
ContextReader is primarily here to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||
|
||||
The Read method should use an internal context and chunk size.
|
||||
|
||||
The ReadWithContext method should use an internal chunk size.
|
||||
*/
|
||||
ContextReader interface {
|
||||
ChunkReader
|
||||
ContextSetter
|
||||
ReadWithContext(ctx context.Context, p []byte) (n int64, err error)
|
||||
}
|
||||
|
||||
/*
|
||||
ContextWriter is primarily here to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||
|
||||
The Write method should use an internal context.
|
||||
|
||||
The WriteWithContext should use an internal chunk size.
|
||||
*/
|
||||
ContextWriter interface {
|
||||
ChunkWriter
|
||||
ContextSetter
|
||||
WriteWithContext(ctx context.Context, p []byte) (n int64, err error)
|
||||
WriteNWithContext(ctx context.Context, p []byte, n int64) (written int64, err error)
|
||||
}
|
||||
|
||||
/*
|
||||
ContextReadWriter is primarily here to allow for consumer-provided types.
|
||||
|
||||
See [CtxIO] for a package-provided type.
|
||||
*/
|
||||
ContextReadWriter interface {
|
||||
ContextReader
|
||||
ContextWriter
|
||||
}
|
||||
|
||||
/*
|
||||
LenReader allows one to read bytes (conforming to [io.Reader]),
|
||||
and also provides a Len() method which returns an int of remaining unread bytes.
|
||||
[bytes.Buffer] and [bytes.Reader] conforms to this, but see [CtxIO] for
|
||||
a package-provided type.
|
||||
*/
|
||||
LenReader interface {
|
||||
io.Reader
|
||||
Len() (unread int)
|
||||
}
|
||||
)
|
||||
|
||||
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]
|
||||
* [LenReader]
|
||||
|
||||
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{}
|
||||
)
|
||||
|
||||
+7
-1
@@ -1,9 +1,15 @@
|
||||
- ScopedLogger, take an io.Writer for each log level
|
||||
|
||||
- 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?):
|
||||
@@ -12,7 +18,7 @@
|
||||
-- log.LlongFile and log.Lshortfile flags don't currently work properly for StdLogger/FileLogger; they refer to the file in logging package rather than the caller.
|
||||
-- ZeroLog seems to be able to do it, take a peek there.
|
||||
|
||||
- StdLogger2; where stdout and stderr are both logged to depending on severity level.
|
||||
- StdLvlLogger; where stdout and stderr are both logged to depending on severity level.
|
||||
- make configurable via OR bitmask
|
||||
|
||||
- Suport remote loggers? (eventlog, syslog, journald)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//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.
|
||||
|
||||
@@ -23,8 +22,8 @@ const (
|
||||
// LogUndefined indicates an undefined Logger type.
|
||||
const LogUndefined bitmask.MaskBit = iota
|
||||
const (
|
||||
// LogJournald flags a SystemDLogger Logger type.
|
||||
LogJournald = 1 << iota
|
||||
// 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.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
|
||||
|
||||
package logging
|
||||
|
||||
var (
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
)
|
||||
|
||||
// Flags for logger configuration. These are used internally.
|
||||
// LogUndefined indicates an undefined Logger type.
|
||||
const LogUndefined bitmask.MaskBit = 0
|
||||
const (
|
||||
// LogUndefined indicates an undefined Logger type.
|
||||
LogUndefined bitmask.MaskBit = 1 << iota
|
||||
// LogWinLogger indicates a WinLogger Logger type (Event Log).
|
||||
LogWinLogger
|
||||
LogWinLogger bitmask.MaskBit = 1 << iota
|
||||
// LogFile flags a FileLogger Logger type.
|
||||
LogFile
|
||||
// LogStdout flags a StdLogger Logger type.
|
||||
|
||||
@@ -17,7 +17,9 @@ func (l *logPrio) HasFlag(prio logPrio) (hasFlag bool) {
|
||||
m = bitmask.NewMaskBitExplicit(uint(*l))
|
||||
p = bitmask.NewMaskBitExplicit(uint(prio))
|
||||
|
||||
hasFlag = m.HasFlag(*p)
|
||||
// Use IsOneOf instead in case PriorityAll is passed for prio.
|
||||
// hasFlag = m.HasFlag(*p)
|
||||
hasFlag = m.IsOneOf(*p)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ func (l *logWriter) Write(b []byte) (n int, err error) {
|
||||
|
||||
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)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
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, logFlags int, logPaths ...string) (err error) {
|
||||
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
|
||||
|
||||
var l Logger
|
||||
var exists bool
|
||||
@@ -36,9 +36,9 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID,
|
||||
}
|
||||
|
||||
if logPaths != nil {
|
||||
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...)
|
||||
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...)
|
||||
} else {
|
||||
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags)
|
||||
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
|
||||
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
|
||||
|
||||
// Linux is excluded because it has its own.
|
||||
|
||||
|
||||
+46
-15
@@ -10,32 +10,63 @@ import (
|
||||
)
|
||||
|
||||
/*
|
||||
GetLogger returns an instance of Logger that best suits your system's capabilities. Note that this is a VERY generalized interface to the Windows Event Log.
|
||||
GetLogger returns an instance of Logger that best suits your system's capabilities.
|
||||
Note that this is a VERY generalized interface to the Windows Event Log to conform with multiplatform compat.
|
||||
You'd have a little more flexibility with [GetLoggerWindows] (this function wraps that one).
|
||||
If you need more custom behavior than that, I recommend using [golang.org/x/sys/windows/svc/eventlog] directly
|
||||
(or using another logging module).
|
||||
|
||||
If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).
|
||||
|
||||
The `prefix` correlates to the `source` parameter in [GetLoggerWindows], and this function inherently uses [DefaultEventID],
|
||||
but otherwise it remains the same as [GetLoggerWindows] - refer to it for documentation on the other parameters.
|
||||
|
||||
If you call [GetLogger], you will only get a single ("best") logger your system supports.
|
||||
If you want to log to multiple [Logger] destinations at once (or want to log to an explicit [Logger] type),
|
||||
use [GetMultiLogger].
|
||||
*/
|
||||
func GetLogger(enableDebug bool, prefix string, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
||||
|
||||
if logger, err = GetLoggerWindows(enableDebug, prefix, DefaultEventID, logConfigFlags, logPaths...); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetLoggerWindows returns an instance of Logger that best suits your system's capabilities.
|
||||
This is a slightly less (but still quite) generalized interface to the Windows Event Log than [GetLogger].
|
||||
|
||||
If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
|
||||
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.
|
||||
|
||||
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.
|
||||
`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.
|
||||
|
||||
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).
|
||||
`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.
|
||||
Currently this will almost always return a WinLogger.
|
||||
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 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.
|
||||
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, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
||||
func GetLoggerWindows(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
||||
|
||||
var logPath string
|
||||
var logFlags bitmask.MaskBit
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestDefaultLogger(t *testing.T) {
|
||||
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||
}
|
||||
|
||||
if l, err = GetLogger(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
|
||||
if l, err = GetLoggerWindows(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
|
||||
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestMultiLogger(t *testing.T) {
|
||||
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
if err = l.AddDefaultLogger("DefaultLogger", DefaultEventID, logFlags, tempfilePath); err != nil {
|
||||
if err = l.AddDefaultLogger("DefaultLogger", logFlags, tempfilePath); err != nil {
|
||||
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package mapsx includes functions that probably should have been in [maps] but aren't.
|
||||
*/
|
||||
package mapsx
|
||||
@@ -0,0 +1,9 @@
|
||||
package mapsx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("key not found")
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,10 @@
|
||||
package netx
|
||||
|
||||
import (
|
||||
`net/netip`
|
||||
)
|
||||
|
||||
var (
|
||||
ip4In6Legacy netip.Prefix = netip.MustParsePrefix("::/96")
|
||||
ip4In6Modern netip.Prefix = netip.MustParsePrefix("::ffff:0:0/96")
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadChars error = errors.New("netx/dnsx: invalid characters/encoding were encountered")
|
||||
ErrBadLabelLen error = errors.New("netx/dnsx: a label with invalid length was encountered")
|
||||
ErrBadPtrLen error = errors.New("netx/dnsx: a PTR record with invalid length was encountered")
|
||||
ErrBadPtrRoot error = errors.New("netx/dnsx: a PTR record with invalid root encountered")
|
||||
)
|
||||
@@ -0,0 +1,571 @@
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`encoding/base32`
|
||||
`fmt`
|
||||
`math`
|
||||
`net`
|
||||
`net/netip`
|
||||
`strings`
|
||||
|
||||
`go4.org/netipx`
|
||||
`r00t2.io/goutils/stringsx`
|
||||
)
|
||||
|
||||
/*
|
||||
AddrFromPtr returns a [net/netip.Addr] from a PTR record.
|
||||
|
||||
It is the inverse of [AddrToPtr].
|
||||
|
||||
See also [IpFromPtr].
|
||||
*/
|
||||
func AddrFromPtr(s string) (ip netip.Addr, err error) {
|
||||
|
||||
var idx int
|
||||
var ipStr string
|
||||
var tmpStr string
|
||||
var spl []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
switch len(spl) {
|
||||
case 6:
|
||||
if strings.Join(spl[4:], ".") != "in-addr.arpa" {
|
||||
err = ErrBadPtrRoot
|
||||
return
|
||||
}
|
||||
ipStr = fmt.Sprintf("%s.%s.%s.%s", spl[3], spl[2], spl[1], spl[0])
|
||||
case 34:
|
||||
if strings.Join(spl[32:], ".") != "ip6.arpa" {
|
||||
err = ErrBadPtrRoot
|
||||
return
|
||||
}
|
||||
tmpStr = stringsx.Reverse(strings.ReplaceAll(strings.Join(spl[:32], ""), ".", ""))
|
||||
for idx = 0; idx < len(tmpStr); idx++ {
|
||||
if idx%4 == 0 && idx != 0 {
|
||||
ipStr += ":"
|
||||
}
|
||||
ipStr += string(rune(tmpStr[idx]))
|
||||
}
|
||||
default:
|
||||
err = ErrBadPtrLen
|
||||
return
|
||||
}
|
||||
|
||||
if ip, err = netip.ParseAddr(ipStr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
AddrToPtr returns a PTR record from ip.
|
||||
|
||||
It is the inverse of [AddrFromPtr].
|
||||
|
||||
It includes the root label at the end (the trailing period).
|
||||
*/
|
||||
func AddrToPtr(ip netip.Addr) (s string) {
|
||||
|
||||
var idx int
|
||||
var b []byte
|
||||
var ipStr string
|
||||
|
||||
if ip.Is6() {
|
||||
ipStr = stringsx.Reverse(strings.ReplaceAll(ip.StringExpanded(), ":", ""))
|
||||
ipStr = strings.Join(
|
||||
strings.Split(ipStr, ""),
|
||||
".",
|
||||
)
|
||||
s = fmt.Sprintf("%s.ip6.arpa.", ipStr)
|
||||
} else {
|
||||
b = make([]byte, 4)
|
||||
copy(b, ip.AsSlice())
|
||||
for idx = len(b) - 1; idx >= 0; idx-- {
|
||||
ipStr += fmt.Sprintf("%d.", b[idx])
|
||||
}
|
||||
s = fmt.Sprintf("%s.in-addr.arpa.", ipStr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
DnsStrToWire returns a wire-format of a DNS name.
|
||||
|
||||
No validation or conversion (other than to wire format) is performed,
|
||||
and it is expected that any IDN(A)/Punycode translation has
|
||||
*already been performed* such that recordNm is in the ASCII form.
|
||||
(See [IsFqdn] for more information on IDN(A)/Punycode.)
|
||||
|
||||
For encoding reasons, if any given label/segment has a length of 0 or greater than 255 ([math.MaxUint8]),
|
||||
[ErrBadLabelLen] will be returned.
|
||||
|
||||
See [DnsWireToStr] for the inverse.
|
||||
*/
|
||||
func DnsStrToWire(recordNm string) (recordNmBytes []byte, err error) {
|
||||
|
||||
var cLen int
|
||||
var c []byte
|
||||
var cStr string
|
||||
var spl []string
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
spl = strings.Split(strings.TrimSuffix(recordNm, "."), ".")
|
||||
for _, cStr = range spl {
|
||||
c = []byte(cStr)
|
||||
cLen = len(c)
|
||||
if !(cLen > 0 && cLen <= math.MaxUint8) {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
buf.Write(append([]byte{uint8(cLen)}, c...))
|
||||
}
|
||||
|
||||
recordNmBytes = append(buf.Bytes(), 0x00)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
DnsWireToStr is the inverse of [DnsStrToWire]. A trailing . is not included.
|
||||
|
||||
For decoding reasons, it will exit with [ErrBadLabelLen] if recordNmBytes is nil/empty or
|
||||
if no terminating nullbyte is found after 256 label characters have been encountered.
|
||||
*/
|
||||
func DnsWireToStr(recordNmBytes []byte) (recordNm string, err error) {
|
||||
|
||||
var c []byte
|
||||
var cLen uint8
|
||||
var arrLen int
|
||||
var numChars int
|
||||
var labels []string
|
||||
var buf *bytes.Buffer
|
||||
|
||||
if recordNmBytes == nil || len(recordNmBytes) == 0 {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
buf = bytes.NewBuffer(recordNmBytes)
|
||||
labels = make([]string, 0)
|
||||
|
||||
arrLen = len(recordNmBytes)
|
||||
for {
|
||||
if cLen, err = buf.ReadByte(); err != nil {
|
||||
return
|
||||
}
|
||||
if cLen == 0x00 {
|
||||
break
|
||||
}
|
||||
numChars += int(cLen)
|
||||
if numChars > 255 {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
if numChars > arrLen {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
c = buf.Next(int(cLen))
|
||||
labels = append(labels, string(c))
|
||||
}
|
||||
|
||||
recordNm = strings.Join(labels, ".")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpFromPtr is like [AddrFromPtr] but with a [net.IP] instead.
|
||||
|
||||
It is the inverse of [IpToPtr].
|
||||
*/
|
||||
func IpFromPtr(s string) (ip net.IP, err error) {
|
||||
|
||||
var a netip.Addr
|
||||
|
||||
if a, err = AddrFromPtr(s); err != nil {
|
||||
return
|
||||
}
|
||||
ip = net.IP(a.AsSlice())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpToPtr is like [AddrToPtr] but with a [net.IP] instead.
|
||||
|
||||
It is the inverse of [IpFromPtr].
|
||||
*/
|
||||
func IpToPtr(ip net.IP) (s string) {
|
||||
|
||||
var a netip.Addr
|
||||
|
||||
a, _ = netipx.FromStdIP(ip)
|
||||
s = AddrToPtr(a)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdn returns a boolean indicating if s is an FQDN that strictly adheres to RFC format requirements.
|
||||
It performs no lookups/resolution attempts or network operations otherwise.
|
||||
|
||||
It will return true for the "apex record" (e.g. the "naked domain"), as this is a valid assignable FQDN.
|
||||
|
||||
It will return false for wildcard records (see [IsFqdnWildcard]).
|
||||
|
||||
s may or may not end in a period (the root zone; "absolute" FQDNs) (0x00 in wire format).
|
||||
|
||||
# TLDs
|
||||
|
||||
Because valid TLDs are fairly dynamic and can change frequently,
|
||||
validation is *not* performed against the TLD itself.
|
||||
This only ensures that s has a TLD label conforming to the character rules in the referenced RFCs.
|
||||
See [golang.org/x/net/publicsuffix] if precise TLD validation is required (though true TLD validation generally
|
||||
requires fetching the current TLD lists from IANA at runtime like [github.com/bombsimon/tld-validator]).
|
||||
|
||||
# Special RFC-Defined Accommodations
|
||||
|
||||
RFC 2181 [§ 11] specifies that site-local DNS software may accommodate non-RFC-conforming rules.
|
||||
This function may and likely will return false for these site-local deviations.
|
||||
The Lookup* functions/mthods in [net] should be used to validate in these casts
|
||||
if that accommodation is necessary.
|
||||
|
||||
Note that underscores are not valid for "true" FQDNs as they are only valid for e.g. SRV record names,
|
||||
TXT records, etc. - not A/AAAA/CNAME, etc. - see RFC 8553 for details.
|
||||
|
||||
See the following functions for allowing additional syntax/rule validation
|
||||
that have record-type-specific accommodations made:
|
||||
|
||||
* [IsFqdnDefinedTxt]
|
||||
* [IsFqdnNsec3]
|
||||
* [IsFqdnSrv]
|
||||
* [IsFqdnWildcard]
|
||||
|
||||
# RFC Coverage
|
||||
|
||||
This function should conform properly to:
|
||||
|
||||
* RFC 952
|
||||
* RFC 1034 and RFC 1035
|
||||
* RFC 1123
|
||||
* RFC 2181 (selectively, see above)
|
||||
|
||||
preferring the most up-to-date rules where relevant (e.g. labels may start with digits, as per RFC 1123).
|
||||
It enforces/checks label and overall length limits as defined by RFC.
|
||||
|
||||
# IDN(A) and Punycode
|
||||
|
||||
Note that it expects the ASCII-only/presentation form of a record name and
|
||||
will not perform any IDN/IDNA nor Punycode translation.
|
||||
If a caller anticipates FQDNs in their localized format,
|
||||
the caller must perform translation first
|
||||
(via e.g. [gitlab.com/golang-commonmark/puny], [golang.org/x/net/idna], etc.).
|
||||
|
||||
To reiterate, IDN/IDNA:
|
||||
|
||||
* RFC 3490
|
||||
* RFC 5890
|
||||
* RFC 5891
|
||||
* RFC 5892
|
||||
* RFC 5893
|
||||
* RFC 5894
|
||||
|
||||
and Punycode (RFC 3492) *MUST* use their ASCII forms, NOT the localized/Unicode formats.
|
||||
|
||||
[§ 11]: https://datatracker.ietf.org/doc/html/rfc2181#section-11
|
||||
*/
|
||||
func IsFqdn(s string) (fqdn bool) {
|
||||
|
||||
var lbl string
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
for _, lbl = range lbls {
|
||||
if !IsLabel(lbl) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fqdn = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnDefinedTxt is like [IsFqdn] but explicitly *only* allows fully-qualified
|
||||
RFC-defined TXT "subtypes":
|
||||
|
||||
* ACME DNS-01 (RFC 8555)
|
||||
* BIMI (RFC draft [bimi])
|
||||
* DKIM (RFC 6376, RFC 8301, RFC 8463)
|
||||
* DKIM ATPS (RFC 6541)
|
||||
* DMARC (RFC 7489, RFC 9091, RFC 9616)
|
||||
* MTA-STS (RFC 8461)
|
||||
* TLSRPT (RFC 8460)
|
||||
|
||||
Note that the following TXT "subtypes" do not have special formatting in labels/name,
|
||||
and thus are not covered by this function:
|
||||
|
||||
* SPF (RFC 4408, RFC 7208)
|
||||
|
||||
[bimi]: https://datatracker.ietf.org/doc/html/draft-brand-indicators-for-message-identification
|
||||
*/
|
||||
func IsFqdnDefinedTxt(fqdn string) (isOk bool) {
|
||||
|
||||
var lbls []string = strings.Split(fqdn, ".")
|
||||
|
||||
if lbls == nil || len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
switch lbls[0] {
|
||||
case "_dmarc", "_mta-sts", "_acme-challenge":
|
||||
if len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
isOk = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
case "_smtp":
|
||||
if len(lbls) <= 2 {
|
||||
return
|
||||
}
|
||||
if lbls[1] != "_tls" {
|
||||
return
|
||||
}
|
||||
isOk = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
default:
|
||||
if !IsLabel(lbls[0]) {
|
||||
return
|
||||
}
|
||||
switch lbls[1] {
|
||||
case "_domainkey", "_atps", "_bimi":
|
||||
isOk = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnNsec3 confirms (partially) that s is a valid NSEC3 record name.
|
||||
|
||||
Note that due to the record name being a base32 encoding of a *hash*, the validity
|
||||
can't be 100% confirmed with certainty - only basic checks can be done.
|
||||
|
||||
NSEC3 can be found via:
|
||||
|
||||
* RFC 5155
|
||||
* RFC 6840
|
||||
* RFC 6944
|
||||
* RFC 7129
|
||||
* RFC 8198
|
||||
* RFC 9077
|
||||
* RFC 9157
|
||||
* RFC 9276
|
||||
* RFC 9905
|
||||
|
||||
At the time of writing, only one hashing algorithm (SHA-1) has been specified.
|
||||
However, because this function does not check against the IANA registration at runtime,
|
||||
it's possible that this changes but the library may not immediately reflect this.
|
||||
*/
|
||||
func IsFqdnNsec3(s string) (maybeNsec3 bool) {
|
||||
|
||||
var h []byte
|
||||
var err error
|
||||
var isAscii bool
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
if len(lbls) <= 2 {
|
||||
return
|
||||
}
|
||||
if len(lbls[0]) != 32 { // SHA1 is 160 bits/20 bytes digest, which is always 32 chars in base32(hex)
|
||||
return
|
||||
}
|
||||
if h, err = base32.StdEncoding.DecodeString(strings.ToUpper(lbls[0])); err != nil {
|
||||
return
|
||||
}
|
||||
if isAscii, err = stringsx.IsAsciiSpecial(
|
||||
strings.ToLower(lbls[0]),
|
||||
false, false, false, false,
|
||||
[]byte{
|
||||
// Normally, Base32 goes A-Z, 2-7
|
||||
// but NSEC3 uses Base32Hex (RFC 4648 § 7),
|
||||
// which is 0-9A-V
|
||||
'0', '1', '2', '3', '4', '5',
|
||||
'6', '7', '8', '9', 'a', 'b',
|
||||
'c', 'd', 'e', 'f', 'g', 'h',
|
||||
'j', 'k', 'm', 'n', 'p', 'q',
|
||||
'r', 's', 't', 'u', 'v',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if isAscii {
|
||||
return
|
||||
}
|
||||
if len(h) != 20 {
|
||||
return
|
||||
}
|
||||
maybeNsec3 = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnSrv is like [IsFqdn], but explicitly *only* allows fully-qualified SRV records
|
||||
(i.e. underscores must start the first two labels, and there must be at least two additional
|
||||
labels after these labels).
|
||||
|
||||
Note that the protocol is not checked for validity, as that would require runtime
|
||||
validation against a resource liable to change and would need to be fetched dynamically - see
|
||||
the [IANA Protocol Numbers registry].
|
||||
|
||||
[IANA Protocol Numbers registry]: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
||||
*/
|
||||
func IsFqdnSrv(s string) (srv bool) {
|
||||
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
if len(lbls) <= 4 {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(lbls[0], "_") {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(lbls[1], "_") {
|
||||
return
|
||||
}
|
||||
srv = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsFqdnWildcard is like [IsFqdn] but explicitly *only* allows fully-qualified wildcard records.
|
||||
func IsFqdnWildcard(s string) (wildcard bool) {
|
||||
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
if lbls[0] != "*" {
|
||||
return
|
||||
}
|
||||
wildcard = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsLabel returns true if s is a valid DNS label for standard records.
|
||||
func IsLabel(s string) (isLbl bool) {
|
||||
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(s, "-") {
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(s, "-") {
|
||||
return
|
||||
}
|
||||
if isLbl, err = stringsx.IsAsciiSpecial(
|
||||
s,
|
||||
false, false, false, false,
|
||||
[]byte{
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l',
|
||||
'm', 'n', 'o', 'p', 'q', 'r',
|
||||
's', 't', 'u', 'v', 'w', 'x',
|
||||
'y', 'z', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9',
|
||||
'-',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsPtr returns true if s is a PTR (also called an "rDNS" or "reverse DNS" record) name.
|
||||
|
||||
If true, the IP is returned as well (otherwise it will be nil).
|
||||
*/
|
||||
func IsPtr(s string) (isPtr bool, addr net.IP) {
|
||||
|
||||
var err error
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if len(lbls) < 6 {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = AddrFromPtr(s); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
isPtr = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// commonFqdn is used to validate some rules common to all record names.
|
||||
func commonFqdn(s string) (isOk bool) {
|
||||
|
||||
var err error
|
||||
var lbl string
|
||||
var isAscii bool
|
||||
var labels []string
|
||||
var domstr string = strings.ToLower(strings.TrimSuffix(s, "."))
|
||||
|
||||
if isAscii, err = stringsx.IsAsciiSpecial(
|
||||
domstr, false, false, false, false,
|
||||
[]byte{
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l',
|
||||
'm', 'n', 'o', 'p', 'q', 'r',
|
||||
's', 't', 'u', 'v', 'w', 'x',
|
||||
'y', 'z', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9',
|
||||
'-', '_', '.',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if !isAscii {
|
||||
return
|
||||
}
|
||||
|
||||
if (len(domstr) + 1) > 255 { // +1 for root label
|
||||
return
|
||||
}
|
||||
labels = strings.Split(domstr, ".")
|
||||
|
||||
for _, lbl = range labels {
|
||||
if len(lbl) < 1 || len(lbl) > 63 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO?
|
||||
|
||||
isOk = true
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
`net/netip`
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPtr(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var ptr string
|
||||
var ip netip.Addr
|
||||
var ipStr string = "::ffff:192.168.0.1"
|
||||
var ptrStr string = "1.0.0.0.8.a.0.c.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa."
|
||||
|
||||
if ip, err = AddrFromPtr(ptrStr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("PTR -> Addr: %s -> %s", ptrStr, ip.String())
|
||||
if ip.String() != ipStr {
|
||||
t.Fatalf("expect IP %v, got %v", ipStr, ip.String())
|
||||
}
|
||||
|
||||
ptr = AddrToPtr(ip)
|
||||
if ptr != ptrStr {
|
||||
t.Fatalf("expect PTR %v, got %v", ptrStr, ptr)
|
||||
}
|
||||
t.Logf("Addr -> PTR: %s -> %s", ip.String(), ptr)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package netx includes extensions to the stdlib [net] module.
|
||||
*/
|
||||
package netx
|
||||
@@ -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")
|
||||
)
|
||||
+706
@@ -0,0 +1,706 @@
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
FamilyToVer returns a more "human-friendly" IP version from a system/lower-level IP family
|
||||
([AFUnspec], [AFInet], [AFInet6]).
|
||||
|
||||
ipVer will be int(4) for [AFInet], int(6) for [AFInet6], int(0) for [AFUnspec], or
|
||||
int(-1) for an unknown family.
|
||||
*/
|
||||
func FamilyToVer(family uint16) (ipVer int) {
|
||||
|
||||
switch family {
|
||||
case AFInet:
|
||||
ipVer = 4
|
||||
case AFInet6:
|
||||
ipVer = 6
|
||||
case AFUnspec:
|
||||
ipVer = 0
|
||||
default:
|
||||
ipVer = -1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetAddrFamily returns the network family of a [net/netip.Addr].
|
||||
|
||||
See also [GetIpFamily].
|
||||
|
||||
Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6).
|
||||
(See [FamilyToVer] to get the associated higher-level value.)
|
||||
|
||||
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].
|
||||
|
||||
Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6).
|
||||
(See [FamilyToVer] to get the associated higher-level value.)
|
||||
|
||||
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.
|
||||
|
||||
See also [IpRfcStr] for providing an IP address as a 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
|
||||
}
|
||||
|
||||
/*
|
||||
IpRfcStr implements [IpRfc]/[AddrRfc] for string representations of an IP address s.
|
||||
|
||||
If s is an IPv6 address already in the bracketed RFC format,
|
||||
then rfcStr will be equal to s.
|
||||
|
||||
If s is not a string representation of an IP address, rfcStr will be empty.
|
||||
|
||||
See [IpStripRfcStr] for the inverse (removing any brackets from s if present).
|
||||
*/
|
||||
func IpRfcStr(s string) (rfcStr string) {
|
||||
|
||||
var ip net.IP
|
||||
|
||||
if !IsIpAddr(s) {
|
||||
return
|
||||
}
|
||||
if IsBracketedIp6(s) {
|
||||
rfcStr = s
|
||||
return
|
||||
}
|
||||
ip = net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return
|
||||
}
|
||||
rfcStr = IpRfc(ip)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpStripRfcStr returns IP address string s without any brackets.
|
||||
|
||||
If s is not a valid IP address, stripStr will be empty.
|
||||
*/
|
||||
func IpStripRfcStr(s string) (stripStr string) {
|
||||
|
||||
if !IsIpAddr(s) {
|
||||
return
|
||||
}
|
||||
if !IsBracketedIp6(s) {
|
||||
stripStr = s
|
||||
return
|
||||
}
|
||||
stripStr = strings.TrimPrefix(s, "[")
|
||||
stripStr = strings.TrimSuffix(stripStr, "]")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
IpVerStr provides the IP family of IP address/network string s.
|
||||
|
||||
s may be one of the following formats/syntaxes:
|
||||
|
||||
* 203.0.113.0
|
||||
* 203.0.113.1
|
||||
* 203.0.113.0/24
|
||||
* 203.0.113.1/24
|
||||
* 2001:db8::
|
||||
* 2001:db8::1
|
||||
* 2001:db8::/32
|
||||
* 2001:db8::1/32
|
||||
* [2001:db8::]
|
||||
* [2001:db8::1]
|
||||
|
||||
|
||||
Unlike [GetAddrFamily]/[GetIpFamily], this returns a more "friendly"
|
||||
version - if s is not valid syntax, ipVer will be int(0),
|
||||
otherwise ipVer will be int(4) for family IPv4 and int(6) for family IPv6.
|
||||
(See [VerToFamily] to get the associated system/lower-level value.)
|
||||
*/
|
||||
func IpVerStr(s string) (ipVer int) {
|
||||
|
||||
var err error
|
||||
var ipstr string
|
||||
var p netip.Prefix
|
||||
|
||||
ipstr = strings.TrimPrefix(s, "[")
|
||||
ipstr = strings.TrimSuffix(ipstr, "]")
|
||||
|
||||
if p, err = netip.ParsePrefix(ipstr); err != nil {
|
||||
return
|
||||
}
|
||||
if p.Addr().Is6() {
|
||||
ipVer = 6
|
||||
} else {
|
||||
ipVer = 4
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsBracketedIp6 returns a boolean indicating if s is a valid bracket-enclosed IPv6 in string format
|
||||
(e.g. "[2001:db8::1]").
|
||||
|
||||
It will return false for *non-bracketed* IPv6 addresses (e.g. "2001:db8::1"), IPv4 addresses,
|
||||
or if s is not a valid IPv6 address string.
|
||||
|
||||
[IpRfcStr] or [IpStripRfcStr] can be used to coerce a string to a specific format.
|
||||
*/
|
||||
func IsBracketedIp6(s string) (isBrktdIp bool) {
|
||||
|
||||
var ip net.IP
|
||||
var ipstr string
|
||||
|
||||
if IpVerStr(s) != 6 {
|
||||
return
|
||||
}
|
||||
|
||||
ipstr = strings.TrimPrefix(s, "[")
|
||||
ipstr = strings.TrimSuffix(ipstr, "]")
|
||||
|
||||
if ip = net.ParseIP(ipstr); ip == nil {
|
||||
return
|
||||
}
|
||||
|
||||
isBrktdIp = ipstr == s
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsIpAddr returns a boolean indicating if s is an IP address (either IPv4 or IPv6) in string format.
|
||||
|
||||
For IPv6, it will return true for both of these formats:
|
||||
|
||||
* 2001:db8::1
|
||||
* [2001:db8::1]
|
||||
|
||||
[IsBracketedIp6] can be used to narrow down which form.
|
||||
*/
|
||||
func IsIpAddr(s string) (isIp bool) {
|
||||
|
||||
var err error
|
||||
var a netip.Addr
|
||||
|
||||
if a, err = netip.ParseAddr(s); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
isIp = a.IsValid()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsPrefixNet returns true if s is a (valid) IP address or network (either IPv4 or IPv6) in:
|
||||
|
||||
<addr_or_net>/<prefix_len>
|
||||
|
||||
format.
|
||||
*/
|
||||
func IsPrefixNet(s string) (isNet bool) {
|
||||
|
||||
var err error
|
||||
var p netip.Prefix
|
||||
|
||||
if p, err = netip.ParsePrefix(s); err != nil {
|
||||
return
|
||||
}
|
||||
isNet = p.Masked().IsValid()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsPublic returns true if ALL of the following conditions are *true* for ip:
|
||||
|
||||
* [net/netip.Addr.IsGlobalUnicast]
|
||||
* [net/netip.Addr.IsValid]
|
||||
* (IPv4) Is not in 192.0.2.0/24, 198.51.100.0/24, or 203.0.113.0/24 ([RFC 5737])
|
||||
* (IPv6) Is not in 2001:db8::/32 or 3fff::/20 ([RFC 3849], [RFC 9637])
|
||||
|
||||
AND ALL of the following conditions are *false* for ip:
|
||||
|
||||
* [net/netip.Addr.IsLinkLocalMulticast]
|
||||
* [net/netip.Addr.IsInterfaceLocalMulticast]
|
||||
* [net/netip.Addr.IsLinkLocalUnicast]
|
||||
* [net/netip.Addr.IsMulticast]
|
||||
* [net/netip.Addr.IsLoopback]
|
||||
* [net/netip.Addr.IsPrivate]
|
||||
* [net/netip.Addr.IsUnspecified]
|
||||
|
||||
4-in-6 addresses (::0:0/96, "IPv4-Compatible IPv6 Address" [RFC 4291 § 2.5.5.1] (Legacy/Obsolete);
|
||||
::ffff:0:0/96, "IPv4-Mapped IPv6" [RFC 4291 § 2.5.5.2]) will be internally
|
||||
"unwrapped" back to native IPv4 before performing conditional checks.
|
||||
|
||||
[RFC 5737]: https://datatracker.ietf.org/doc/html/rfc5737
|
||||
[RFC 4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1
|
||||
[RFC 4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
|
||||
[RFC 3849]: https://datatracker.ietf.org/doc/html/rfc3849
|
||||
[RFC 9637]: https://datatracker.ietf.org/doc/html/rfc9637
|
||||
*/
|
||||
func IsPublic(ip netip.Addr) (isPub bool) {
|
||||
|
||||
var err error
|
||||
var v bool
|
||||
var addr netip.Addr
|
||||
|
||||
// Clone to avoid modification to the passed in value.
|
||||
if addr, err = netip.ParseAddr(ip.String()); err != nil {
|
||||
return
|
||||
}
|
||||
addr = addr.Unmap()
|
||||
|
||||
// Short-circuit on invalid first to avoid any possible logic oddness.
|
||||
if !addr.IsValid() {
|
||||
return
|
||||
}
|
||||
// Following must be FALSE
|
||||
for _, v = range []bool{
|
||||
addr.IsLinkLocalMulticast(),
|
||||
addr.IsInterfaceLocalMulticast(),
|
||||
addr.IsLinkLocalUnicast(),
|
||||
addr.IsMulticast(),
|
||||
addr.IsLoopback(),
|
||||
addr.IsPrivate(),
|
||||
addr.IsUnspecified(),
|
||||
// these are in the FALSE to keep syntax pattern but it's cleaner to doc as if it's a !<cond> in TRUE.
|
||||
ip4In6Legacy.Contains(ip),
|
||||
ip4In6Modern.Contains(ip),
|
||||
} {
|
||||
if v {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Following must be TRUE
|
||||
for _, v = range []bool{
|
||||
addr.IsGlobalUnicast(),
|
||||
} {
|
||||
if !v {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// All conditions pass
|
||||
isPub = true
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
VerToFamily takes a "human-readable" IP version ipVer (4 or 6) and returns
|
||||
a system-level constant (e.g. [AFUnspec], [AFInet], [AFInet6]).
|
||||
|
||||
If not a known IP version (i.e. neither 4 nor 6), family will be [AFUnspec].
|
||||
|
||||
It is the inverse of [FamilyToVer].
|
||||
*/
|
||||
func VerToFamily(ipVer int) (family uint16) {
|
||||
|
||||
switch ipVer {
|
||||
case 4:
|
||||
family = AFInet
|
||||
case 6:
|
||||
family = AFInet6
|
||||
default:
|
||||
family = AFUnspec
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package netx
|
||||
|
||||
import (
|
||||
`math`
|
||||
`net`
|
||||
`net/netip`
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFuncsDns(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var domBin []byte
|
||||
var domStr string
|
||||
var domEx string = "foo.r00t2.io"
|
||||
|
||||
if domBin, err = DnsStrToWire(domEx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Domain %s to wire: %#x\n", domEx, domBin)
|
||||
if domStr, err = DnsWireToStr(domBin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Domain wire %#x to string: %s\n", domBin, domStr)
|
||||
if domStr != domEx {
|
||||
t.Fatalf("DNS str wrong (%s != %s)\n)", domStr, domEx)
|
||||
}
|
||||
t.Logf("Domain string %s matches %s", domStr, domEx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
This is mostly just for generating some reference. It's not really intended for public consumption.
|
||||
@@ -0,0 +1,20 @@
|
||||
module r00t2.io/goutils/netx/internal
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require github.com/olekukonko/tablewriter v1.1.4
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.3.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
|
||||
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
||||
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
||||
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
@@ -0,0 +1,295 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
const (
|
||||
sectBordChar string = "#"
|
||||
sectBordWdth int = 2
|
||||
sectSpcPad int = 1
|
||||
|
||||
sectSidePad int = sectBordWdth + sectSpcPad
|
||||
sectBothPad int = sectSidePad * 2
|
||||
)
|
||||
|
||||
type (
|
||||
IpInfo struct {
|
||||
// Desc string
|
||||
Index int
|
||||
// IP netip.Addr
|
||||
Is4 bool
|
||||
Is4In6 bool
|
||||
Is6 bool
|
||||
IsGlobalUnicast bool
|
||||
IsLinkLocalUnicast bool
|
||||
IsInterfaceLocalMulticast bool
|
||||
IsLinkLocalMulticast bool
|
||||
IsMulticast bool
|
||||
IsLoopback bool
|
||||
IsPrivate bool
|
||||
IsUnspecified bool
|
||||
IsValid bool
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
datHdrsFunc []string = []string{
|
||||
"-",
|
||||
".Is4", ".Is4In6()", ".Is6()",
|
||||
".IsGlobalUnicast()", ".IsLinkLocalUnicast()",
|
||||
".IsInterfaceLocalMulticast()", ".IsLinkLocalMulticast()", ".IsMulticast()",
|
||||
".IsLoopback()",
|
||||
".IsPrivate()",
|
||||
".IsUnspecified()", ".IsValid()",
|
||||
}
|
||||
|
||||
datHdrsKey []string = []string{
|
||||
"Address Index (See Fig. 3)",
|
||||
"IPv4", "4-in-6", "IPv6",
|
||||
"Global Unicast", "Link-Local Unicast",
|
||||
"Interface Local Multicast", "Link-Local Multicast", "Multicast",
|
||||
"Loopback",
|
||||
"Private/LAN",
|
||||
"\"Unspecified\" Address", "Valid Address",
|
||||
}
|
||||
|
||||
datHdrsShort = []string{
|
||||
"IDX",
|
||||
"4", "6(4)", "6",
|
||||
"GU", "LLU",
|
||||
"ILM", "LLM", "M",
|
||||
"LO",
|
||||
"P",
|
||||
"U", "V",
|
||||
}
|
||||
|
||||
exampleAddrs [][2]string = [][2]string{
|
||||
[2]string{"IPv4", "203.0.113.10"}, // RFC 5737 address (https://datatracker.ietf.org/doc/html/rfc5737)
|
||||
[2]string{"IPv4 Unspecified", "0.0.0.0"}, // Generally used to represent all of IPv4 address space
|
||||
[2]string{"IPv4 Global Unicast", "173.230.132.76"}, // r00t2.io
|
||||
[2]string{"IPv4 Multicast (All) (RFC 1112 § 4)", "224.0.0.1"}, // Should encompass all the multicast below. 224.0.0.0 is reserved.
|
||||
[2]string{"IPv4 Multicast (Reserved) (RFC 1112 § 4)", "224.0.0.0"}, // Reserved per RFC but it should still report multicast.
|
||||
[2]string{"IPv4 Multicast (Link-Local Multicast/Local Network Control Block) (RFC 5771 § 4)", "224.0.0.18"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-4 (VRRP multicast addr)
|
||||
[2]string{"IPv4 Multicast (Internetwork Control Block) (RFC 5771 § 5)", "224.0.1.68"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-5 (mdhcpdiscover, RFC 2730)
|
||||
[2]string{"IPv4 Multicast (AD-HOC I) (RFC 5771 § 6)", "224.0.2.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-6
|
||||
[2]string{"IPv4 Multicast (SDP/SAP) (RFC 5771 § 7)", "224.2.0.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-7
|
||||
[2]string{"IPv4 Multicast (AD-HOC II) (RFC 5771 § 6)", "224.3.0.10"}, // (above)
|
||||
[2]string{"IPv4 Multicast (Source-Specific) (RFC 5771 § 8)", "232.0.0.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-8
|
||||
[2]string{"IPv4 Multicast (GLOP) (RFC 5771 § 9)", "233.0.0.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-9
|
||||
[2]string{"IPv4 Multicast (AD-HOC III) (RFC 5771 § 6)", "233.252.0.10"}, // (above)
|
||||
[2]string{"IPv4 Multicast (Administrative) (RFC 5771 § 10)", "239.0.0.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-10
|
||||
[2]string{"IPv4 Link-Local Unicast (RFC 3927 § 2.1)", "169.254.1.10"}, // https://datatracker.ietf.org/doc/html/rfc3927#section-2.1
|
||||
[2]string{"IPv4 Loopback", "127.0.1.10"}, // It's actually 127/8. Cannot believe how many people do not know this.
|
||||
[2]string{"IPv4 Private (RFC 1918)", "10.0.0.10"}, // https://datatracker.ietf.org/doc/html/rfc1918
|
||||
[2]string{"4-in-6 (RFC 4291 § 2.5.5.1)", "::203.0.113.10"}, // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1
|
||||
[2]string{"4-in-6 (RFC 4291 § 2.5.5.1) (Native)", "::cb00:710a"}, // ""
|
||||
[2]string{"4-in-6 (RFC 4291 § 2.5.5.2)", "::ffff:203.0.113.10"}, // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
|
||||
[2]string{"4-in-6 (RFC 4291 § 2.5.5.2) (Native)", "::ffff:cb00:710a"}, // ""
|
||||
[2]string{"IPv6", "2001:db8::cb00:710a"}, // RFC 3849 (https://datatracker.ietf.org/doc/html/rfc3849) / RFC 9637 (https://datatracker.ietf.org/doc/html/rfc9637) address
|
||||
[2]string{"IPv6 Unspecified", "::"}, // Generally used to represent all of IPv6 address space
|
||||
[2]string{"IPv6 Global Unicast", "2600:3c02::f03c:91ff:fe93:c0a7"}, // r00t2.io
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (Reserved Net)", "ff00::"}, // https://datatracker.ietf.org/doc/html/rfc4291#section-2.7.1
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (Reserved)", "ff00::1"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Nodes) (Interface-Local)", "ff01::1"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Nodes) (Link-Local)", "ff02::1"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Interface-Local)", "ff01::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Link-Local)", "ff02::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Admin-Local)", "ff04::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Site-Local)", "ff05::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Org-Local)", "ff08::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Internet/Global)", "ff0e::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (Solicited Node)", "ff02::1:ff00:10"}, // ""
|
||||
[2]string{"IPv6 Link-Local Unicast (RFC 4291 § 2.5.6)", "fe80::cb00:710a"}, // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.6
|
||||
[2]string{"IPv6 Loopback", "::1"}, // It's explicitly always a /128 with the address ::1 per RFC 4291 § 2.5.3.
|
||||
[2]string{"IPv6 Private (Unique-Local Addresses) (RFC 4193) (Reserved)", "fc00::10"}, // https://datatracker.ietf.org/doc/html/rfc4193
|
||||
[2]string{"IPv6 Private (Unique-Local Addresses) (RFC 4193) (Valid)", "fd00::10"}, // ""
|
||||
}
|
||||
|
||||
descs []string = make([]string, len(exampleAddrs))
|
||||
ips []netip.Addr = make([]netip.Addr, len(exampleAddrs))
|
||||
|
||||
sectSep string = "\n" + strings.Repeat("-", 80) + "\n"
|
||||
)
|
||||
|
||||
func genHdr(sectNm string) (s string) {
|
||||
|
||||
// Wish I finished stringsx.Banner at time of writing this...
|
||||
var fillLen int = sectBothPad + len(sectNm)
|
||||
|
||||
s = fmt.Sprintf(
|
||||
"%s\n"+ // top border
|
||||
// begin title line
|
||||
"%-*s"+ // left-justify/right-pad
|
||||
"%s"+ // sectNm text
|
||||
"%[2]*[3]s\n"+ // one-indexed; repeat pad num and pad str from left border as right border
|
||||
// end title line
|
||||
"%[1]s\n", // bottom border
|
||||
strings.Repeat(sectBordChar, fillLen), // top and bottom (bottom uses index)
|
||||
sectSidePad, strings.Repeat(sectBordChar, sectBordWdth), // title left and right borders (right uses index)
|
||||
sectNm, // title text
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var idx int
|
||||
var s string
|
||||
var desc string
|
||||
var pair [2]string
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
var tbl *tablewriter.Table = tablewriter.NewTable(
|
||||
buf,
|
||||
tablewriter.WithConfig(
|
||||
tablewriter.Config{
|
||||
Header: tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoFormat: tw.Off,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
// requires .Batch(), and the autoheader forces all caps.
|
||||
// https://github.com/olekukonko/tablewriter/issues/143
|
||||
// https://github.com/olekukonko/tablewriter/issues/190
|
||||
/*
|
||||
tablewriter.WithBehavior(
|
||||
tw.Behavior{
|
||||
Structs: tw.Struct{
|
||||
AutoHeader: tw.On,
|
||||
},
|
||||
},
|
||||
),
|
||||
*/
|
||||
tablewriter.WithRendition(
|
||||
tw.Rendition{
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{BetweenRows: tw.On},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
var tErr error
|
||||
if tbl != nil {
|
||||
if tErr = tbl.Close(); tErr != nil {
|
||||
log.Printf("Error closing table: %v", tErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
buf.WriteString(genHdr("Fig. 1: Address Evaluations"))
|
||||
buf.WriteString("(See Fig. 2 for a key of header symbols to names)\n\n")
|
||||
|
||||
tbl.Header(datHdrsShort)
|
||||
for idx, pair = range exampleAddrs {
|
||||
desc, s = pair[0], pair[1]
|
||||
descs[idx] = desc
|
||||
if ips[idx], err = netip.ParseAddr(s); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
// Currently no way to skip cols etc. https://github.com/olekukonko/tablewriter/issues/317
|
||||
// rows[idx] = IpInfo{
|
||||
if err = tbl.Append(
|
||||
IpInfo{
|
||||
// Desc: desc,
|
||||
Index: idx,
|
||||
// IP: ip,
|
||||
Is4: ips[idx].Is4(),
|
||||
Is4In6: ips[idx].Is4In6(),
|
||||
Is6: ips[idx].Is6(),
|
||||
IsGlobalUnicast: ips[idx].IsGlobalUnicast(),
|
||||
IsLinkLocalUnicast: ips[idx].IsLinkLocalUnicast(),
|
||||
IsInterfaceLocalMulticast: ips[idx].IsInterfaceLocalMulticast(),
|
||||
IsLinkLocalMulticast: ips[idx].IsLinkLocalMulticast(),
|
||||
IsMulticast: ips[idx].IsMulticast(),
|
||||
IsLoopback: ips[idx].IsLoopback(),
|
||||
IsPrivate: ips[idx].IsPrivate(),
|
||||
IsUnspecified: ips[idx].IsUnspecified(),
|
||||
IsValid: ips[idx].IsValid(),
|
||||
},
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
/*
|
||||
if err = tbl.Bulk(rows); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
*/
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
buf.WriteString(sectSep)
|
||||
buf.WriteString(genHdr("Fig. 2: Headers Key for Fig. 1"))
|
||||
tbl.Reset()
|
||||
tbl.Header("Symbol", "Description", "net/netip.Addr Method")
|
||||
for idx = range datHdrsKey {
|
||||
if err = tbl.Append(
|
||||
[]string{
|
||||
datHdrsShort[idx],
|
||||
datHdrsKey[idx],
|
||||
datHdrsFunc[idx],
|
||||
},
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
buf.WriteString(sectSep)
|
||||
buf.WriteString(genHdr("Fig. 3: Test/Example IP Address Reference/Lookup for Fig. 1"))
|
||||
buf.WriteString("(See Fig. 4 for Descriptions/Detailed Information)\n\n")
|
||||
tbl.Reset()
|
||||
tbl.Header("Index", "Address (Raw)", "Address (Parsed)")
|
||||
for idx = range exampleAddrs {
|
||||
if err = tbl.Append(
|
||||
[]string{
|
||||
strconv.Itoa(idx),
|
||||
exampleAddrs[idx][1],
|
||||
ips[idx].String(),
|
||||
},
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
buf.WriteString(sectSep)
|
||||
buf.WriteString(genHdr("Fig. 4: Extended Information for Fig. 3"))
|
||||
tbl.Reset()
|
||||
tbl.Header("Index", "Description")
|
||||
for idx = range exampleAddrs {
|
||||
if err = tbl.Append(
|
||||
[]string{
|
||||
strconv.Itoa(idx),
|
||||
descs[idx],
|
||||
},
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
fmt.Println(buf.String())
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"maps"
|
||||
"net/netip"
|
||||
`os`
|
||||
"slices"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
`github.com/olekukonko/tablewriter/tw`
|
||||
)
|
||||
|
||||
type (
|
||||
IpInfo struct {
|
||||
Desc string `r:"-"`
|
||||
IP netip.Addr `r:"-"`
|
||||
Is4 bool
|
||||
Is4In6 bool
|
||||
Is6 bool
|
||||
IsGlobalUnicast bool
|
||||
IsInterfaceLocalMulticast bool
|
||||
IsLinkLocalMulticast bool
|
||||
IsLinkLocalUnicast bool
|
||||
IsLoopback bool
|
||||
IsMulticast bool
|
||||
IsPrivate bool
|
||||
IsUnspecified bool
|
||||
IsValid bool
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
var idx int
|
||||
var desc string
|
||||
var ip netip.Addr
|
||||
var rows []IpInfo
|
||||
/*
|
||||
var typ reflect.Type
|
||||
var val reflect.Value
|
||||
var fieldVal reflect.Value
|
||||
var field reflect.StructField
|
||||
*/
|
||||
var exampleAddrs map[string]string
|
||||
var tbl *tablewriter.Table = tablewriter.NewTable(
|
||||
os.Stdout,
|
||||
tablewriter.WithBehavior(
|
||||
tw.Behavior{
|
||||
Structs: tw.Struct{
|
||||
AutoHeader: tw.On,
|
||||
},
|
||||
},
|
||||
),
|
||||
tablewriter.WithRendition(
|
||||
tw.Rendition{
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{BetweenRows: tw.On},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
var tErr error
|
||||
if tbl != nil {
|
||||
if tErr = tbl.Close(); tErr != nil {
|
||||
log.Printf("Error closing table: %v", tErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
exampleAddrs = map[string]string{
|
||||
"IPv4": "203.0.113.10", // RFC 5737 address (https://datatracker.ietf.org/doc/html/rfc5737)
|
||||
"IPv4 Unspecified": "0.0.0.0",
|
||||
"IPv4 Global Unicast": "173.230.132.76", // r00t2.io
|
||||
"IPv4 Multicast (All) (RFC 1112 § 4)": "224.0.0.1", // Should encompass all the multicast below. 224.0.0.0 is reserved.
|
||||
"IPv4 Multicast (Reserved) (RFC 1112 § 4)": "224.0.0.0", // Reserved per RFC but it should still report multicast.
|
||||
"IPv4 Multicast (Link-Local Multicast/Local Network Control Block) (RFC 5771 § 4)": "224.0.0.18", // https://datatracker.ietf.org/doc/html/rfc5771#section-4 (VRRP multicast addr)
|
||||
"IPv4 Multicast (Internetwork Control Block) (RFC 5771 § 5)": "224.0.1.68", // https://datatracker.ietf.org/doc/html/rfc5771#section-5 (mdhcpdiscover, RFC 2730)
|
||||
"IPv4 Multicast (AD-HOC I) (RFC 5771 § 6)": "224.0.2.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-6
|
||||
"IPv4 Multicast (SDP/SAP) (RFC 5771 § 7)": "224.2.0.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-7
|
||||
"IPv4 Multicast (AD-HOC II) (RFC 5771 § 6)": "224.3.0.10", // (above)
|
||||
"IPv4 Multicast (Source-Specific) (RFC 5771 § 8)": "232.0.0.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-8
|
||||
"IPv4 Multicast (GLOP) (RFC 5771 § 9)": "233.0.0.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-9
|
||||
"IPv4 Multicast (AD-HOC III) (RFC 5771 § 6)": "233.252.0.10", // (above)
|
||||
"IPv4 Multicast (Administrative) (RFC 5771 § 10)": "239.0.0.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-10
|
||||
"IPv4 Link-Local Unicast (RFC 3927 § 2.1)": "169.254.1.10", // https://datatracker.ietf.org/doc/html/rfc3927#section-2.1
|
||||
"IPv4 Loopback": "127.0.1.10", // It's actually 127/8. Cannot believe how many people do not know this.
|
||||
"IPv4 Private (RFC 1918)": "10.0.0.10", // https://datatracker.ietf.org/doc/html/rfc1918
|
||||
"4-in-6 (RFC 4291 § 2.5.5.1)": "::203.0.113.10", // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1
|
||||
"4-in-6 (RFC 4291 § 2.5.5.1) (Native)": "::cb00:710a", // ""
|
||||
"4-in-6 (RFC 4291 § 2.5.5.2)": "::ffff:203.0.113.10", // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
|
||||
"4-in-6 (RFC 4291 § 2.5.5.2) (Native)": "::ffff:cb00:710a", // ""
|
||||
"IPv6": "2001:db8::cb00:710a", // RFC 3849 (https://datatracker.ietf.org/doc/html/rfc3849) / RFC 9637 (https://datatracker.ietf.org/doc/html/rfc9637) address
|
||||
"IPv6 Unspecified": "::",
|
||||
"IPv6 Global Unicast": "2600:3c02::f03c:91ff:fe93:c0a7", // r00t2.io
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (Reserved Net)": "ff00::", // https://datatracker.ietf.org/doc/html/rfc4291#section-2.7.1
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (Reserved)": "ff00::1", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Nodes) (Interface-Local)": "ff01::1", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Nodes) (Link-Local)": "ff02::1", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Interface-Local)": "ff01::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Link-Local)": "ff02::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Admin-Local)": "ff04::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Site-Local)": "ff05::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Org-Local)": "ff08::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Internet/Global)": "ff0e::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (Solicited Node)": "ff02::1:ff00:10", // ""
|
||||
"IPv6 Link-Local Unicast (RFC 4291 § 2.5.6)": "fe80::cb00:710a", // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.6
|
||||
"IPv6 Loopback": "::1", // It's explicitly always a /128 with the address ::1 per RFC 4291 § 2.5.3.
|
||||
"IPv6 Private (Unique-Local Addresses) (RFC 4193) (Reserved)": "fc00::10", // https://datatracker.ietf.org/doc/html/rfc4193
|
||||
"IPv6 Private (Unique-Local Addresses) (RFC 4193) (Valid)": "fd00::10", // ""
|
||||
}
|
||||
rows = make([]IpInfo, len(exampleAddrs))
|
||||
|
||||
for idx, desc = range slices.Sorted(maps.Keys(exampleAddrs)) {
|
||||
s = exampleAddrs[desc]
|
||||
if ip, err = netip.ParseAddr(s); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
// Currently no way to skip cols etc. https://github.com/olekukonko/tablewriter/issues/317
|
||||
rows[idx] = IpInfo{
|
||||
Desc: desc,
|
||||
IP: ip,
|
||||
Is4: ip.Is4(),
|
||||
Is4In6: ip.Is4In6(),
|
||||
Is6: ip.Is6(),
|
||||
IsGlobalUnicast: ip.IsGlobalUnicast(),
|
||||
IsInterfaceLocalMulticast: ip.IsInterfaceLocalMulticast(),
|
||||
IsLinkLocalMulticast: ip.IsLinkLocalMulticast(),
|
||||
IsLinkLocalUnicast: ip.IsLinkLocalUnicast(),
|
||||
IsLoopback: ip.IsLoopback(),
|
||||
IsMulticast: ip.IsMulticast(),
|
||||
IsPrivate: ip.IsPrivate(),
|
||||
IsUnspecified: ip.IsUnspecified(),
|
||||
IsValid: ip.IsValid(),
|
||||
}
|
||||
}
|
||||
if err = tbl.Bulk(rows); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
- write a flag parser/converter for github.com/scorpionknifes/go-pcre (and github.com/GRbit/go-pcre) :
|
||||
----
|
||||
// i CASELESS
|
||||
// m MULTILINE
|
||||
// s DOTALL
|
||||
// x EXTENDED
|
||||
// U UNGREEDY
|
||||
// 8 UTF8 (non-standard flag letter; standard PCRE uses (*UTF8) outside (?...) syntax)
|
||||
//
|
||||
// Flags after a '-' inside group disable associated flag and are ignored
|
||||
// (applies inside the regex engine once prefix is stripped).
|
||||
// pattern, flags := ParsePrefixFlags(`(?im)^hello`)
|
||||
// re := pcre.MustCompile(pattern, flags)
|
||||
func ParsePrefixFlags(pattern string) (string, int) {
|
||||
if !strings.HasPrefix(pattern, "(?") {
|
||||
return pattern, 0
|
||||
}
|
||||
|
||||
end := strings.IndexByte(pattern, ')')
|
||||
if end < 0 {
|
||||
return pattern, 0
|
||||
}
|
||||
|
||||
inner := pattern[2:end] // everything between "(?" and ")"
|
||||
|
||||
for _, ch := range inner {
|
||||
if !strings.ContainsRune("imsxU8-", ch) {
|
||||
return pattern, 0
|
||||
}
|
||||
}
|
||||
|
||||
positive := inner
|
||||
if dash := strings.IndexByte(inner, '-'); dash >= 0 {
|
||||
positive = inner[:dash]
|
||||
}
|
||||
|
||||
var flags int
|
||||
for _, ch := range positive {
|
||||
switch ch {
|
||||
case 'i':
|
||||
flags |= pcre.CASELESS
|
||||
case 'm':
|
||||
flags |= pcre.MULTILINE
|
||||
case 's':
|
||||
flags |= pcre.DOTALL
|
||||
case 'x':
|
||||
flags |= pcre.EXTENDED
|
||||
case 'U':
|
||||
flags |= pcre.UNGREEDY
|
||||
case '8':
|
||||
flags |= pcre.UTF8
|
||||
}
|
||||
}
|
||||
|
||||
return pattern[end+1:], flags
|
||||
}
|
||||
----
|
||||
+9
-1
@@ -1,4 +1,12 @@
|
||||
/*
|
||||
Package remap provides convenience functions around regular expressions.
|
||||
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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
+782
-18
@@ -1,20 +1,210 @@
|
||||
package remap
|
||||
|
||||
/*
|
||||
Map returns a map[string]<match bytes> for regexes with named capture groups matched in bytes b.
|
||||
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).
|
||||
|
||||
matches will be nil if no named capture group matches were found.
|
||||
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) (matches map[string][]byte) {
|
||||
func (r *ReMap) Map(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][][]byte) {
|
||||
|
||||
var m [][]byte
|
||||
var tmpMap map[string][]byte = make(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)
|
||||
|
||||
m = r.Regexp.FindSubmatch(b)
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for idx, grpNm := range r.Regexp.SubexpNames() {
|
||||
if idx != 0 && grpNm != "" {
|
||||
tmpMap[grpNm] = m[idx]
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,20 +216,594 @@ func (r *ReMap) Map(b []byte) (matches map[string][]byte) {
|
||||
}
|
||||
|
||||
/*
|
||||
MapString returns a map[string]<match string> for regexes with named capture groups matched in string s.
|
||||
MapAll behaves exactly like [ReMap.Map] but will "squash"/consolidate *all* found matches, not just the first occurrence,
|
||||
into the group name.
|
||||
|
||||
matches will be nil if no named capture group matches were found.
|
||||
You likely want to use this instead of [ReMap.Map] for multiline patterns.
|
||||
*/
|
||||
func (r *ReMap) MapString(s string) (matches map[string]string) {
|
||||
func (r *ReMap) MapAll(b []byte, inclNoMatch, inclNoMatchStrict, mustMatch bool) (matches map[string][][]byte) {
|
||||
|
||||
var m []string
|
||||
var tmpMap map[string]string = make(map[string]string)
|
||||
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)
|
||||
|
||||
m = r.Regexp.FindStringSubmatch(s)
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for idx, grpNm := range r.Regexp.SubexpNames() {
|
||||
if idx != 0 && grpNm != "" {
|
||||
tmpMap[grpNm] = m[idx]
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+63
-5
@@ -1,10 +1,68 @@
|
||||
package remap
|
||||
|
||||
import (
|
||||
`regexp`
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// ReMap provides some map-related functions around a regexp.Regexp.
|
||||
type ReMap struct {
|
||||
*regexp.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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
- 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
|
||||
|
||||
- Tokenize() (new function)
|
||||
-- PosixFilename() (new function) -- https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
|
||||
-- strings.ToLower()
|
||||
-- compact consecutive:
|
||||
--- whitespace
|
||||
--- .
|
||||
--- ,
|
||||
-- set custom replacement string (defaults to "_")
|
||||
-- replace whitespace (after/during compact) with _ (customizable?)
|
||||
|
||||
- create bytesx package that duplicates the functions here?
|
||||
@@ -0,0 +1,6 @@
|
||||
package stringsx
|
||||
|
||||
const (
|
||||
// DefMaskStr is the string used as the default maskStr if left empty in [Redact].
|
||||
DefMaskStr string = "***"
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1,18 @@
|
||||
package stringsx
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
)
|
||||
|
||||
// Error conforms an [AsciiInvalidError] to an error interface.
|
||||
func (a *AsciiInvalidError) Error() (errStr string) {
|
||||
|
||||
errStr = fmt.Sprintf(
|
||||
"non-ASCII character '%c' at line:linepos %d:%d (byte %d), "+
|
||||
"string position %d (byte %d): bytes %#x, UTF-8 codepoint U+%04X",
|
||||
a.BadChar, a.Line, a.LineChar, a.LineByte,
|
||||
a.Char, a.Byte, a.BadBytes, a.BadChar,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package stringsx
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`errors`
|
||||
`fmt`
|
||||
`io`
|
||||
`slices`
|
||||
`strings`
|
||||
`unicode`
|
||||
)
|
||||
|
||||
/*
|
||||
HasBookend returns true if string s both begins AND ends with sym.
|
||||
|
||||
It is more strict than [HasBoundary] (which only requires
|
||||
that s has sym at the beginning OR the end.)
|
||||
|
||||
Examples:
|
||||
HasBookend("|foo|", "|") → true
|
||||
HasBookend("|foo", "|") → false
|
||||
HasBookend("foo|", "|") → false
|
||||
HasBookend("fo|o", "|") → false
|
||||
HasBookend("|foo| ", "|") → false // Whitespace prevents match
|
||||
HasBookend(" |foo| ", "|") → false
|
||||
|
||||
sym may be a multi-rune string.
|
||||
If sym is empty, HasBookend will *always* return true.
|
||||
*/
|
||||
func HasBookend(s, sym string) (bounded bool) {
|
||||
|
||||
bounded = strings.HasPrefix(s, sym) && strings.HasSuffix(s, sym)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
HasBoundary returns true if string s starts OR ends with symbol sym.
|
||||
|
||||
Examples:
|
||||
HasBoundary("|foo|", "|") → true
|
||||
HasBoundary("|foo", "|") → true
|
||||
HasBoundary("foo|", "|") → true
|
||||
HasBoundary("fo|o", "|") → false
|
||||
HasBoundary("|foo| ", "|") → true
|
||||
HasBoundary(" |foo| ", "|") → false // Whitespace prevents match
|
||||
|
||||
sym may be a multi-rune string.
|
||||
If sym is empty, HasBoundary will *always* return true.
|
||||
|
||||
If you instead require string s to be *enclosed* by sym, see [HasBookend].
|
||||
*/
|
||||
func HasBoundary(s, sym string) (bounded bool) {
|
||||
|
||||
bounded = strings.HasPrefix(s, sym) || strings.HasSuffix(s, sym)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsAscii returns true if all characters in string s are ASCII.
|
||||
|
||||
This simply wraps [IsAsciiSpecial]:
|
||||
|
||||
isAscii, err = IsAsciiSpecial(s, allowCtl, true, allowExt, true, nil, nil)
|
||||
*/
|
||||
func IsAscii(s string, allowCtl, allowExt bool) (isAscii bool, err error) {
|
||||
|
||||
if isAscii, err = IsAsciiSpecial(
|
||||
s, allowCtl, true, allowExt, true, nil, nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsAsciiBuf returns true if all of buffer buf is valid ASCII.
|
||||
|
||||
Note that the buffer will be consumed/read by this function.
|
||||
|
||||
This simply wraps [IsAsciiBufSpecial]:
|
||||
|
||||
isAscii, err = IsAsciiBufSpecial(r, allowCtl, true, allowExt, true, nil, nil)
|
||||
*/
|
||||
func IsAsciiBuf(r io.RuneReader, allowCtl, allowExt bool) (isAscii bool, err error) {
|
||||
|
||||
if isAscii, err = IsAsciiBufSpecial(
|
||||
r, allowCtl, true, allowExt, true, nil, nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsAsciiSpecial allows for specifying specific ASCII ranges.
|
||||
|
||||
allowCtl, if true, will allow control characters (0x00 to 0x1f inclusive).
|
||||
|
||||
allowPrint, if true, will allow printable characters (what most people think of
|
||||
when they say "ASCII") (0x20 to 0x7f inclusive).
|
||||
|
||||
allowExt, if true, will allow for "extended ASCII" - some later dialects expand
|
||||
to a full 8-bit ASCII range (0x80 to 0xff inclusive).
|
||||
|
||||
wsCtl, if true, "shifts" the "whitespace control characters" (\t, \n, \r) to the "printable" space
|
||||
(such that allowPrint controls their validation). Thus:
|
||||
|
||||
IsAsciiSpecial(s, false, true, false, true, nil, nil)
|
||||
|
||||
has the same effect as specifying:
|
||||
|
||||
IsAsciiSpecial(s, false, true, false, (-), []byte("\t\n\r"), nil)
|
||||
|
||||
incl, if non-nil and non-empty, allows *additional* characters to be specified as included
|
||||
that would normally *not* be allowed.
|
||||
|
||||
excl, if non-nil and non-empty, invalidates on additional characters that would normally be allowed.
|
||||
|
||||
excl, if specified, takes precedence over incl if specified.
|
||||
|
||||
An [AsciiInvalidError] will be returned on the first encountered invalid character.
|
||||
*/
|
||||
func IsAsciiSpecial(s string, allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (isAscii bool, err error) {
|
||||
|
||||
var buf *bytes.Buffer = bytes.NewBufferString(s)
|
||||
|
||||
if isAscii, err = IsAsciiBufSpecial(buf, allowCtl, allowPrint, allowExt, allowWs, incl, excl); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsAsciiBufSpecial is the same as [IsAsciiSpecial] but operates on an [io.RuneReader].
|
||||
|
||||
Note that the buffer will be consumed/read by this function.
|
||||
|
||||
It will not return an [io.EOF] if encountered, but any other errors encountered will be returned.
|
||||
It is expected that r will return an [io.EOF] when exhausted.
|
||||
|
||||
An [AsciiInvalidError] will be returned on the first encountered invalid character.
|
||||
*/
|
||||
func IsAsciiBufSpecial(r io.RuneReader, allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (isAscii bool, err error) {
|
||||
|
||||
var b rune
|
||||
var bLen int
|
||||
var nextNewline bool
|
||||
var tmpErr *AsciiInvalidError = new(AsciiInvalidError)
|
||||
// I know, I know. This is essentually a lookup table. Keeps it speedy.
|
||||
var allowed [256]bool = getAsciiCharMap(allowCtl, allowPrint, allowExt, allowWs, incl, excl)
|
||||
|
||||
for {
|
||||
if b, bLen, err = r.ReadRune(); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
err = nil
|
||||
isAscii = true
|
||||
}
|
||||
return
|
||||
}
|
||||
// Set these *before* OK
|
||||
if nextNewline {
|
||||
tmpErr.Line++
|
||||
tmpErr.LineByte = 0
|
||||
tmpErr.LineChar = 0
|
||||
nextNewline = false
|
||||
} else {
|
||||
tmpErr.LineChar++
|
||||
}
|
||||
tmpErr.Char++
|
||||
|
||||
if b == '\n' {
|
||||
nextNewline = true
|
||||
}
|
||||
if b == rune(0xfffd) {
|
||||
// not even valid unicode
|
||||
tmpErr.BadChar = b
|
||||
tmpErr.BadBytes = []byte(string(b))
|
||||
err = tmpErr
|
||||
return
|
||||
}
|
||||
if bLen > 2 || b > 0xff {
|
||||
// ASCII only occupies a single byte, ISO-8859-1 occupies 2
|
||||
tmpErr.BadChar = b
|
||||
tmpErr.BadBytes = []byte(string(b))
|
||||
err = tmpErr
|
||||
return
|
||||
}
|
||||
if !allowed[byte(b)] {
|
||||
tmpErr.BadChar = b
|
||||
tmpErr.BadBytes = []byte{byte(b)}
|
||||
err = tmpErr
|
||||
return
|
||||
}
|
||||
|
||||
// Set these *after* OK
|
||||
tmpErr.LineByte += uint64(bLen)
|
||||
tmpErr.Byte += uint64(bLen)
|
||||
}
|
||||
|
||||
isAscii = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
// Reverse reverses string s. (It's absolutely insane that this isn't in stdlib.)
|
||||
func Reverse(s string) (revS string) {
|
||||
|
||||
var rsl []rune = []rune(s)
|
||||
|
||||
slices.Reverse(rsl)
|
||||
|
||||
revS = string(rsl)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// getAsciiCharMap returns a lookup "table" for ASCII characters.
|
||||
func getAsciiCharMap(allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (charmap [256]bool) {
|
||||
|
||||
var idx uint8
|
||||
|
||||
if allowCtl {
|
||||
for idx < 0x1f {
|
||||
charmap[idx] = true
|
||||
idx++
|
||||
}
|
||||
} else {
|
||||
idx = 0x1f
|
||||
}
|
||||
if allowPrint {
|
||||
for idx < 0x7f {
|
||||
charmap[idx] = true
|
||||
idx++
|
||||
}
|
||||
} else {
|
||||
idx = 0x7f
|
||||
}
|
||||
if allowExt {
|
||||
for {
|
||||
charmap[idx] = true
|
||||
if idx == 0xff {
|
||||
break
|
||||
}
|
||||
idx++
|
||||
}
|
||||
} else {
|
||||
idx = 0xff
|
||||
}
|
||||
if allowWs {
|
||||
charmap['\t'] = true
|
||||
charmap['\n'] = true
|
||||
charmap['\r'] = true
|
||||
}
|
||||
|
||||
if incl != nil && len(incl) > 0 {
|
||||
for _, idx = range incl {
|
||||
charmap[idx] = true
|
||||
}
|
||||
}
|
||||
if excl != nil && len(excl) > 0 {
|
||||
for _, idx = range excl {
|
||||
charmap[idx] = false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
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 TestFuncsAscii(t *testing.T) {
|
||||
|
||||
var err error
|
||||
// var s string = "This is a §\nmulti-line\nstring 😀 with\nunicode text.\n"
|
||||
var s string = "This is a §\nmulti-line\nstring with\nno unicode text.\n"
|
||||
|
||||
if _, err = IsAscii(s, false, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
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 TestReverse(t *testing.T) {
|
||||
|
||||
var rev string
|
||||
var s string = "012345679abcdef"
|
||||
|
||||
rev = Reverse(s)
|
||||
if rev != "fedcba976543210" {
|
||||
t.Errorf("reverse of s '%s'; expected 'fedcba976543210', got '%s'", s, rev)
|
||||
}
|
||||
t.Logf("s: %s\nReverse: %s", s, rev)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package stringsx
|
||||
|
||||
type (
|
||||
/*
|
||||
AsciiInvalidError is an error used to return an error for the IsAscii* validations.
|
||||
|
||||
It is returned on the first found instance of an invalid ASCII character.
|
||||
*/
|
||||
AsciiInvalidError struct {
|
||||
// Line is a 0-indexed line number where the invalid character was found.
|
||||
Line uint64
|
||||
// LineByte is the 0-indexed byte position for the current Line.
|
||||
LineByte uint64
|
||||
// LineChar is a 0-indexed character (rune) position where the invalid character was found on line number Line.
|
||||
LineChar uint64
|
||||
// Byte is the 0-indexed byte position across the entire input.
|
||||
Byte uint64
|
||||
// Char is the 0-indexed character (rune) position across the entire input.
|
||||
Char uint64
|
||||
// BadChar is the invalid rune
|
||||
BadChar rune
|
||||
// BadBytes is BadChar as bytes.
|
||||
BadBytes []byte
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package timex provides some handy [time]-related functions.
|
||||
*/
|
||||
package timex
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package tplx
|
||||
|
||||
const (
|
||||
TplTypeText tplType = iota
|
||||
TplTypeHtml
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package tplx provides some "shortcuts" to [text/template] and [html/template] rendering.
|
||||
*/
|
||||
package tplx
|
||||
@@ -0,0 +1,9 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidTplType = errors.New("unknown/invalid template type")
|
||||
)
|
||||
+235
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
- base64, base16, base32, hex, pem encoding/decoding tpl funcs
|
||||
-- template hashing functions?
|
||||
|
||||
- os env vars
|
||||
@@ -0,0 +1,5 @@
|
||||
[source,subs="+attributes,+post_replacements",opts="novalidate"]
|
||||
.Function Signature
|
||||
----
|
||||
func {func}{sig}
|
||||
----
|
||||
@@ -0,0 +1,101 @@
|
||||
################################################################################
|
||||
# RUNTIME #
|
||||
################################################################################
|
||||
|
||||
{{- $rntm := sysRuntime }}
|
||||
|
||||
Arch: {{ sysArch }}
|
||||
CPUs: {{ sysNumCpu }}
|
||||
OS: {{ sysNumCpu }}
|
||||
|
||||
RUNTIME: {{ $rntm }}
|
||||
{{ range $rntmk, $rntmv := $rntm }}
|
||||
{{ $rntmk }}:
|
||||
{{ $rntmv }}
|
||||
{{- end }}
|
||||
{{ dump $rntm }}
|
||||
|
||||
|
||||
################################################################################
|
||||
# PATHS #
|
||||
################################################################################
|
||||
|
||||
###########
|
||||
# Generic #
|
||||
###########
|
||||
|
||||
pathJoin "a" "b" "c"
|
||||
{{ pathJoin "a" "b" "c" }}
|
||||
|
||||
pathJoin "/" "a" "b" "c"
|
||||
{{ pathJoin "/" "a" "b" "c" }}
|
||||
|
||||
pathJoin "/a" "b" "c"
|
||||
{{ pathJoin "/a" "b" "c" }}
|
||||
|
||||
#
|
||||
|
||||
pathPipeJoin "b" "c" "d" "a"
|
||||
{{ pathPipeJoin "b" "c" "d" "a" }}
|
||||
|
||||
"a" | pathPipeJoin "b" "c" "d"
|
||||
{{ "a" | pathPipeJoin "b" "c" "d"}}
|
||||
#
|
||||
|
||||
$base := "/"
|
||||
$myPsjSlice := "a,b,c" | splitList ","
|
||||
pathSliceJoin $myPsjSlice
|
||||
{{- $base := "/" }}
|
||||
{{- $myPsjSlice := "a,b,c" | splitList "," }}
|
||||
{{ pathSliceJoin $myPsjSlice }}
|
||||
|
||||
#
|
||||
|
||||
$base | pathSlicePipeJoin $myPsjSlice
|
||||
{{ $base | pathSlicePipeJoin $myPsjSlice }}
|
||||
|
||||
#
|
||||
|
||||
pathSubJoin $base "a" "b" "c"
|
||||
{{ pathSubJoin $base "a" "b" "c" }}
|
||||
|
||||
|
||||
######################
|
||||
# OS/System/Platform #
|
||||
######################
|
||||
|
||||
osPathJoin "a" "b" "c"
|
||||
{{ osPathJoin "a" "b" "c" }}
|
||||
|
||||
osPathJoin "/" "a" "b" "c"
|
||||
{{ osPathJoin "a" "b" "c" }}
|
||||
|
||||
osPathJoin "/a" "b" "c"
|
||||
{{ osPathJoin "a" "b" "c" }}
|
||||
|
||||
#
|
||||
|
||||
osPathPipeJoin "b" "c" "d" "a"
|
||||
{{ osPathPipeJoin "b" "c" "d" "a" }}
|
||||
|
||||
"a" | osPathPipeJoin "b" "c" "d"
|
||||
{{ "a" | osPathPipeJoin "b" "c" "d" }}
|
||||
|
||||
#
|
||||
|
||||
$osBase := "/"
|
||||
$myOsPsjSlice := "a,b,c" | splitList ","
|
||||
osPathSliceJoin $myOsPsjSlice
|
||||
{{- $osBase := "/" }}
|
||||
{{- $myOsPsjSlice := "a,b,c" | splitList "," }}
|
||||
{{ osPathSliceJoin $myOsPsjSlice }}
|
||||
|
||||
#
|
||||
|
||||
$osBase | osPathSlicePipeJoin $myOsPsjSlice
|
||||
{{ $osBase | osPathSlicePipeJoin $myOsPsjSlice }}
|
||||
|
||||
#
|
||||
|
||||
osPathSubJoin $osBase "a" "b" "c"
|
||||
{{ osPathSubJoin $osBase "a" "b" "c" }}
|
||||
@@ -0,0 +1,249 @@
|
||||
package sprigx
|
||||
|
||||
import (
|
||||
`net`
|
||||
`net/netip`
|
||||
`os`
|
||||
`os/user`
|
||||
`path`
|
||||
`path/filepath`
|
||||
`runtime`
|
||||
`time`
|
||||
|
||||
`github.com/davecgh/go-spew/spew`
|
||||
`github.com/shirou/gopsutil/v4/cpu`
|
||||
`github.com/shirou/gopsutil/v4/disk`
|
||||
`github.com/shirou/gopsutil/v4/host`
|
||||
`github.com/shirou/gopsutil/v4/load`
|
||||
`github.com/shirou/gopsutil/v4/mem`
|
||||
psnet `github.com/shirou/gopsutil/v4/net`
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
`github.com/shirou/gopsutil/v4/sensors`
|
||||
`go4.org/netipx`
|
||||
`r00t2.io/goutils/netx`
|
||||
`r00t2.io/goutils/netx/dnsx`
|
||||
`r00t2.io/goutils/stringsx`
|
||||
`r00t2.io/goutils/timex`
|
||||
`r00t2.io/sysutils`
|
||||
)
|
||||
|
||||
var (
|
||||
// genericMap holds functions usable/intended for use in either an [html/template.FuncMap] or [text/template.FuncMap].
|
||||
genericMap map[string]any = map[string]any{
|
||||
// Debugging
|
||||
"dump": spew.Sdump,
|
||||
/*
|
||||
"Meta"/Template-Helpers
|
||||
*/
|
||||
"metaIsNil": metaIsNil,
|
||||
/*
|
||||
Networking (net)
|
||||
*/
|
||||
"netCidrMask": net.CIDRMask,
|
||||
"netExtractAddr": netExtractAddr,
|
||||
"netExtractHost": netExtractHost,
|
||||
"netExtractIpnet": netExtractIpnet,
|
||||
"netExtractPort": netExtractPort,
|
||||
"netIfaces": net.Interfaces,
|
||||
"netIp4Mask": netIp4Mask,
|
||||
"netJoinHostPort": net.JoinHostPort,
|
||||
"netParseIP": net.ParseIP,
|
||||
/*
|
||||
Networking (net/netip)
|
||||
*/
|
||||
"netipAddrPort": netip.AddrPortFrom,
|
||||
"netipParseAddr": netip.ParseAddr,
|
||||
"netipParseAddrPort": netip.ParseAddrPort,
|
||||
"netipParsePrefix": netip.ParsePrefix,
|
||||
"netipPrefix": netip.PrefixFrom,
|
||||
/*
|
||||
Networking (go4.org/netipx)
|
||||
*/
|
||||
"netipxAddrIpNet": netipx.AddrIPNet,
|
||||
"netipxCmpPfx": netipx.ComparePrefix,
|
||||
"netipxFromStdAddr": netipxFromStdAddr,
|
||||
"netipxFromIp": netipxFromIp,
|
||||
"netipxFromIpNet": netipxFromIpNet,
|
||||
"netipxParseRange": netipx.ParseIPRange,
|
||||
"netipxPfxAddr": netipx.ParsePrefixOrAddr,
|
||||
"netipxPfxIpNet": netipx.PrefixIPNet,
|
||||
"netipxPfxLast": netipx.PrefixLastIP,
|
||||
"netipxPfxRange": netipx.RangeOfPrefix,
|
||||
"netipxRange": netipx.IPRangeFrom,
|
||||
/*
|
||||
Networking (r00t.io/goutils/netx)
|
||||
*/
|
||||
"netxAddrRfc": netx.AddrRfc,
|
||||
"netxCidr4IpMask": netx.Cidr4ToIPMask,
|
||||
"netxCidr4Mask": netx.Cidr4ToMask,
|
||||
"netxCidr4Str": netx.Cidr4ToStr,
|
||||
"netxFamilyVer": netx.FamilyToVer,
|
||||
"netxGetAddrFam": netx.GetAddrFamily,
|
||||
"netxGetIpFam": netx.GetIpFamily,
|
||||
"netxIpRfc": netx.IpRfc,
|
||||
"netxIpRfcStr": netx.IpRfcStr,
|
||||
"netxIpStripRfc": netx.IpStripRfcStr,
|
||||
"netxIp4MaskCidr": netx.IPMask4ToCidr,
|
||||
"netxIp4MaskMask": netx.IPMask4ToMask,
|
||||
"netxIp4MaskStr": netx.IPMask4ToStr,
|
||||
"netxIpVerStr": netx.IpVerStr,
|
||||
"netxIsBrktd6": netx.IsBracketedIp6,
|
||||
"netxIsIp": netx.IsIpAddr,
|
||||
"netxIsPfx": netx.IsPrefixNet,
|
||||
"netxMask4Cidr": netx.Mask4ToCidr,
|
||||
"netxMask4StrCidr": netx.Mask4StrToCidr,
|
||||
"netxMask4StrIpMask": netx.Mask4StrToIPMask,
|
||||
"netxMask4StrMask": netx.Mask4StrToMask,
|
||||
"netxVerFamily": netx.VerToFamily,
|
||||
/*
|
||||
Networking (r00t.io/goutils/netx/dnsx)
|
||||
*/
|
||||
"dnsxPtrAddr": dnsx.AddrFromPtr,
|
||||
"dnsxAddrPtr": dnsx.AddrToPtr,
|
||||
"dnsxStrWire": dnsx.DnsStrToWire,
|
||||
"dnsxWireStr": dnsx.DnsWireToStr,
|
||||
"dnsxPtrIp": dnsx.IpFromPtr,
|
||||
"dnsxIpPtr": dnsx.IpToPtr,
|
||||
"dnsxIsFqdn": dnsx.IsFqdn,
|
||||
"dnsxIsTxt": dnsx.IsFqdnDefinedTxt,
|
||||
"dnsxIsNsec3": dnsx.IsFqdnNsec3,
|
||||
"dnsxIsSrv": dnsx.IsFqdnSrv,
|
||||
"dnsxIsWild": dnsx.IsFqdnWildcard,
|
||||
"dnsxIsLbl": dnsx.IsLabel,
|
||||
"dnsxIsPtr": dnsx.IsPtr,
|
||||
/*
|
||||
Numbers/Math
|
||||
*/
|
||||
"numFloat32Str": numFloat32Str,
|
||||
"numFloat64": numFloat64,
|
||||
"numFloat64Str": numFloat64Str,
|
||||
"numFloatStr": numFloatStr,
|
||||
/*
|
||||
OS
|
||||
*/
|
||||
"osFQDN": osFQDN,
|
||||
"osGroupById": osGroupById,
|
||||
"osGroupByName": user.LookupGroup,
|
||||
"osHost": osHost,
|
||||
"osHostname": os.Hostname,
|
||||
"osIdState": sysutils.GetIDState,
|
||||
"osUser": user.Current,
|
||||
"osUserById": osUserById,
|
||||
"osUserByName": user.Lookup,
|
||||
/*
|
||||
Paths
|
||||
*/
|
||||
// Paths: Generic
|
||||
"pathJoin": path.Join,
|
||||
"pathPipeJoin": pathPipeJoin,
|
||||
"pathSliceJoin": pathSliceJoin,
|
||||
"pathSlicePipeJoin": pathSlicePipeJoin,
|
||||
"pathSubJoin": pathSubJoin,
|
||||
// Paths: OS/Platform
|
||||
"osPathJoin": filepath.Join,
|
||||
"osPathPipeJoin": osPathPipeJoin,
|
||||
"osPathSep": osPathSep,
|
||||
"osPathSliceJoin": osPathSliceJoin,
|
||||
"osPathSlicePipeJoin": osPathSlicePipeJoin,
|
||||
"osPathSubJoin": osPathSubJoin,
|
||||
/*
|
||||
PSUtil
|
||||
(https://pkg.go.dev/github.com/shirou/gopsutil/v4)
|
||||
*/
|
||||
// .../cpu
|
||||
"psCpuCnts": cpu.Counts,
|
||||
"psCpuInfo": cpu.Info,
|
||||
"psCpuPct": cpu.Percent,
|
||||
"psCpuTimes": cpu.Times,
|
||||
// .../disk
|
||||
"psDiskIoCnts": disk.IOCounters,
|
||||
"psDiskLabel": disk.Label,
|
||||
"psDiskParts": disk.Partitions,
|
||||
"psDiskSerial": disk.SerialNumber,
|
||||
"psDiskUsage": disk.Usage,
|
||||
// .../host
|
||||
"psHostBoot": host.BootTime,
|
||||
"psHostId": host.HostID,
|
||||
"psHostInfo": host.Info,
|
||||
"psHostKernArch": host.KernelArch,
|
||||
"psHostKernVer": host.KernelVersion,
|
||||
"psHostPlatInfo": psHostPlatInfo,
|
||||
"psHostUptime": host.Uptime,
|
||||
"psHostUsers": host.Users,
|
||||
"psHostVirt": psHostVirt,
|
||||
// .../load
|
||||
"psLoadAvg": load.Avg,
|
||||
"psLoadMisc": load.Misc,
|
||||
// .../mem
|
||||
"psMemSwap": mem.SwapMemory,
|
||||
"psMemSwapDevs": mem.SwapDevices,
|
||||
"psMemVMem": mem.VirtualMemory,
|
||||
// .../net
|
||||
"psNetConns": psnet.Connections,
|
||||
"psNetConnsMax": psnet.ConnectionsMax,
|
||||
"psNetConnsPid": psnet.ConnectionsPid,
|
||||
"psNetConnsPidMax": psnet.ConnectionsPidMax,
|
||||
"psNetCTStats": psnet.ConntrackStats,
|
||||
"psNetCTStatList": psnet.NewConntrackStatList,
|
||||
"psNetFilterCnts": psnet.FilterCounters,
|
||||
"psNetIoCnts": psnet.IOCounters,
|
||||
"psNetIoCntsFile": psnet.IOCountersByFile,
|
||||
"psNetIfaces": psnet.Interfaces,
|
||||
"psNetPids": psnet.Pids,
|
||||
"psNetProtoCnt": psnet.ProtoCounters,
|
||||
// .../process
|
||||
"psProcs": process.Processes,
|
||||
"psProcNew": process.NewProcess,
|
||||
"psProcPids": process.Pids,
|
||||
"psProcPidExists": process.PidExists,
|
||||
// .../sensors
|
||||
"psSensorTemps": sensors.SensorsTemperatures,
|
||||
/*
|
||||
Strings (Standalone)
|
||||
*/
|
||||
"extIndent": extIndent, // PR in: https://github.com/Masterminds/sprig/pull/468
|
||||
/*
|
||||
Strings (r00t.io/goutils/stringsx)
|
||||
*/
|
||||
"strsxIsAscii": stringsx.IsAscii,
|
||||
"strsxIsAsciiBuf": stringsx.IsAsciiBuf,
|
||||
"strsxIsAsciiSpcl": stringsx.IsAsciiSpecial,
|
||||
"strsxIsAsciiBufSpcl": stringsx.IsAsciiBufSpecial,
|
||||
"strsxLenSpl": stringsx.LenSplit,
|
||||
"strsxLenSplStr": stringsx.LenSplitStr,
|
||||
"strsxPad": stringsx.Pad,
|
||||
"strsxRedact": stringsx.Redact,
|
||||
"strsxRev": stringsx.Reverse,
|
||||
"strsxTrimLns": stringsx.TrimLines,
|
||||
"strsxTrimSpcLft": stringsx.TrimSpaceLeft,
|
||||
"strsxTrimSpcRt": stringsx.TrimSpaceRight,
|
||||
/*
|
||||
System/Platform
|
||||
*/
|
||||
"sysArch": sysArch,
|
||||
"sysNumCpu": runtime.NumCPU,
|
||||
"sysOsName": sysOsNm,
|
||||
"sysRuntime": sysRuntime,
|
||||
/*
|
||||
Time/Dates/Timestamps
|
||||
*/
|
||||
"tmDate": time.Date,
|
||||
"tmFmt": tmFmt,
|
||||
"tmFloatMicro": timex.F64Microseconds,
|
||||
"tmFloatMilli": timex.F64Milliseconds,
|
||||
"tmFloatNano": timex.F64Nanoseconds,
|
||||
"tmFloat": timex.F64Seconds,
|
||||
"tmNow": time.Now,
|
||||
"tmParseDur8n": time.ParseDuration,
|
||||
"tmParseMonth": tmParseMonth,
|
||||
"tmParseMonthInt": tmParseMonthInt,
|
||||
"tmParseMonthStr": tmParseMonthStr,
|
||||
"tmParseTime": time.Parse,
|
||||
}
|
||||
|
||||
// htmlMap holds functions usable/intended for use in only an [html/template.FuncMap].
|
||||
htmlMap map[string]any = map[string]any{}
|
||||
|
||||
// txtMap holds functions usable/intended for use in only a [text/template.FuncMap].
|
||||
txtMap map[string]any = map[string]any{}
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build darwin
|
||||
|
||||
package sprigx
|
||||
|
||||
var (
|
||||
osGenericMap map[string]any = map[string]any{}
|
||||
osHtmlMap map[string]any = map[string]any{}
|
||||
osTxtMap map[string]any = map[string]any{}
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build linux
|
||||
|
||||
package sprigx
|
||||
|
||||
import (
|
||||
`github.com/shirou/gopsutil/v4/mem`
|
||||
psnet `github.com/shirou/gopsutil/v4/net`
|
||||
)
|
||||
|
||||
var (
|
||||
osGenericMap map[string]any = map[string]any{
|
||||
/*
|
||||
PSUtil
|
||||
(https://pkg.go.dev/github.com/shirou/gopsutil/v4)
|
||||
*/
|
||||
// .../mem
|
||||
"psMemExVMem": mem.NewExLinux().VirtualMemory,
|
||||
// .../net
|
||||
"psNetRev": psnet.Reverse,
|
||||
// .../sensors
|
||||
"psSensorExTemp": psSensorExTemp,
|
||||
}
|
||||
osHtmlMap map[string]any = map[string]any{}
|
||||
osTxtMap map[string]any = map[string]any{}
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !(linux || windows || darwin)
|
||||
|
||||
package sprigx
|
||||
|
||||
var (
|
||||
osGenericMap map[string]any = map[string]any{}
|
||||
osHtmlMap map[string]any = map[string]any{}
|
||||
osTxtMap map[string]any = map[string]any{}
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build windows
|
||||
|
||||
package sprigx
|
||||
|
||||
import (
|
||||
`github.com/shirou/gopsutil/v4/mem`
|
||||
`github.com/shirou/gopsutil/v4/winservices`
|
||||
)
|
||||
|
||||
var (
|
||||
osGenericMap map[string]any = map[string]any{
|
||||
/*
|
||||
PSUtil
|
||||
(https://pkg.go.dev/github.com/shirou/gopsutil/v4)
|
||||
*/
|
||||
// .../mem
|
||||
"psMemExVMem": mem.NewExWindows().VirtualMemory,
|
||||
// .../winservices
|
||||
"psWinsvcList": winservices.ListServices,
|
||||
"psWinsvcNew": winservices.NewService,
|
||||
}
|
||||
osHtmlMap map[string]any = map[string]any{}
|
||||
osTxtMap map[string]any = map[string]any{}
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
Package sprigx aims to provide additional functions that the author believes are missing from [sprig] ([Go docs]).
|
||||
|
||||
It's a decent enough "basics" library, but I frequently find it falls short once you start needing domain-specific data.
|
||||
|
||||
These may get merged into sprig, they may not. It all depends on how responsive they are to PRs.
|
||||
Given that they only update it every 6 months or so, however...
|
||||
|
||||
See the [full documentation] on the [repo].
|
||||
|
||||
[sprig]: https://masterminds.github.io/sprig/
|
||||
[Go docs]: https://pkg.go.dev/github.com/Masterminds/sprig/v3
|
||||
[full documentation]: https://git.r00t2.io/r00t2/go_goutils/src/branch/master/tplx/sprigx/README.adoc
|
||||
[repo]: https://git.r00t2.io/r00t2/go_goutils
|
||||
*/
|
||||
package sprigx
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- https://stackoverflow.com/a/34481639 -->
|
||||
<!-- Generate a nice TOC -->
|
||||
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
|
||||
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tocify/1.9.0/javascripts/jquery.tocify.min.js"></script>
|
||||
<!-- We do not need the tocify CSS because the asciidoc CSS already provides most of what we neeed -->
|
||||
|
||||
<style>
|
||||
.tocify-header {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tocify-subheader {
|
||||
font-style: normal;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.tocify ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tocify-focus {
|
||||
color: #7a2518;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tocify-focus > a {
|
||||
color: #7a2518;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
// Add a new container for the tocify toc into the existing toc so we can re-use its
|
||||
// styling
|
||||
$("#toc").append("<div id='generated-toc'></div>");
|
||||
$("#generated-toc").tocify({
|
||||
extendPage: true,
|
||||
context: "#content",
|
||||
highlightOnScroll: true,
|
||||
hideEffect: "slideUp",
|
||||
// Use the IDs that asciidoc already provides so that TOC links and intra-document
|
||||
// links are the same. Anything else might confuse users when they create bookmarks.
|
||||
hashGenerator: function(text, element) {
|
||||
return $(element).attr("id");
|
||||
},
|
||||
// Smooth scrolling doesn't work properly if we use the asciidoc IDs
|
||||
smoothScroll: false,
|
||||
// Set to 'none' to use the tocify classes
|
||||
theme: "none",
|
||||
// Handle book (may contain h1) and article (only h2 deeper)
|
||||
selectors: $( "#content" ).has( "h1" ).size() > 0 ? "h1,h2,h3,h4,h5" : "h2,h3,h4,h5",
|
||||
ignoreSelector: ".discrete"
|
||||
});
|
||||
|
||||
// Switch between static asciidoc toc and dynamic tocify toc based on browser size
|
||||
// This is set to match the media selectors in the asciidoc CSS
|
||||
// Without this, we keep the dynamic toc even if it is moved from the side to preamble
|
||||
// position which will cause odd scrolling behavior
|
||||
var handleTocOnResize = function() {
|
||||
if ($(document).width() < 768) {
|
||||
$("#generated-toc").hide();
|
||||
$(".sectlevel0").show();
|
||||
$(".sectlevel1").show();
|
||||
}
|
||||
else {
|
||||
$("#generated-toc").show();
|
||||
$(".sectlevel0").hide();
|
||||
$(".sectlevel1").hide();
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(handleTocOnResize);
|
||||
handleTocOnResize();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package sprigx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadAddr error = errors.New("invalid/bad address")
|
||||
ErrBadAddrPort error = errors.New("invalid/bad address/port")
|
||||
ErrBadMonth error = errors.New("could not determine/parse month")
|
||||
ErrBadNet error = errors.New("invalid/bad network")
|
||||
ErrOverflow error = errors.New("integer/buffer overflow")
|
||||
ErrBadType error = errors.New("an invalid/unknown type was passed")
|
||||
ErrNilVal error = errors.New("a nil value was passed")
|
||||
ErrUnderflow error = errors.New("integer/buffer underflow")
|
||||
)
|
||||
@@ -0,0 +1,356 @@
|
||||
package sprigx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
htpl "html/template"
|
||||
`math`
|
||||
`reflect`
|
||||
`strconv`
|
||||
ttpl "text/template"
|
||||
|
||||
`github.com/Masterminds/sprig/v3`
|
||||
)
|
||||
|
||||
/*
|
||||
Many of these functions are modeled after sprig's.
|
||||
*/
|
||||
|
||||
/*
|
||||
CombinedFuncMap returns a generic function map (like [FuncMap]) combined with
|
||||
[github.com/Masterminds/sprig/v3.GenericFuncMap].
|
||||
|
||||
If preferSprigx is true, SprigX function names will override Sprig
|
||||
functions with the same name.
|
||||
If false, Sprig functions will override conflicting SprigX functions
|
||||
with the same name.
|
||||
|
||||
You probably want [CombinedHtmlFuncMap] or [CombinedTxtFuncMap] instead,
|
||||
as they wrap this with the appropriate type.
|
||||
*/
|
||||
func CombinedFuncMap(preferSprigX bool) (fmap map[string]any) {
|
||||
|
||||
var fn any
|
||||
var fnNm string
|
||||
var sprigMap map[string]interface{} = sprig.GenericFuncMap()
|
||||
var sprigxMap map[string]any = FuncMap()
|
||||
|
||||
if preferSprigX {
|
||||
fmap = sprigMap
|
||||
for fnNm, fn = range sprigxMap {
|
||||
fmap[fnNm] = fn
|
||||
}
|
||||
} else {
|
||||
fmap = sprigxMap
|
||||
for fnNm, fn = range sprigMap {
|
||||
fmap[fnNm] = fn
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CombinedHtmlFuncMap returns an [htpl.FuncMap] (like [HtmlFuncMap]) combined with
|
||||
[github.com/Masterminds/sprig/v3.HtmlFuncMap].
|
||||
|
||||
If preferSprigx is true, SprigX function names will override Sprig
|
||||
functions with the same name.
|
||||
If false, Sprig functions will override conflicting SprigX functions
|
||||
with the same name.
|
||||
*/
|
||||
func CombinedHtmlFuncMap(preferSprigX bool) (fmap htpl.FuncMap) {
|
||||
|
||||
var fn any
|
||||
var fnNm string
|
||||
var sprigMap htpl.FuncMap = sprig.HtmlFuncMap()
|
||||
var sprigxMap htpl.FuncMap = HtmlFuncMap()
|
||||
|
||||
if preferSprigX {
|
||||
fmap = sprigMap
|
||||
for fnNm, fn = range sprigxMap {
|
||||
fmap[fnNm] = fn
|
||||
}
|
||||
} else {
|
||||
fmap = sprigxMap
|
||||
for fnNm, fn = range sprigMap {
|
||||
fmap[fnNm] = fn
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CombinedTxtFuncMap returns a [ttpl.FuncMap] (like [TxtFuncMap]) combined with
|
||||
[github.com/Masterminds/sprig/v3.TxtFuncMap].
|
||||
|
||||
If preferSprigx is true, SprigX function names will override Sprig
|
||||
functions with the same name.
|
||||
If false, Sprig functions will override conflicting SprigX functions
|
||||
with the same name.
|
||||
*/
|
||||
func CombinedTxtFuncMap(preferSprigX bool) (fmap ttpl.FuncMap) {
|
||||
|
||||
var fn any
|
||||
var fnNm string
|
||||
var sprigMap ttpl.FuncMap = sprig.TxtFuncMap()
|
||||
var sprigxMap ttpl.FuncMap = TxtFuncMap()
|
||||
|
||||
if preferSprigX {
|
||||
fmap = sprigMap
|
||||
for fnNm, fn = range sprigxMap {
|
||||
fmap[fnNm] = fn
|
||||
}
|
||||
} else {
|
||||
fmap = sprigxMap
|
||||
for fnNm, fn = range sprigMap {
|
||||
fmap[fnNm] = fn
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
FuncMap returns a generic function map.
|
||||
|
||||
You probably want [HtmlFuncMap] or [TxtFuncMap] instead,
|
||||
as they wrap this with the appropriate type.
|
||||
*/
|
||||
func FuncMap() (fmap map[string]any) {
|
||||
|
||||
var fn string
|
||||
var f any
|
||||
|
||||
fmap = make(map[string]any, len(genericMap))
|
||||
|
||||
for fn, f = range genericMap {
|
||||
fmap[fn] = f
|
||||
}
|
||||
if osGenericMap != nil && len(osGenericMap) > 0 {
|
||||
for fn, f = range osGenericMap {
|
||||
fmap[fn] = f
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// HtmlFuncMap returns an [html/template.FuncMap].
|
||||
func HtmlFuncMap() (fmap htpl.FuncMap) {
|
||||
|
||||
var fn string
|
||||
var f any
|
||||
|
||||
fmap = htpl.FuncMap(FuncMap())
|
||||
|
||||
if htmlMap != nil && len(htmlMap) > 0 {
|
||||
for fn, f = range htmlMap {
|
||||
fmap[fn] = f
|
||||
}
|
||||
}
|
||||
|
||||
if osHtmlMap != nil && len(osHtmlMap) > 0 {
|
||||
for fn, f = range osHtmlMap {
|
||||
fmap[fn] = f
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Nop explicitly performs a NO-OP and returns an empty string, allowing one to override "unsafe" functions.
|
||||
func Nop(obj ...any) (s string) {
|
||||
return
|
||||
}
|
||||
|
||||
// TxtFuncMap returns a [text/template.FuncMap].
|
||||
func TxtFuncMap() (fmap ttpl.FuncMap) {
|
||||
|
||||
var fn string
|
||||
var f any
|
||||
|
||||
fmap = ttpl.FuncMap(FuncMap())
|
||||
|
||||
if txtMap != nil && len(txtMap) > 0 {
|
||||
for fn, f = range txtMap {
|
||||
fmap[fn] = f
|
||||
}
|
||||
}
|
||||
|
||||
if osTxtMap != nil && len(osTxtMap) > 0 {
|
||||
for fn, f = range osTxtMap {
|
||||
fmap[fn] = f
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
toFloat64 uses reflection to resolve any string or numeric type (even custom types) to a float64.
|
||||
|
||||
It wraps toString for string types but will fall back to checking numeric types.
|
||||
|
||||
If err != nil, then NaN (if true) indicates that:
|
||||
|
||||
* val is a string (or pointer to a string), but
|
||||
* is not a valid numeric string
|
||||
|
||||
(you can do this from the caller as well by calling `errors.Is(err, strconv.ErrSyntax)`).
|
||||
err will always be non-nil if NaN is true.
|
||||
|
||||
err will be ErrNilVal if val is nil.
|
||||
*/
|
||||
func toFloat64(val any) (f float64, NaN bool, err error) {
|
||||
|
||||
var s string
|
||||
var k reflect.Kind
|
||||
var rv reflect.Value
|
||||
|
||||
// toString will return ErrNilVal if nil.
|
||||
if s, err = toString(val); err != nil {
|
||||
if errors.Is(err, ErrBadType) {
|
||||
// This is OK, it's (hopefully) a number type.
|
||||
err = nil
|
||||
} else {
|
||||
// *probably* ErrNilVal.
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// We can go ahead and parse this directly since it's already deref'd if a ptr.
|
||||
if f, err = strconv.ParseFloat(s, 64); err != nil {
|
||||
NaN = errors.Is(err, strconv.ErrSyntax)
|
||||
}
|
||||
// We can return regardless here; it's up to the caller to check NaN/err.
|
||||
// If they're false/nil, f is parsed already!
|
||||
return
|
||||
}
|
||||
|
||||
rv = reflect.ValueOf(val)
|
||||
k = rv.Kind()
|
||||
|
||||
if k == reflect.Ptr {
|
||||
if rv.IsNil() {
|
||||
// *technically* this should be handled above, but best be safe.
|
||||
err = ErrNilVal
|
||||
return
|
||||
}
|
||||
rv = rv.Elem()
|
||||
k = rv.Kind()
|
||||
}
|
||||
|
||||
switch k {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
f = float64(rv.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
f = float64(rv.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f = rv.Float()
|
||||
default:
|
||||
// No need to check for string types since we do that near the beginning.
|
||||
err = ErrBadType
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
toInt wraps toFloat64, rounds it to the nearest integer,
|
||||
and converts to an int.
|
||||
|
||||
NaN, err have the same meaning as in toFloat64.
|
||||
|
||||
This function will panic if float64(val)'s f return exceeds
|
||||
math.MaxInt on your platform.
|
||||
*/
|
||||
func toInt(val any) (i int, NaN bool, err error) {
|
||||
|
||||
var f float64
|
||||
|
||||
if f, NaN, err = toFloat64(val); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
i = int(math.Round(f))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
toPosFloat64 wraps toFloat64 and ensures that it is a positive float64.
|
||||
|
||||
NaN, err have the same meaning as in toFloat64.
|
||||
*/
|
||||
func toPosFloat64(val any) (f float64, NaN bool, err error) {
|
||||
|
||||
if f, NaN, err = toFloat64(val); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f = math.Abs(f)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
toPosInt wraps toPosFloat64, rounds it to the nearest integer,
|
||||
and converts to an int.
|
||||
|
||||
NaN, err have the same meaning as in toPosFloat64 (and thus toFloat64).
|
||||
|
||||
This function will panic if float64(val)'s f return exceeds
|
||||
math.MaxInt on your platform.
|
||||
*/
|
||||
func toPosInt(val any) (i int, NaN bool, err error) {
|
||||
|
||||
var f float64
|
||||
|
||||
if f, NaN, err = toPosFloat64(val); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
i = int(math.Round(f))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
toString uses reflection to resolve any string value (even custom types and ptrs)
|
||||
to a concrete string.
|
||||
|
||||
err will be ErrBadType if val is not a string type/string-derived type.
|
||||
err will be ErrNilVal if val is nil.
|
||||
*/
|
||||
func toString(val any) (s string, err error) {
|
||||
|
||||
var rv reflect.Value
|
||||
var k reflect.Kind
|
||||
|
||||
if val == nil {
|
||||
err = ErrNilVal
|
||||
return
|
||||
}
|
||||
|
||||
rv = reflect.ValueOf(val)
|
||||
k = rv.Kind()
|
||||
|
||||
if k == reflect.Ptr {
|
||||
if rv.IsNil() {
|
||||
// *technically* this should be handled above, but best be safe.
|
||||
err = ErrNilVal
|
||||
return
|
||||
}
|
||||
rv = rv.Elem()
|
||||
k = rv.Kind()
|
||||
}
|
||||
|
||||
if k == reflect.String {
|
||||
s = rv.String()
|
||||
} else {
|
||||
err = ErrBadType
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user