Compare commits

..

7 Commits

Author SHA1 Message Date
brent saner 58556d7281 v1.16.9
ADDED:
* netx.IsPub
* encodingx/hexx

Rest are mostly small corrections and docs
2026-06-22 18:51:13 -04:00
brent saner c6fc692f5e checking in some WIP
* added some netx funcs
* added netx/dnsx
* currently updating docs and adding *x funcs to sprigx
2026-02-24 17:41:57 -05:00
brent saner 4770052b52 v1.16.8
CLEANUP:
* Docs for uuidx needed to be tweaked/clarified a little bit.
2026-02-20 12:41:29 -05:00
brent saner 1eea0c2672 v1.16.7
ADDED:
* uuidx
FIXED:
* sprigx docs consistency
2026-02-11 10:21:29 -05:00
brent saner 67c7faf449 v1.16.6
FIXED:
* tplx/sprigx float to string non-truncating was... truncating. it no
  longer is.
2026-01-30 20:21:34 -05:00
brent saner 82c69ec542 v1.16.5
FIXED:
* Misaligned `Nop` in README.adoc
2026-01-30 06:49:22 -05:00
brent saner 07e0e587fa v1.16.4
ADDED:
* math, time functions to tplx/sprigx
FIXED:
* logging not initializing properly on some BSDs
2026-01-30 06:35:23 -05:00
61 changed files with 15601 additions and 1093 deletions
+47 -2
View File
@@ -15,23 +15,68 @@ for f in $(find . -type f -iname "README.adoc"); do
nosuffix="${filename%.*}" nosuffix="${filename%.*}"
pfx="${docsdir}/${nosuffix}" pfx="${docsdir}/${nosuffix}"
# Render HTML, include in commit
newf="${pfx}.html" newf="${pfx}.html"
asciidoctor -a ROOTDIR="${orig}/" -o "${newf}" "${f}" asciidoctor -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
echo "Generated ${newf} from ${f}" echo "Generated ${newf} from ${f}"
git add "${newf}" 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; if command -v pandoc &> /dev/null;
then then
newf="${pfx}.md" newf="${pfx}.md"
set +e set +e
asciidoctor -a ROOTDIR="${orig}/" -b docbook -o - "${f}" | pandoc -f docbook -t markdown_strict -o "${newf}" # --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 ]; if [ $? -eq 0 ];
then then
echo "Generated ${newf} from ${f}" echo "Generated ${newf} from ${f}"
git add "${newf}" git add "${newf}"
else else
echo "Failed to generate ${newf} from ${f}" echo "Failed to generate ${newf} from ${f}"
git rm "${newf}" git rm "${newf}" 2>/dev/null
fi fi
set -e set -e
fi fi
+2 -1
View File
@@ -19,12 +19,13 @@
.idea/ .idea/
# https://github.com/github/gitignore/blob/master/Go.gitignore # https://github.com/github/gitignore/blob/master/Go.gitignore
# Binaries for programs and plugins # Binaries for programs and plugins and other data
*.exe *.exe
*.exe~ *.exe~
*.dll *.dll
*.so *.so
*.dylib *.dylib
*.pdf
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test
+3
View File
@@ -0,0 +1,3 @@
- validx: validator functions for https://pkg.go.dev/github.com/go-playground/validator/v10
- hashx?
+30 -39
View File
@@ -11,11 +11,11 @@ import (
type MaskBit uint type MaskBit uint
/* /*
NewMaskBit is a convenience function. NewMaskBit is a convenience function.
It will return a MaskBit with a (referenced) value of 0, so set your consts up accordingly. 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!) It is highly recommended to set this default as a "None" flag (separate from your iotas!)
as shown in the example. as shown in the example.
*/ */
func NewMaskBit() (m *MaskBit) { func NewMaskBit() (m *MaskBit) {
@@ -35,26 +35,10 @@ func NewMaskBitExplicit(value uint) (m *MaskBit) {
} }
/* /*
HasFlag is true if m has MaskBit flag set/enabled. HasFlag is true if m has MaskBit flag set/enabled.
THIS WILL RETURN FALSE FOR OR'd FLAGS. See the "Composite (OR'd) Flags" section in this module's
documentation for important caveats.
For example:
flagA MaskBit = 0x01
flagB MaskBit = 0x02
flagComposite = flagA | flagB
m *MaskBit = NewMaskBitExplicit(uint(flagA))
m.HasFlag(flagComposite) will return false even though flagComposite is an OR
that contains flagA.
Use [MaskBit.IsOneOf] instead if you do not desire this behavior,
and instead want to test composite flag *membership*.
(MaskBit.IsOneOf will also return true for non-composite equality.)
To be more clear, if MaskBit flag is a composite MaskBit (e.g. flagComposite above),
HasFlag will only return true of ALL bits in flag are also set in MaskBit m.
*/ */
func (m *MaskBit) HasFlag(flag MaskBit) (r bool) { func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {
@@ -67,19 +51,20 @@ func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {
} }
/* /*
IsOneOf is like a "looser" form of [MaskBit.HasFlag] HasOneOf is like a "looser" form of [MaskBit.HasFlag]
in that it allows for testing composite membership. in that it allows for testing composite membership.
See [MaskBit.HasFlag] for more information. 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. If composite is *not* an OR'd MaskBit (i.e.
it falls directly on a boundary -- 0, 1, 2, 4, 8, 16, etc.), it falls directly on a boundary -- 0, 1, 2, 4, 8, 16, etc.),
then IsOneOf will behave exactly like HasFlag. then HasOneOf will behave exactly like [MaskBit.HasFlag].
If m is a composite MaskBit (it usually is) and composite is ALSO a composite MaskBit, If m is a composite [MaskBit] (it usually is) and composite is ALSO a composite MaskBit,
IsOneOf will return true if ANY of the flags set in m is set in composite. HasOneOf will return true if ANY of the flags set in composite is set in m.
*/ */
func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) { func (m *MaskBit) HasOneOf(composite MaskBit) (r bool) {
var b MaskBit = *m var b MaskBit = *m
@@ -89,6 +74,13 @@ func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) {
return 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. // AddFlag adds MaskBit flag to m.
func (m *MaskBit) AddFlag(flag MaskBit) { func (m *MaskBit) AddFlag(flag MaskBit) {
@@ -114,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, 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 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. 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 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) { func (m *MaskBit) Bytes(trim bool) (b []byte) {
+117 -44
View File
@@ -11,21 +11,84 @@ However, a [bitmask.MaskBit] is backed by a uint which (depending on your platfo
"But wait, that takes up more memory though!" "But wait, that takes up more memory though!"
Yep, but bitmasking lets you store a "boolean" AT EACH BIT - it operates on 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. whether a bit in a byte/set of bytes at a given position is 0 or 1.
Which means on 32-bit platforms, a [MaskBit] can have up to 4294967295 "booleans" in a single value (0 to (2^32)-1). In other words:
On 64-bit platforms, a [MaskBit] can have up to 18446744073709551615 "booleans" in a single value (0 to (2^64)-1). type (
OptStruct struct {
A bool
B bool
C bool
D bool
E bool
F bool
G bool
H bool
}
)
If you tried to do that with Go bool values, that'd take up 4294967295 bytes (4 GiB) One instance of OptStruct takes up *8 bytes* (*64 bits*).
or 18446744073709551615 bytes (16 EiB - yes, that's [exbibytes]) of RAM for 32-bit/64-bit platforms respectively.
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!" "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. Nope. It's not using compression or anything, the CPU is just comparing bit "A" vs. bit "B" 32/64 times.
That's super easy work for a CPU.
There's a reason Doom used bitmasking for the "dmflags" value in its server configs. 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 # Usage
@@ -34,6 +97,8 @@ To use this library, set constants like thus:
package main package main
import ( import (
"fmt"
"r00t2.io/goutils/bitmask" "r00t2.io/goutils/bitmask"
) )
@@ -45,34 +110,32 @@ To use this library, set constants like thus:
// ... // ...
) )
var MyMask *bitmask.MaskBit var (
MyMask *bitmask.MaskBit = bitmask.NewMaskBit()
)
func main() { func main() {
MyMask = bitmask.NewMaskBit()
MyMask.AddFlag(OPT1) MyMask.AddFlag(OPT1)
MyMask.AddFlag(OPT3) 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:
MyMask.HasFlag(OPT1)
As would this:
MyMask.HasFlag(OPT3)
But this would return false:
MyMask.HasFlag(OPT2)
# Technical Caveats # Technical Caveats
TARGETING Targeting
When implementing, you should always set a "source" mask (e.g. MyMask, from Usage section above)
as the actual value.
When implementing, you should always set MyMask (from Usage section above) as the actual value.
For example, if you are checking a permissions set for a user that has the value, say, 6 For example, if you are checking a permissions set for a user that has the value, say, 6
var userPerms uint = 6 // 0x0000000000000006 var userPerms uint = 6 // 0x0000000000000006
@@ -88,7 +151,7 @@ and your library has the following permission bits defined:
PermsAdmin // 16 PermsAdmin // 16
) )
And you want to see if the user has the PermsRead flag set, you would do: and you want to see if the user has the PermsRead flag set, you would do:
userPermMask = bitmask.NewMaskBitExplicit(userPerms) userPermMask = bitmask.NewMaskBitExplicit(userPerms)
if userPermMask.HasFlag(PermsRead) { if userPermMask.HasFlag(PermsRead) {
@@ -98,55 +161,65 @@ And you want to see if the user has the PermsRead flag set, you would do:
NOT: NOT:
userPermMask = bitmask.NewMaskBitExplicit(PermsRead) userPermMask = bitmask.NewMaskBitExplicit(PermsRead)
// Nor: userPermMask.HasFlag(bitmask.MaskBit(userPerms))
// userPermMask = PermsRead
if userPermMask.HasFlag(userPerms) { NOR:
userPermMask = PermsRead
if userPermMask.HasFlag(bitmask.MaskBit(userPerms)) {
// ... // ...
} }
This will be terribly, horribly wrong, cause incredibly unexpected results, This will be terribly, horribly wrong, cause incredibly unexpected results,
and quite possibly cause massive security issues. Don't do it. and quite possibly cause massive security issues. Don't do it.
COMPOSITES Remember, [MaskBit.HasFlag] (and other methods) are for comparing/modifying an
*authoritative/concrete value* with one or more *attributes*, not the other way around.
If you want to define a set of flags that are a combination of other flags, Composite Flags
your inclination would be to bitwise-OR them together:
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 ( const (
flagA bitmask.MaskBit = 1 << iota // 1 flagA bitmask.MaskBit = 1 << iota // 1
flagB // 2 flagB // 2
flagC // 4
// ...
) )
const ( const (
flagAB bitmask.MaskBit = flagA | flagB // 3 flagAB bitmask.MaskBit = flagA | flagB // 3
) )
Which is fine and dandy. But if you then have: Which is fine, and the correct approach.
But if you then have:
var myMask *bitmask.MaskBit = bitmask.NewMaskBit() var myMask *bitmask.MaskBit = bitmask.NewMaskBit()
myMask.AddFlag(flagA) myMask.AddFlag(flagA)
You may expect this call to [MaskBit.HasFlag]: you may expect this call to [MaskBit.HasFlag]:
myMask.HasFlag(flagAB) myMask.HasFlag(flagAB)
to be true, since flagA is "in" flagAB. to be true, since flagA is "in" flagAB.
It will return false - HasFlag does strict comparisons. It will return false - HasFlag does strict comparisons.
It will only return true if you then ALSO do:
// This would require setting flagA first. It would only return true if you did:
// The order of setting flagA/flagB doesn't matter,
// but you must have both set for HasFlag(flagAB) to return true.
myMask.AddFlag(flagB)
or if you do: // ...
// This can be done with or without additionally setting flagA.
myMask.AddFlag(flagAB) myMask.AddFlag(flagAB)
Instead, if you want to see if a mask has membership within a composite flag, or:
you can use [MaskBit.IsOneOf].
// ...
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 # Other Options
@@ -157,6 +230,6 @@ you may be interested in one of the following libraries:
* [github.com/abice/go-enum] * [github.com/abice/go-enum]
* [github.com/jeffreyrichter/enum/enum] * [github.com/jeffreyrichter/enum/enum]
[exbibytes]: https://simple.wikipedia.org/wiki/Exbibyte [dmflags]: https://doomwiki.org/wiki/DMFlags
*/ */
package bitmask package bitmask
+6
View File
@@ -0,0 +1,6 @@
/*
Package bytesx aims to extend functionality of the stdlib [bytes] module.
TODO.
*/
package bytesx
Executable
+19
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
/*
Package binaryx aims to extend functionality of the stdlib [encoding/binary] module.
*/
package binaryx
+122
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
/*
Package encodingx aims to extend functionality of the stdlib [encoding] module.
*/
package encodingx
+4
View File
@@ -0,0 +1,4 @@
/*
Package hexx aims to extend [encoding/hex] functionality.
*/
package hexx
+30
View File
@@ -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
}
+67
View File
@@ -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
+16
View File
@@ -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
}
)
+23 -11
View File
@@ -1,35 +1,47 @@
module r00t2.io/goutils module r00t2.io/goutils
go 1.25 go 1.25.0
require ( require (
github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0
github.com/coreos/go-systemd/v22 v22.7.0 github.com/coreos/go-systemd/v22 v22.7.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/shirou/gopsutil/v4 v4.25.12 github.com/olekukonko/tablewriter v1.1.4
github.com/shirou/gopsutil/v4 v4.26.5
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/sys v0.40.0 golang.org/x/sys v0.46.0
r00t2.io/sysutils v1.16.0 r00t2.io/sysutils v1.16.2
) )
require ( require (
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // 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 github.com/djherbis/times v1.6.0 // indirect
github.com/ebitengine/purego v0.9.1 // 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/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/huandu/xstrings v1.5.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // 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/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.12.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.53.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.21.0 // indirect
) )
+54
View File
@@ -4,8 +4,16 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 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 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 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 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -14,11 +22,19 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= 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.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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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.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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -31,10 +47,28 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 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-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 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 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 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
@@ -43,6 +77,10 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= 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 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 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 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -51,21 +89,37 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= 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 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= 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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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 h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= 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 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-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.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=
+4
View File
@@ -26,6 +26,10 @@ func (c *CtxIO) GetChunkLen() (size uint) {
return c.l.GetChunkLen() return c.l.GetChunkLen()
} }
func (c *CtxIO) Len() (unread int) {
return c.buf.Len()
}
func (c *CtxIO) Read(p []byte) (n int, err error) { func (c *CtxIO) Read(p []byte) (n int, err error) {
var nr int64 var nr int64
+12
View File
@@ -147,6 +147,17 @@ type (
ContextReader ContextReader
ContextWriter 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 ( type (
@@ -172,6 +183,7 @@ type (
* [ChunkReadWriter] * [ChunkReadWriter]
* [ContextReadWriter] * [ContextReadWriter]
* [SizedCopyBufferer] * [SizedCopyBufferer]
* [LenReader]
Unlike [XIO], it must be non-nil (see [NewCtxIO]) since it maintains state 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 (though technically, one does not need to call [NewCtxIO] if they call
+3 -1
View File
@@ -1,3 +1,5 @@
- ScopedLogger, take an io.Writer for each log level
- logging probably needs mutexes - logging probably needs mutexes
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System". - macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System".
@@ -16,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. -- 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. -- 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 - make configurable via OR bitmask
- Suport remote loggers? (eventlog, syslog, journald) - Suport remote loggers? (eventlog, syslog, journald)
-1
View File
@@ -1,5 +1,4 @@
//go:build !(windows || plan9 || wasip1 || js || ios) //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. // I mean maybe it works for plan9 and ios, I don't know.
@@ -1,3 +1,5 @@
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
package logging package logging
var ( var (
-1
View File
@@ -1,5 +1,4 @@
//go:build !(windows || plan9 || wasip1 || js || ios || linux) //go:build !(windows || plan9 || wasip1 || js || ios || linux)
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
// Linux is excluded because it has its own. // Linux is excluded because it has its own.
+10
View File
@@ -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")
)
+12
View File
@@ -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")
)
+571
View File
@@ -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
}
+29
View File
@@ -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)
}
+4
View File
@@ -0,0 +1,4 @@
/*
Package netx includes extensions to the stdlib [net] module.
*/
package netx
-4
View File
@@ -1,4 +0,0 @@
/*
Package netx includes extensions to the stdlib `net` module.
*/
package netx
+296
View File
@@ -102,11 +102,37 @@ func Cidr4ToStr(cidr uint8) (maskStr string, err error) {
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]. GetAddrFamily returns the network family of a [net/netip.Addr].
See also [GetIpFamily]. 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). 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) { func GetAddrFamily(addr netip.Addr) (family uint16) {
@@ -131,6 +157,9 @@ func GetAddrFamily(addr netip.Addr) (family uint16) {
/* /*
GetIpFamily returns the network family of a [net.IP]. 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]. See also [GetAddrFamily].
If ip is not a "valid" IP address or the version can't be determined, If ip is not a "valid" IP address or the version can't be determined,
@@ -158,6 +187,8 @@ If ip is an IPv4 address, it will simmply be the string representation (e.g. "20
If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::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. 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) { func IpRfc(ip net.IP) (rfcStr string) {
@@ -170,6 +201,56 @@ func IpRfc(ip net.IP) (rfcStr string) {
return 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]. IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask].
@@ -257,6 +338,199 @@ func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) {
return 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. Mask4ToCidr converts an IPv4 netmask *in bitmask form* to a CIDR prefix size/bit size/bit length.
@@ -408,3 +682,25 @@ func Mask4StrToMask(maskStr string) (mask uint32, err error) {
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
}
+23
View File
@@ -7,6 +7,29 @@ import (
"testing" "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) { func TestFuncsIP(t *testing.T) {
var err error var err error
+1
View File
@@ -0,0 +1 @@
This is mostly just for generating some reference. It's not really intended for public consumption.
+20
View File
@@ -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
)
+45
View File
@@ -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=
+295
View File
@@ -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())
}
+147
View File
@@ -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)
}
}
+57
View File
@@ -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
}
----
+10
View File
@@ -2,4 +2,14 @@
-- draw border around multiline s -- draw border around multiline s
-- i have a version in python somewhere that does this, should dig that up -- 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? - create bytesx package that duplicates the functions here?
+18
View File
@@ -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
}
+265
View File
@@ -1,11 +1,212 @@
package stringsx package stringsx
import ( import (
`bytes`
`errors`
`fmt` `fmt`
`io`
`slices`
`strings` `strings`
`unicode` `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. LenSplit formats string `s` to break at, at most, every `width` characters.
@@ -252,6 +453,18 @@ func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted
return 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. TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic. It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
@@ -313,6 +526,58 @@ func TrimSpaceRight(s string) (trimmed string) {
return 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. // 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) { func getNewLine(s string) (nl string) {
+23
View File
@@ -37,6 +37,17 @@ type (
} }
) )
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) { func TestRedact(t *testing.T) {
var out string var out string
@@ -171,6 +182,18 @@ func TestRedact(t *testing.T) {
} }
} }
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) { func TestTrimLines(t *testing.T) {
var out string var out string
+25
View File
@@ -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
}
)
+1475 -515
View File
File diff suppressed because it is too large Load Diff
+2461 -453
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
- base64, base16, base32, hex, pem encoding/decoding tpl funcs
-- template hashing functions?
- os env vars
+5
View File
@@ -0,0 +1,5 @@
[source,subs="+attributes,+post_replacements",opts="novalidate"]
.Function Signature
----
func {func}{sig}
----
+121 -2
View File
@@ -1,11 +1,14 @@
package sprigx package sprigx
import ( import (
`net`
`net/netip`
`os` `os`
`os/user` `os/user`
`path` `path`
`path/filepath` `path/filepath`
`runtime` `runtime`
`time`
`github.com/davecgh/go-spew/spew` `github.com/davecgh/go-spew/spew`
`github.com/shirou/gopsutil/v4/cpu` `github.com/shirou/gopsutil/v4/cpu`
@@ -16,6 +19,11 @@ import (
psnet `github.com/shirou/gopsutil/v4/net` psnet `github.com/shirou/gopsutil/v4/net`
`github.com/shirou/gopsutil/v4/process` `github.com/shirou/gopsutil/v4/process`
`github.com/shirou/gopsutil/v4/sensors` `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` `r00t2.io/sysutils`
) )
@@ -28,6 +36,88 @@ var (
"Meta"/Template-Helpers "Meta"/Template-Helpers
*/ */
"metaIsNil": metaIsNil, "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 OS
*/ */
@@ -109,9 +199,24 @@ var (
// .../sensors // .../sensors
"psSensorTemps": sensors.SensorsTemperatures, "psSensorTemps": sensors.SensorsTemperatures,
/* /*
Strings Strings (Standalone)
*/ */
"extIndent": extIndent, // PR in: https://github.com/Masterminds/sprig/pull/468 "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 System/Platform
*/ */
@@ -119,7 +224,21 @@ var (
"sysNumCpu": runtime.NumCPU, "sysNumCpu": runtime.NumCPU,
"sysOsName": sysOsNm, "sysOsName": sysOsNm,
"sysRuntime": sysRuntime, "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 holds functions usable/intended for use in only an [html/template.FuncMap].
+77
View File
@@ -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>
+16
View File
@@ -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")
)
+275
View File
@@ -1,14 +1,116 @@
package sprigx package sprigx
import ( import (
`errors`
htpl "html/template" htpl "html/template"
`math`
`reflect`
`strconv`
ttpl "text/template" ttpl "text/template"
`github.com/Masterminds/sprig/v3`
) )
/* /*
Many of these functions are modeled after sprig's. 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. FuncMap returns a generic function map.
@@ -57,6 +159,11 @@ func HtmlFuncMap() (fmap htpl.FuncMap) {
return 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]. // TxtFuncMap returns a [text/template.FuncMap].
func TxtFuncMap() (fmap ttpl.FuncMap) { func TxtFuncMap() (fmap ttpl.FuncMap) {
@@ -79,3 +186,171 @@ func TxtFuncMap() (fmap ttpl.FuncMap) {
return 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
}
-5
View File
@@ -1,10 +1,5 @@
package sprigx package sprigx
// Nop explicitly performs a NO-OP and returns an empty string, allowing one to override "unsafe" functions.
func Nop(obj ...any) (s string) {
return
}
// metaIsNil returns true if obj is explicitly nil. // metaIsNil returns true if obj is explicitly nil.
func metaIsNil(obj any) (isNil bool) { func metaIsNil(obj any) (isNil bool) {
+82
View File
@@ -0,0 +1,82 @@
package sprigx
import (
`math`
`net`
`strconv`
)
// netExtractAddr calls net.ParseCIDR and returns the net.IP from it.
func netExtractAddr(s string) (addr net.IP, err error) {
if addr, _, err = net.ParseCIDR(s); err != nil {
return
}
return
}
// netExtractHost extracts the host component from hostPort.
func netExtractHost(hostPort string) (host string, err error) {
if host, _, err = net.SplitHostPort(hostPort); err != nil {
return
}
return
}
// netExtractIpnet calls net.ParseCIDR and returns the net.IPNet from it.
func netExtractIpnet(s string) (ipnet *net.IPNet, err error) {
if _, ipnet, err = net.ParseCIDR(s); err != nil {
return
}
return
}
// netExtractPort extracts the port component from hostPort.
func netExtractPort(hostPort string) (port uint16, err error) {
var portStr string
var u64 uint64
if _, portStr, err = net.SplitHostPort(hostPort); err != nil {
return
}
if u64, err = strconv.ParseUint(portStr, 10, 16); err != nil {
return
}
port = uint16(u64)
return
}
// netIp4Mask is a more flexible wrapper around net.IPv4Mask.
func netIp4Mask(a, b, c, d any) (mask net.IPMask, err error) {
var idx int
var elem any
var elemInt int
var mBytes [4]byte
var orig [4]any = [4]any{a, b, c, d}
for idx, elem = range orig {
if elemInt, _, err = toPosInt(elem); err != nil {
return
}
if elemInt > math.MaxUint8 {
err = ErrOverflow
return
}
mBytes[idx] = byte(uint8(elemInt))
}
mask = net.IPv4Mask(
mBytes[0], mBytes[1], mBytes[2], mBytes[3],
)
return
}
+47
View File
@@ -0,0 +1,47 @@
package sprigx
import (
`net`
`net/netip`
`go4.org/netipx`
)
// netipxFromStdAddr wraps go4.org/netipx.FromStdAddr to comply with Go template requirements.
func netipxFromStdAddr(ip net.IP, port int, zone string) (addrPort netip.AddrPort, err error) {
var ok bool
if addrPort, ok = netipx.FromStdAddr(ip, port, zone); !ok {
err = ErrBadAddrPort
return
}
return
}
// netipxFromIp wraps go4.org/netipx.FromStdIP to comply with Go template requirements.
func netipxFromIp(ip net.IP) (addr netip.Addr, err error) {
var ok bool
if addr, ok = netipx.FromStdIP(ip); !ok {
err = ErrBadAddr
return
}
return
}
// netipxFromIpNet wraps go4.org/netipx.FromStdIPNet to comply with Go template requirements.
func netipxFromIpNet(ipnet *net.IPNet) (pfx netip.Prefix, err error) {
var ok bool
if pfx, ok = netipx.FromStdIPNet(ipnet); !ok {
err = ErrBadNet
return
}
return
}
+51
View File
@@ -0,0 +1,51 @@
package sprigx
import (
`math/big`
)
// numFloat64 returns any string representation of a numeric value or any type of numeric value to a float64.
func numFloat64(val any) (f float64, err error) {
if f, _, err = toFloat64(val); err != nil {
return
}
return
}
/*
numFloatStr wraps numFloat32Str and numFloat64Str.
val can be a string representation of any numeric value or any type of numeric value.
*/
func numFloatStr(val any) (s string, err error) {
var f float64
if f, _, err = toFloat64(val); err != nil {
return
}
s = numFloat64Str(f)
return
}
// numFloat32Str returns float32 f as a complete string representation with no truncation (or right-padding).
func numFloat32Str(f float32) (s string) {
s = numFloat64Str(float64(f))
return
}
// numFloat64Str returns float64 f as a complete string representation with no truncation (or right-padding).
func numFloat64Str(f float64) (s string) {
var bf *big.Float
bf = big.NewFloat(f)
s = bf.Text('f', -1)
return
}
+40 -14
View File
@@ -7,16 +7,29 @@ import (
`strings` `strings`
) )
// osGroupById returns os/user.LookupGroupId. Can accept either an integer or a string. /*
func osGroupById[T string | int](gid T) (g *user.Group, err error) { osGroupById returns os/user.LookupGroupId.
Can accept either a string (`"1000"`) or any
numeric type (`1000`, `-1000`, `1000.0`, `MyCustomType(1000)`, etc.)
*/
func osGroupById(gid any) (g *user.Group, err error) {
var i int
var NaN bool
var gidStr string var gidStr string
switch t := any(gid).(type) { if i, NaN, err = toPosInt(gid); err != nil {
case string: if NaN {
gidStr = t err = nil
case int: if gidStr, err = toString(gid); err != nil {
gidStr = strconv.Itoa(t) return
}
} else {
return
}
} else {
gidStr = strconv.Itoa(i)
} }
g, err = user.LookupGroupId(gidStr) g, err = user.LookupGroupId(gidStr)
@@ -55,16 +68,29 @@ func osHost() (hostNm string, err error) {
return return
} }
// osUserById returns an os/user.LookupId. Can accept either an integer or a string. /*
func osUserById[T string | int](uid T) (u *user.User, err error) { osUserById returns an os/user.LookupId.
Can accept either a string (`"1000"`) or any
numeric type (`1000`, `-1000`, `1000.0`, `MyCustomType(1000)`, etc.)
*/
func osUserById(uid any) (u *user.User, err error) {
var i int
var NaN bool
var uidStr string var uidStr string
switch t := any(uid).(type) { if i, NaN, err = toPosInt(uid); err != nil {
case string: if NaN {
uidStr = t err = nil
case int: if uidStr, err = toString(uid); err != nil {
uidStr = strconv.Itoa(t) return
}
} else {
return
}
} else {
uidStr = strconv.Itoa(i)
} }
u, err = user.LookupId(uidStr) u, err = user.LookupId(uidStr)
+139
View File
@@ -0,0 +1,139 @@
package sprigx
import (
`errors`
`strconv`
`strings`
`time`
)
/*
tmFmt formats time t using format string fstr.
While one certainly can do the same via e.g.
{{- $t := tmNow -}}
{{ $t.Format $fstr }}
This takes a time.Time as the second (and last) parameter,
allowing it to work in pipelines.
*/
func tmFmt(fstr string, t time.Time) (out string) {
out = t.Format(fstr)
return
}
/*
tmParseMonth attempts to first try tmParseMonthInt
and then tries tmParseMonthStr if v is not "numeric".
*/
func tmParseMonth(v any) (mon time.Month, err error) {
var s string
if mon, err = tmParseMonthInt(v); err != nil {
if errors.Is(err, strconv.ErrSyntax) {
// NaN
err = nil
} else {
return
}
}
// If it gets here, it's a non-numeric string.
if s, err = toString(v); err != nil {
return
}
if mon, err = tmParseMonthStr(s); err != nil {
return
}
return
}
/*
tmParseMonthInt parses a number representation of month n to a time.Month.
n may be any numeric type or a string representation of a number
(or a custom type derived from those).
A negative integer (or float, etc.) will be converted to a positive one (e.g. -6 => 6 => time.June).
floats are rounded to the nearest integer.
The integer should map directly to the month constants in the time module:
* 1: January
* 2: February
* 3: March
* 4: April
* 5: May
* 6: June
* 7: July
* 8: August
* 9: September
* 10: October
* 11: November
* 12: December
If n resolves to 0, mon will be the current month (as determined by time.Now).
If n resolves to > 12, err will be ErrBadMonth.
*/
func tmParseMonthInt(n any) (mon time.Month, err error) {
var i int
if i, _, err = toPosInt(n); err != nil {
return
}
if i == 0 {
mon = time.Now().Month()
return
}
if i > 12 {
err = ErrBadMonth
return
}
mon = time.Month(i)
return
}
/*
tmParseMonthStr parses a string representation of month s to a time.Month.
It normalizes s to lowercase and only uses the first 3 characters
(the minimum length needed to determine month name
uniqueness - "June" vs. "July", "March" vs. "May").
An empty (or whitespace-only) string will use the current month (as determined by time.Now).
*/
func tmParseMonthStr(s string) (mon time.Month, err error) {
var i int
var m time.Month
if strings.TrimSpace(s) == "" {
mon = time.Now().Month()
return
}
s = strings.ToLower(strings.TrimSpace(s))[0:3]
for i = range 12 {
m = time.Month(i + 1)
if strings.ToLower(m.String())[0:3] == s {
mon = m
return
}
}
err = ErrBadMonth
return
}
+11
View File
@@ -0,0 +1,11 @@
package uuidx
const (
RfcNone RfcGen = iota
Rfc4122
Rfc9562
)
const (
MsGuidThreshold int = 4
)
+73
View File
@@ -0,0 +1,73 @@
/*
Package uuidx intends to supplement [github.com/google/uuid].
# Microsoft GUID Shenanigans
The following functions are provided to deal with [Microsoft's incompetence]:
* [DetectMsGuid] (a confidence'd determination if a UUID is a Microsoft GUID or not)
* [IsFlippedEndian] for flipped-endian [uuid.UUID] comparison (e.g. a is the Microsoft-flipped-endian version of b)
* [IsMsGuid] (wraps [DetectMsGuid] and returns true if confidence is reasonably strong that it's a Microsoft GUID)
* [IsRfc] (the inverse of IsMsGuid, but also checks for strict RFC compliance and returns which RFC)
* [MsGuidToUuid] (explicitly convert/ensure a GUID/UUID is likely a UUID)
* [ToggleUuidMsGuid] (blindly flip the endianness of selected byte ranges for MS GUID <-> UUID conversion)
* [UuidToMsGuid] (explicitly convert/ensure a GUID/UUID is likely an MS GUID)
Microsoft, in their typical insanity, uses a proprietary UUID format (usually referred to as the "Microsoft GUID Format"
or "Mixed-Endian Format").
Normally for, for example a UUIDv4, it's structured as thus per RFC 9562 [§ 5.4] (which obsoletes RFC 4122 [§ 4.4]):
A B C D E
HEX(BE(uint32))-HEX(BE(uint16))-HEX(BE(uint16))-HEX(BE(<uint16>), BE(<6 bytes>))
(where <BE> is big-endian packing).
However, thanks to Microsoft we can't have nice things. They decided to completely ignore the standard, and
instead keep D/E as big-endian *but use little-endian* for A through C inclusive:
A B C D E
HEX(LE(uint32))-HEX(LE(uint16))-HEX(LE(uint16))-HEX(BE(<uint16>), BE(<6 bytes>))
"Surely that had SOME reason to do that," you may say to yourself, "they wouldn't make some arbitrary formatting
change from a standard just because."
You would be wrong. To my knowledge, they have never provided any technological justfification to this insanity,
and now it's infected its way into a slew of other technologies they've had their grubby little hands involved in
(e.g. UEFI). And it's of course too late to change.
So anyways here's a library to make dealing with Microsoft's hubris a little easier.
# Validation/Verification
Aside from trying to address Microsoft silliness, there are some additional functions:
* [Equal] for [uuid.UUID] comparison
* [IsMaxUUID] (if a given [uuid.UUID] is an RFC 9562 [§ 5.10] UUID)
* [IsNilUUID] (if a given [uuid.UUID] is an RFC 9562 [§ 5.9] UUID)
* [IsValid] (If an RFC can be considered safely conformant to RFC spec)
# Future Incorporation/Deprecation/Obsolescence
Worth keeping an eye on are:
* https://github.com/google/uuid/pull/192
* https://github.com/golang/go/issues/62026
* https://github.com/golang/go/issues/76319
(generally it's a bad idea for an API addition overall, but some good ideas were raised)
Some of these additions may deprecate/obsolete components of this package.
I'll try to keep them around but mark as deprecated as they are (if they are),
but I make no concrete promises - I hate making new major releases in Go's
[silly module architecture] even more than I do keeping old deprecated code around.
So caveat emptor.
[Microsoft's incompetence]: https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid
[§ 5.4]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.4
[§ 4.4]: https://datatracker.ietf.org/doc/html/rfc4122#section-4.4
[§ 5.9]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.9
[§ 5.10]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.10
[github:google/uuid#192]: https://github.com/google/uuid/pull/192
[silly module architecture]: https://go.dev/doc/modules/major-version
*/
package uuidx
+481
View File
@@ -0,0 +1,481 @@
package uuidx
import (
"github.com/google/uuid"
)
/*
DetectMsGuid tries to guess if a given [uuid.UUID] is actually a Microsoft GUID or not.
Note that there are technically *two* types of Microsoft GUIDs:
* One is predictable, and defined in RFC 9562 [§ 4.2] as a known variant.
Detecting this is very easy and (assuming an RFC-compliant UUID is originally passed) is detectable with 100% confidence.
It's also legacy, and Microsoft no longer uses this format. Because they are insane and enjoy the suffering of others.
* The other, MODERN Microsoft GUID currently in use is the endianness-flipped version (see [ToggleUuidMsGuid]).
This is impossible to 100% determine, but analysis can get *pretty* close.
cs is a confidence scoring. As more logic is added, it *is* mathematically possible
(though unlikely) that cs == 0, so the caller is then responsible for making further
guesswork based on contextual analysis ("Did I get this UUID/GUID from an Active Directory attribute?"
"Is it a SID constant?" etc.).
A score > 0 indicates a confidence leaning towards the provided UUID/GUID being a Microsoft GUID.
A score < 0 indicates a confidence leaning towards the provided UUID/GUID *not* being a Microsoft GUID.
Note that a score of < 0 does not necessarily indicate it is a *proper, standard RFC-compliant UUID*,
simply that it is likely NOT a Microsoft GUID. [IsRfc] will be of further help in these cases.
csFlip indicates a score for the [ToggleUuidMsGuid]-flipped version of u.
It follows the same rules for thresholds and such as cs, but may be awarded different confidence levels
internally due to different chances of false positives.
If both cs and csFlip are > 0 but csFlip > cs, it is better to assume that u is *not* in the flipped-endian format
but *is* a Microsoft GUID (in other words, it is likely that u has *already been flipped* to proper/consistent endianness
instead of being a mixed-endian GUID).
In some cases where flipped-endianness does not matter (e.g. [IsNilUUID], [IsMaxUUID]),
cs and csFlip will be equal.
*Randomly-generated* GUIDs on Windows Server 2000-family and up are almost always UUIDv4.
Pre-Windows Server 2000 family *OR* any *statically-defined* GUIDs (schemaIDGUID, rightsGUID, CLSID constants, etc.)
are all over the place - TYPICALLY UUIDv1, but it's nothing predictable enough to be useful in definitive classification.
COM interfaces are all OVER the place in UUID version, but usually *not* UUIDv4.
A target/expected UUID version can be provided via tgtVer. To disable version analysis, use 0 (or 0x00, etc.).
It is *highly* recommended to provide a tgtVer if it is known; it can significantly boost confidence in the correct direction.
A warning, though - if a tgtVer IS specified but is wrong (e.g. a UUIDv1 was passed in but tgtVer was specified as 4 for UUIDv4),
it can *negatively* affect confidence accuracy.
Thus if you aren't ABSOLUTELY certain of the target UUID version, it's better to use 0/0x00 to disable the check.
Providing a target version can be key to breaking some "ties" (e.g. both cs and csFlip are equal).
For example, the given RFC-compliant UUIDv4:
8d8e35ae-58d2-4d28-b09d-ffffffffffff
when flipped evaluates to an RFC-compliant UUIDv2:
ae358e8d-d258-284d-b09d-ffffffffffff
and in this case, cs and csFlip will both end up as 0.
Providing a tgtVer of 4 shifts this to a proper "tie-breaker" of cs == -3 and csFlip == 0.
Similarly, the endian-flipped UUIDv4 evaluates as a UUIDv2:
9856ea36-c2ca-2347-af0c-3b42f76c9eca
from the original unflipped UUIDv4:
36ea5698-cac2-4723-af0c-3b42f76c9eca
which results in a cs == 1 and csFlip == 0 - not very high confidence (but at least a correct and non-zero lean).
Providing a tgtVer == 4 changes this to cs == 7 and csFlip == 0, which is *much* more decisive.
UUIDs/GUIDs found to be strictly RFC-conforming (via [IsRfc], which returns false for Microsoft GUIDs)
are *heavily* weighted negatively in their respective scoring forms.
Confidence levels can be generally considered as the following:
* cs >= 7: Likely Microsoft GUID (mixed-endian)
* cs >= 4: Likely Microsoft GUID
* 0 < cs < 4: Leans Microsoft GUID, but untrusted/ambiguous
* cs == 0: Entirely and completely ambiguous/indeterminate
* -4 < cs < 0: Leans UUID/non-Microsoft GUID but untrusted/ambiguous
* cs <= -5: Likely UUID/not Microsoft GUID
* csFlip >= cs && csFlip >= 4: u is likely a pre-flipped (ToggleUuidMsGuid'd) Microsoft GUID
(i.e. u is likely directly from a Microsoft API/ data source
and no normalization has occurred yet)
[§ 4.2]: https://datatracker.ietf.org/doc/html/rfc9562#section-4.2
*/
func DetectMsGuid(u uuid.UUID, tgtVer uuid.Version) (cs, csFlip int) {
var isRfc bool
var flippedRfc bool
var flipped uuid.UUID = ToggleUuidMsGuid(u)
// These are the exact same when flipped, and are statically defined.
if IsNilUUID(u) || IsMaxUUID(u) {
cs = -12
csFlip = -12
return
}
// Most/all(?) Microsoft GUIDs are not NCS.
if IsNcs(u) {
cs -= 2
}
if IsNcs(flipped) {
// The flipped has a higher likelihood of false-pos, so we don't score it as confidently.
csFlip -= 1
}
if u.Version() == 0 {
if u.Variant() == uuid.Microsoft {
cs += 10
} else {
cs -= 2
}
}
if flipped.Version() == 0 {
if flipped.Variant() == uuid.Microsoft {
csFlip += 4
} else {
csFlip -= 1
}
}
// Valid RFC version and variant. IsRfc returns false for the Microsoft Variant and version == 0.
// Modern MS uses an RFC 4122 variant indicator but flips the endianness.
isRfc, _ = IsRfc(u)
flippedRfc, _ = IsRfc(flipped)
if u.Variant() == uuid.RFC4122 { // This might be the strongest indicator.
if isRfc && !flippedRfc {
// This is *very* strong of being an MS GUID.
cs -= 8
csFlip += 4
} else if !isRfc && flippedRfc {
// It probably is an MS GUID but was already flipped.
csFlip += 6
} else if isRfc && flippedRfc {
/*
If both are RFC-compat, it's a weird case where
it actually IS RFC compliant and by chance the flipped is *also* RFC compat.
An example of this is:
8d8e35ae-58d2-4d28-b09d-ffffffffffff
Which has the flipped version of:
ae358e8d-d258-284d-b09d-ffffffffffff
The original is a v4, the flipped evaluates as a v2!
Providing a target version breaks this away to a definitive score.
*/
}
}
// *HEAVILY* weigh a provided version.
if tgtVer != 0 {
// NCS does some weird things to the versioning field. We return early on it though.
// MS GUIDs have a pretty small chance of matching,
// but their flipped counterpart SHOULD match versions.
if flipped.Version() == tgtVer {
cs += 7
} else {
cs -= 3
}
} else {
// Give a *very small* boost to flippedRfc and flipped.Version() == 4, since it's so common.
// Don't make this too high though since the version is explicitly specified as unknown.
if flippedRfc && flipped.Version() == 4 {
cs += 1
}
}
return
}
/*
Equal returns `true` if the two provided [uuid.UUID] are the same.
Currently it just wraps:
eq = a == b
but is provided as a safety guarantee if the underlying structures/types should change.
*/
func Equal(a, b uuid.UUID) (eq bool) {
eq = a == b
return
}
/*
IsFlippedEndian can be used to check if [uuid.UUID] is a direct endian-flipped ([ToggleUuidMsGuid])
of b (or vice versa, obviously).
It simply wraps:
isFlipped = Equal(a, ToggleUuidMsGuid(b))
but can be useful for shorthand/readability.
*/
func IsFlippedEndian(a, b uuid.UUID) (isFlipped bool) {
isFlipped = Equal(a, ToggleUuidMsGuid(b))
return
}
/*
IsMaxUUID returns `true` if the specified UUID is explicitly an RFC-defined
"Max UUID". (You may also see it specified in some places as the "Omni UUID".)
For details, see RFC 9562 [§ 5.10].
[§ 5.10]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.10
*/
func IsMaxUUID(u uuid.UUID) (isMax bool) {
isMax = u == uuid.Max
return
}
/*
IsMsGuid wraps
if cmp, _ = DetectMsGuid(msGUID, tgtVer); cmp < -3 {
isMs = true
}
Note that [uuid.Microsoft] is an actual RFC-defined variant, but *Microsoft no longer uses it*
and in MODERN implementations do the endianness flip [ToggleUuidMsGuid] of (USUALLY) a UUIDv4.
See [DetectMsGuid] for a more in-depth result that will let you use the confidence level directly,
and for details on the weird things that can go wrong with this guesswork.
Note that this won't be 100% reliable due to math things, but it should be ...somewhat reliable
enough most of the time. Maybe. But you are *strongly* recommended to use [DetectMsGuid] instead
wherever possible.
See also [MsGuidToUuid] and [UuidToMsGuid].
*/
func IsMsGuid(msGUID uuid.UUID, tgtVer uuid.Version) (isMs bool) {
var cmp int
if cmp, _ = DetectMsGuid(msGUID, tgtVer); cmp < -3 {
isMs = true
}
return
}
/*
IsNcs is shorthand for:
isNcs = u.Variant() == uuid.Reserved
See also the notes in [IsRfc].
*/
func IsNcs(u uuid.UUID) (isNcs bool) {
// https://archive.org/details/networkcomputing0000zahn/page/10/mode/1up
isNcs = u.Variant() == uuid.Reserved
return
}
/*
IsNilUUID returns `true` if the specified UUID is explicitly an RFC-defined
"Nil UUID".
For details, see RFC 9562 [§ 5.9].
[§ 5.9]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.9
*/
func IsNilUUID(u uuid.UUID) (isNil bool) {
isNil = u == uuid.Nil
return
}
/*
IsRfc returns `true` if the specified UUID is a proper standard RFC UUID.
Because Microsoft is insane, rfc will be false even if it's a (legacy) Microsoft form
of an RFC UUID. Use [IsMsGuid] for that.
In the special case of u being a valid NCS UUID, rfc will be false but gen will be [Rfc4122].
This is because RFC 9652 deprecates the NCS UUID. See [IsNcs].
(You are highly unlikely to encounter an NCS UUID "in the wild" unless you are working
with VERY old systems/data or are receiving a UUID from someone who severely
misunderstands that UUIDs are structured/versioned/typed and thinks they're
just random byes in hex with hyphens in certain places.)
(They aren't that, if you're one of those someones.)
Nil UUID ([IsNilUUID]) and Max UUID ([IsMaxUUID]) return true with RFCs 4122 and RFC 9562 respectively.
*/
func IsRfc(u uuid.UUID) (rfc bool, gen RfcGen) {
if IsNilUUID(u) {
rfc = true
gen = Rfc4122
return
}
if IsMaxUUID(u) {
rfc = true
gen = Rfc9562
return
}
if IsNcs(u) {
gen = Rfc4122
return
}
// TODO: Are there any sub-version checks that can be applied?
switch u.Variant() {
case uuid.Invalid, uuid.Microsoft, uuid.Future:
return
case uuid.RFC4122:
if !(0x01 <= u.Version() && u.Version() <= 0x08) {
return
}
rfc = true
gen = Rfc4122
// 4122 only covers UUIDv1 through UUIDv5.
if 0x06 <= u.Version() && u.Version() <= 0x08 {
gen = Rfc9562
}
default: // Safety net in case upstream adds a uuid.RFC9562 variant or something.
if !(0x01 <= u.Version() && u.Version() <= 0x08) {
return
}
if u.Variant() < uuid.Future {
return
}
rfc = true
gen = RfcNone
// 4122 only covers UUIDv1 through UUIDv5.
if 0x06 <= u.Version() && u.Version() <= 0x08 {
gen = Rfc9562
}
}
return
}
/*
IsValid indicates if the given [uuid.UUID] strictly conforms to RFC.
A Nil UUID (as in RFC 9562 [§ 5.9], not a `nil` *uuid.UUID) will return `true`
as it IS technically defined per RFC despite not conforming to a version.
Use [IsNilUUID] to further determine that.
Likewise, a Max UUID (RFC 9562 [§ 5.10]) will return `true` as it is also
defined per RFC despite not conforming to a version.
Use [IsMaxUUID] to further determine that.
Microsoft GUIDs will always return false since they defy RFC.
Use [IsMsGuid] to check for that condition.
[§ 5.9]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.9
[§ 5.10]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.10
*/
func IsValid(u uuid.UUID) (valid bool) {
if IsNilUUID(u) {
valid = true
return
}
if IsMaxUUID(u) {
valid = true
return
}
switch u.Variant() {
case uuid.Invalid, uuid.Reserved, uuid.Microsoft, uuid.Future:
return
case uuid.RFC4122:
valid = true
// TODO: If they add an RFC9562 or something, need a case here.
default:
return
}
// If we got here, it *should* be RFC.
if valid, _ = IsRfc(u); !valid {
return
}
return
}
/*
MsGuidToUuid converts a Microsoft GUID to a UUID.
If [IsMsGuid] is false for msGUID, u will be equal to msGUID.
See [UuidToMsGuid] for the inverse, and [IsRfc] to check
if the result is a strictly conforming UUID.
Use [ToggleUuidMsGuid] if you want msGUID explicitly flipped
with no gating from [IsMsGuid].
*/
func MsGuidToUuid(msGUID uuid.UUID) (u uuid.UUID) {
if !IsMsGuid(msGUID, 0x00) {
u = msGUID
return
}
u = ToggleUuidMsGuid(msGUID)
return
}
/*
ToggleUuidMsGuid switches the src to it's "other" format:
* if it's a Microsoft GUID, it will be converted to a UUID
* if it's a UUID, it will be converted to a Microsoft GUID
No detection ([IsRfc], [IsMsGuid], etc.) nor validation/verification ([IsValid]) is performed,
which is why this is a "toggle" - it just flips some endianness for certain byte ranges.
If you prefer something a little more explicit, see [MsGuidToUuid] and/or [UuidToMsGuid].
Alternatively call [IsMsGuid] or [IsRfc] directly.
*/
func ToggleUuidMsGuid(orig uuid.UUID) (converted uuid.UUID) {
var cb [16]byte
var ob [16]byte = orig
// Can just directly map the allocations;
// the operation is the exact same regardless of whether the original is RFC and target is MS or vice versa.
cb = [16]byte{
// THESE GET ENDIAN-SWAPPED
ob[3], ob[2], ob[1], ob[0], // "A"
ob[5], ob[4], // "B"
ob[7], ob[6], // "C"
// THESE STAY THE SAME (should be BE for both)
ob[8], ob[9], ob[10], ob[11], // "D"
ob[12], ob[13], ob[14], ob[15], // "E"
}
converted = uuid.UUID(cb)
return
}
/*
UuidToMsGuid converts a UUID to a Microsoft GUID.
If [DetectMsGuid] indicates a good likelihood for u already being a Microsoft GUID
(cs being greater than or equal to [MsGuidThreshold]), msGUID will be equal to u.
If [DetectMsGuid] detects it as a Microsoft unflipped endianness (i.e. csFlip > cs),
msGUID will be the [ToggleUuidMsGuid]-flipped format of u UNLESS u.Variant == [uuid.Microsoft].
Thus this function *should* (with plenty of caveats mentioned elsewhere in other functions/documentation)
be usable for flipping a purely non-Microsoft-tainted RFC-conforming UUID, and not flipping it otherwise
as it's already a "Microsoft GUID" of some generation (either legacy or newer).
See [MsGuidToUuid] for the inverse.
Use [ToggleUuidMsGuid] if you want u explicitly flipped with no gating from [DetectMsGuid] or
via u's [uuid.Variant].
*/
func UuidToMsGuid(u uuid.UUID) (msGUID uuid.UUID) {
var msCmp int
var flipped int
if msCmp, flipped = DetectMsGuid(u, 0x00); msCmp >= MsGuidThreshold && msCmp > flipped {
msGUID = u
return
}
if u.Variant == uuid.Microsoft {
msGUID = u
return
}
msGUID = ToggleUuidMsGuid(u)
return
}
+22
View File
@@ -0,0 +1,22 @@
package uuidx
// String conforms an [RfcGen] to a [fmt.Stringer] interface.
func (g *RfcGen) String() (s string) {
if g == nil {
s = "UNSPECIFIED_NIL"
}
switch *g {
case RfcNone:
s = "INVALID"
case Rfc4122:
s = "RFC 4122"
case Rfc9562:
s = "RFC 9562"
default:
s = "UNKNOWN"
}
return
}
+5
View File
@@ -0,0 +1,5 @@
package uuidx
type (
RfcGen uint8
)