Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
58556d7281
|
|||
|
c6fc692f5e
|
|||
|
4770052b52
|
|||
|
1eea0c2672
|
|||
|
67c7faf449
|
|||
|
82c69ec542
|
@@ -15,18 +15,61 @@ for f in $(find . -type f -iname "README.adoc"); do
|
||||
nosuffix="${filename%.*}"
|
||||
pfx="${docsdir}/${nosuffix}"
|
||||
|
||||
# Render HTML, include in commit
|
||||
newf="${pfx}.html"
|
||||
asciidoctor -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
|
||||
echo "Generated ${newf} from ${f}"
|
||||
git add "${newf}"
|
||||
|
||||
# If asciidoctor-pdf is installed, render as PDF for local use
|
||||
# (Does not get added to commit, and *.pdf is in .gitignore for a reason)
|
||||
if command -v asciidoctor-pdf &> /dev/null;
|
||||
then
|
||||
newf="${pfx}.pdf"
|
||||
|
||||
asciidoctor-pdf -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
|
||||
fi
|
||||
|
||||
# If pandoc is installed, render to "GitHub-flavored Markdown" for better rendering on forks/mirrors
|
||||
# and marginally better rendering on https://pkg.go.dev/ and add to commit.
|
||||
#
|
||||
# <rant>
|
||||
# There is no such thing as "Markdown".
|
||||
# The closest thing you have to any sort of standard is https://daringfireball.net/projects/markdown/
|
||||
# but everybody and their mother adds their own "extensions"/"flavor", and sometimes even
|
||||
# change how formatting works compared to the Daring Fireball/John Gruber spec (the original creator of the "syntax").
|
||||
# Ergo "Markdown" inherently has no meaning.
|
||||
# It's one of the worst formatting languages out there - just because it's popular doesn't mean it's good.
|
||||
#
|
||||
# If you're writing docs, you should stick to one of these which have defined, canonical, standardized
|
||||
# syntax:
|
||||
# * AsciiDoc/AsciiDoctor
|
||||
# * Supports much more extensive formatting than any Markdown flavor I've seen
|
||||
# * Source/raw/unrendered still *quite* readable by human eyes
|
||||
# * Somewhat limited parsers/renderers
|
||||
# * https://asciidoc.org/
|
||||
# * https://asciidoctor.org/
|
||||
# * DocBook
|
||||
# * Supports even more extensive and flexible but exact formatting
|
||||
# * Great for publishing, though - especially if you need control over formatting/layout
|
||||
# * XML-based
|
||||
# * Harder to read in plaintext, but fairly doable (XML lends to decent mental rendering)
|
||||
# * Very wide support for parsing/rendering
|
||||
# * https://docbook.org/
|
||||
# * LaTex
|
||||
# * Allows for *very* extensive domain-specific ligature/representation (very common in mathematic/scientific literature)
|
||||
# * But nigh unreadable by human eyes unless you've rather familiar with it
|
||||
# * Parsing/rendering support about on-par with DocBook
|
||||
# * https://www.latex-project.org/
|
||||
# </rant>
|
||||
if command -v pandoc &> /dev/null;
|
||||
then
|
||||
newf="${pfx}.md"
|
||||
|
||||
set +e
|
||||
#asciidoctor -a ROOTDIR="${orig}/" -b docbook -o - "${f}" | pandoc -f docbook -t markdown_strict -o "${newf}"
|
||||
#asciidoctor -a ROOTDIR="${orig}/" -b html -o - "${f}" | pandoc -f html -t markdown_strict -o "${newf}"
|
||||
asciidoctor -a ROOTDIR="${orig}/" -b html -o - "${f}" | pandoc -f html -t gfm -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 ];
|
||||
then
|
||||
echo "Generated ${newf} from ${f}"
|
||||
|
||||
+2
-1
@@ -19,12 +19,13 @@
|
||||
.idea/
|
||||
|
||||
# https://github.com/github/gitignore/blob/master/Go.gitignore
|
||||
# Binaries for programs and plugins
|
||||
# Binaries for programs and plugins and other data
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.pdf
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
- validx: validator functions for https://pkg.go.dev/github.com/go-playground/validator/v10
|
||||
|
||||
- hashx?
|
||||
+16
-25
@@ -37,24 +37,8 @@ func NewMaskBitExplicit(value uint) (m *MaskBit) {
|
||||
/*
|
||||
HasFlag is true if m has MaskBit flag set/enabled.
|
||||
|
||||
THIS WILL RETURN FALSE FOR OR'd FLAGS.
|
||||
|
||||
For example:
|
||||
|
||||
flagA MaskBit = 0x01
|
||||
flagB MaskBit = 0x02
|
||||
flagComposite = flagA | flagB
|
||||
|
||||
m *MaskBit = NewMaskBitExplicit(uint(flagA))
|
||||
|
||||
m.HasFlag(flagComposite) will return false even though flagComposite is an OR
|
||||
that contains flagA.
|
||||
Use [MaskBit.IsOneOf] instead if you do not desire this behavior,
|
||||
and instead want to test composite flag *membership*.
|
||||
(MaskBit.IsOneOf will also return true for non-composite equality.)
|
||||
|
||||
To be more clear, if MaskBit flag is a composite MaskBit (e.g. flagComposite above),
|
||||
HasFlag will only return true of ALL bits in flag are also set in MaskBit m.
|
||||
See the "Composite (OR'd) Flags" section in this module's
|
||||
documentation for important caveats.
|
||||
*/
|
||||
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.
|
||||
|
||||
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.
|
||||
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,
|
||||
IsOneOf will return true if ANY of the flags set in m is set in composite.
|
||||
If m is a composite [MaskBit] (it usually is) and composite is ALSO a composite MaskBit,
|
||||
HasOneOf will return true if ANY of the flags set in composite is set in m.
|
||||
*/
|
||||
func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) {
|
||||
func (m *MaskBit) HasOneOf(composite MaskBit) (r bool) {
|
||||
|
||||
var b MaskBit = *m
|
||||
|
||||
@@ -89,6 +74,13 @@ func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsOneOf is the old name for [MaskBit.HasOneOf].
|
||||
|
||||
DEPRECATED: This method will be removed sometime in the future. Use [MaskBit.HasOneOf] instead.
|
||||
*/
|
||||
func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) { return m.HasOneOf(composite) }
|
||||
|
||||
// AddFlag adds MaskBit flag to m.
|
||||
func (m *MaskBit) AddFlag(flag MaskBit) {
|
||||
|
||||
@@ -122,7 +114,6 @@ func (m *MaskBit) ToggleFlag(flag MaskBit) {
|
||||
|
||||
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) {
|
||||
|
||||
|
||||
+117
-44
@@ -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!"
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
or 18446744073709551615 bytes (16 EiB - yes, that's [exbibytes]) of RAM for 32-bit/64-bit platforms respectively.
|
||||
One instance of OptStruct takes up *8 bytes* (*64 bits*).
|
||||
|
||||
As a uint8 bitmask, however, it takes up exactly *one byte/8 bits*:
|
||||
|
||||
type (
|
||||
Opt uint8
|
||||
)
|
||||
|
||||
const OptNone Opt = 0
|
||||
const (
|
||||
A Opt = 1 << iota // 1
|
||||
B // 2
|
||||
C // 4
|
||||
D // 8
|
||||
E // 16
|
||||
F // 32
|
||||
G // 64
|
||||
H // 128
|
||||
// Would overflow to 0:
|
||||
// I
|
||||
)
|
||||
|
||||
As shown, the size of an unsigned integer determines the number of bit-boundaried flags for that bitmask.
|
||||
|
||||
Which means on 32-bit platforms, a [MaskBit] can have up to 32 different flags but only occupies 4 bytes of memory.
|
||||
|
||||
On 64-bit platforms, a [MaskBit] can have up to 64 different flags but only occupies 8 bytes of memory.
|
||||
|
||||
If you tried to do that with a struct of booleans instead, that'd occupy 32 *bytes* and 64 *bytes* respectively.
|
||||
|
||||
In summary, a bitmask set occupies 1/8th the amount of memory as a set of equal number of booleans in Go.
|
||||
|
||||
You can, of course, extend this even further by defining meaningful "composites",
|
||||
or OR'd-combination of flags (see the "Composite Flags" section below):
|
||||
|
||||
type Perms uint8
|
||||
const NoPerms Perms = 0
|
||||
const (
|
||||
PermsRead Perms = 1 << iota
|
||||
PermsList
|
||||
PermsUpdate
|
||||
PermsCreate
|
||||
PermsDelete
|
||||
PermsUpdateBulk
|
||||
PermsCreateBulk
|
||||
PermsDeleteBulk
|
||||
|
||||
// This makes it easy to add multiple permissions at once.
|
||||
PermsCRUD Perms = PermsCreate | PermsRead | PermsUpdate | PermsDelete
|
||||
PermsCRUDL Perms = PermsCRUD | PermsList
|
||||
// And so forth.
|
||||
)
|
||||
|
||||
"But that has to be so slow to unpack that!"
|
||||
|
||||
Nope. It's not using compression or anything, the CPU is just comparing bit "A" vs. bit "B" 32/64 times. That's super easy work for a CPU.
|
||||
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
|
||||
|
||||
@@ -34,6 +97,8 @@ To use this library, set constants like thus:
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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() {
|
||||
MyMask = bitmask.NewMaskBit()
|
||||
|
||||
MyMask.AddFlag(OPT1)
|
||||
MyMask.AddFlag(OPT3)
|
||||
|
||||
_ = MyMask
|
||||
// This would print true.
|
||||
fmt.Println(MyMask.HasFlag(OPT1))
|
||||
|
||||
// As would this:
|
||||
fmt.Println(MyMask.HasFlag(OPT3))
|
||||
|
||||
// But this would print false:
|
||||
fmt.Println(MyMask.HasFlag(OPT2))
|
||||
}
|
||||
|
||||
This would return true:
|
||||
|
||||
MyMask.HasFlag(OPT1)
|
||||
|
||||
As would this:
|
||||
|
||||
MyMask.HasFlag(OPT3)
|
||||
|
||||
But this would return false:
|
||||
|
||||
MyMask.HasFlag(OPT2)
|
||||
|
||||
# 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
|
||||
|
||||
var userPerms uint = 6 // 0x0000000000000006
|
||||
@@ -88,7 +151,7 @@ and your library has the following permission bits defined:
|
||||
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)
|
||||
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:
|
||||
|
||||
userPermMask = bitmask.NewMaskBitExplicit(PermsRead)
|
||||
// Nor:
|
||||
// userPermMask = PermsRead
|
||||
if userPermMask.HasFlag(userPerms) {
|
||||
userPermMask.HasFlag(bitmask.MaskBit(userPerms))
|
||||
|
||||
NOR:
|
||||
userPermMask = PermsRead
|
||||
if userPermMask.HasFlag(bitmask.MaskBit(userPerms)) {
|
||||
// ...
|
||||
}
|
||||
|
||||
This will be terribly, horribly wrong, cause incredibly unexpected results,
|
||||
and quite possibly cause massive security issues. Don't do it.
|
||||
|
||||
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,
|
||||
your inclination would be to bitwise-OR them together:
|
||||
Composite Flags
|
||||
|
||||
If you want to define a set of flags that are a combination
|
||||
(a "composite", or OR'd set) of other flags, your inclination
|
||||
would be to bitwise-OR them together:
|
||||
|
||||
const (
|
||||
flagA bitmask.MaskBit = 1 << iota // 1
|
||||
flagB // 2
|
||||
flagC // 4
|
||||
// ...
|
||||
)
|
||||
|
||||
const (
|
||||
flagAB bitmask.MaskBit = flagA | flagB // 3
|
||||
)
|
||||
|
||||
Which is fine and dandy. But if you then have:
|
||||
Which is fine, and the correct approach.
|
||||
But if you then have:
|
||||
|
||||
var myMask *bitmask.MaskBit = bitmask.NewMaskBit()
|
||||
|
||||
myMask.AddFlag(flagA)
|
||||
|
||||
You may expect this call to [MaskBit.HasFlag]:
|
||||
you may expect this call to [MaskBit.HasFlag]:
|
||||
|
||||
myMask.HasFlag(flagAB)
|
||||
|
||||
to be true, since flagA is "in" flagAB.
|
||||
|
||||
It will return false - HasFlag does strict comparisons.
|
||||
It will only return true if you then ALSO do:
|
||||
|
||||
// This would require setting flagA first.
|
||||
// The order of setting flagA/flagB doesn't matter,
|
||||
// but you must have both set for HasFlag(flagAB) to return true.
|
||||
myMask.AddFlag(flagB)
|
||||
It would only return true if you did:
|
||||
|
||||
or if you do:
|
||||
|
||||
// This can be done with or without additionally setting flagA.
|
||||
// ...
|
||||
myMask.AddFlag(flagAB)
|
||||
|
||||
Instead, if you want to see if a mask has membership within a composite flag,
|
||||
you can use [MaskBit.IsOneOf].
|
||||
or:
|
||||
|
||||
// ...
|
||||
myMask.AddFlag(flagA)
|
||||
myMask.AddFlag(flagB)
|
||||
|
||||
Instead, if you want to see if a mask has *at least one flag of* a composite flag,
|
||||
you can use [MaskBit.HasOneOf].
|
||||
|
||||
# Other Options
|
||||
|
||||
@@ -157,6 +230,6 @@ you may be interested in one of the following libraries:
|
||||
* [github.com/abice/go-enum]
|
||||
* [github.com/jeffreyrichter/enum/enum]
|
||||
|
||||
[exbibytes]: https://simple.wikipedia.org/wiki/Exbibyte
|
||||
[dmflags]: https://doomwiki.org/wiki/DMFlags
|
||||
*/
|
||||
package bitmask
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
Package bytesx aims to extend functionality of the stdlib [bytes] module.
|
||||
|
||||
TODO.
|
||||
*/
|
||||
package bytesx
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package binaryx aims to extend functionality of the stdlib [encoding/binary] module.
|
||||
*/
|
||||
package binaryx
|
||||
@@ -0,0 +1,122 @@
|
||||
package binaryx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
/*
|
||||
Marshal provides a Golang convention marshaling function entry point
|
||||
(e.g. like [encoding/json.Marshal], [encoding/xml.Marshal]) for arbitrary
|
||||
binary data.
|
||||
|
||||
It simply wraps [OrderedMarshal] with [encoding/binary.NativeEndian]
|
||||
(which can differ from platform to platform) as the ord parameter to [OrderedMarshal].
|
||||
|
||||
If you need to use a different or explicit [encoding/binary.ByteOrder],
|
||||
use [OrderedMarshal] directly instead.
|
||||
*/
|
||||
func Marshal(v any) (data []byte, err error) {
|
||||
|
||||
if data, err = OrderedMarshal(v, binary.NativeEndian); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
OrderedMarshal provides a slightly more flexible Golang convention marshaling
|
||||
function entry point (e.g. similar to [encoding/json.MarshalIndent],
|
||||
[encoding/xml.MarshalIndent]) for arbitrary binary data.
|
||||
|
||||
If v conforms to [encoding.BinaryMarshaler], then the marshal method will be called.
|
||||
|
||||
Otherwise, it wraps [encoding/binary.Write] with an internal buffer using ord
|
||||
as the [encoding/binary.ByteOrder] (endianness), so refer to that function
|
||||
for all caveats and other details.
|
||||
Note that [encoding/binary.Write] is *much* more strict than marshaling,
|
||||
and requires very basic typing.
|
||||
|
||||
If you have no need for a specific byte order/endianness or want to explicitly use this
|
||||
system's native byte order/endianness,
|
||||
use [Marshal] instead.
|
||||
*/
|
||||
func OrderedMarshal(v any, ord binary.ByteOrder) (data []byte, err error) {
|
||||
|
||||
var ok bool
|
||||
var b binary.Marshaler
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
if b, ok = v.(encoding.BinaryMarshaler); ok {
|
||||
if data, err = b.MarshalBinary(); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = binary.Write(buf, ord, v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data = buf.Bytes()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Unmarshal provides a Golang convention unmarshaling function entry point
|
||||
(e.g. like [encoding/json.Unmarshal], [encoding/xml.Unmarshal]) for arbitrary
|
||||
binary data.
|
||||
|
||||
It simply wraps [OrderedUnmarshal] with [encoding/binary.NativeEndian]
|
||||
(which can differ from platform to platform) as the ord parameter to [OrderedUnmarshal].
|
||||
|
||||
If you need to use a different or explicit [encoding/binary.ByteOrder],
|
||||
use [OrderedUnmarshal] directly instead.
|
||||
*/
|
||||
func Unmarshal(data []byte, v any) (err error) {
|
||||
|
||||
if err = OrderedUnmarshal(data, v, binary.NativeEndian); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
OrderedUnmarshal provides a slightly more flexible Golang convention unmarshaling
|
||||
for arbitrary binary data.
|
||||
|
||||
If v conforms to [encoding.BinaryUnmarshaler], then the unmarshal method will be called.
|
||||
|
||||
Otherwise, it wraps [encoding/binary.Read] with an internal buffer using ord
|
||||
as the [encoding/binary.ByteOrder] (endianness), so refer to that function
|
||||
for all caveats and other details.
|
||||
Note that [encoding/binary.Read] is *much* more strict than unmarshaling,
|
||||
and requires very basic typing.
|
||||
|
||||
If you have no need for a specific byte order/endianness or want to explicitly use this
|
||||
system's native byte order/endianness,
|
||||
use [Unmarshal] instead.
|
||||
*/
|
||||
func OrderedUnmarshal(data []byte, v any, ord binary.ByteOrder) (err error) {
|
||||
|
||||
var ok bool
|
||||
var b encoding.BinaryUnmarshaler
|
||||
var buf *bytes.Reader = bytes.NewReader(data)
|
||||
|
||||
if b, ok = v.(encoding.BinaryUnmarshaler); ok {
|
||||
if err = b.UnmarshalBinary(data); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = binary.Read(buf, ord, v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package encodingx aims to extend functionality of the stdlib [encoding] module.
|
||||
*/
|
||||
package encodingx
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package hexx aims to extend [encoding/hex] functionality.
|
||||
*/
|
||||
package hexx
|
||||
@@ -0,0 +1,30 @@
|
||||
package hexx
|
||||
|
||||
import (
|
||||
`io`
|
||||
)
|
||||
|
||||
/*
|
||||
HexString returns a custom-formatted hex representation of b.
|
||||
*/
|
||||
func HexString(b []byte, opts ...hexStrFmtrOpt) (out string) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
HexStringStream encodes a stream read from r and written into w.
|
||||
|
||||
HexStringStream will return cleanly if r returns no more bytes or if an [io.EOF] is encountered (the [io.EOF] will not be returned).
|
||||
Any other error will immediately halt reading/writing and will b returned in err.
|
||||
|
||||
If w is of a fixed capacity, it must be at least 2x the size of r's capacity.
|
||||
*/
|
||||
func HexStringStream(r io.Reader, w io.Writer, opts ...hexStrFmtrOpt) (read, wrtn uint64, err error) {
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package hexx
|
||||
|
||||
/*
|
||||
HexStrWithPrefix sets whether a prefix ("0x" by default, see [HexStrWithPrefixStr])
|
||||
*/
|
||||
func HexStrWithPrefix(pfx bool) (f hexStrFmtrOpt) {
|
||||
|
||||
f = func(h *hexStrFmtr) {
|
||||
if h.pfxStr == "" {
|
||||
h.pfxStr = "0x"
|
||||
}
|
||||
h.pfx = pfx
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
HexStrWithPrefixStr sets the prefix used if a prefix is to be included (see [HexStrWithPrefix]).
|
||||
|
||||
The default is "0x".
|
||||
*/
|
||||
func HexStrWithPrefixStr(pfx string) (f hexStrFmtrOpt) { return func(h *hexStrFmtr) { h.pfxStr = pfx } }
|
||||
|
||||
/*
|
||||
HexStrWithLower uses lowercase or uppercase hex character representation. The default is lowercase.
|
||||
|
||||
To use uppercase, specify this option with lower == false.
|
||||
*/
|
||||
func HexStrWithLower(lower bool) (f hexStrFmtrOpt) { return func(h *hexStrFmtr) { h.lower = lower } }
|
||||
|
||||
/*
|
||||
HexStrWithLeftPad specifies if left zero-padding should be used.
|
||||
|
||||
The default is true.
|
||||
*/
|
||||
func HexStrWithLeftPad(leftPad bool) (f hexStrFmtrOpt) {
|
||||
return func(h *hexStrFmtr) { h.noLeftPad = !leftPad }
|
||||
}
|
||||
|
||||
/*
|
||||
HexStrWithSegLeftPad specifies if left zero-padding should be used for each segment (see [HexStrWithSplit]).
|
||||
|
||||
The default is true.
|
||||
*/
|
||||
func HexStrWithSegLeftPad(segLeftPad bool) (f hexStrFmtrOpt) {
|
||||
return func(h *hexStrFmtr) { h.noSegmentLeftPad = !segLeftPad }
|
||||
}
|
||||
|
||||
/*
|
||||
HexStrWithSplit will split the hex string into "chunks" of numBytes bytes.
|
||||
|
||||
For example:
|
||||
numBytes == 0:
|
||||
0x0123456789abcdef
|
||||
|
||||
numBytes == 1:
|
||||
0x01 0x23 0x45 0x67 0x89 0xab 0xcd 0xef
|
||||
|
||||
numBytes == 2:
|
||||
0x0123 0x4567 0x89ab 0xcdef
|
||||
*/
|
||||
func HexStrWithSplit(numBytes uint) (f hexStrFmtrOpt) {
|
||||
return func(h *hexStrFmtr) { h.segmentBytes = numBytes }
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -0,0 +1,16 @@
|
||||
package hexx
|
||||
|
||||
type (
|
||||
hexStrFmtrOpt func(h *hexStrFmtr)
|
||||
|
||||
hexStrFmtr struct {
|
||||
pfx bool
|
||||
pfxStr string
|
||||
lower bool
|
||||
noLeftPad bool
|
||||
noSegmentLeftPad bool
|
||||
segmentBytes uint
|
||||
segmentSep string
|
||||
newlineBytes uint
|
||||
}
|
||||
)
|
||||
@@ -1,35 +1,47 @@
|
||||
module r00t2.io/goutils
|
||||
|
||||
go 1.25
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
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
|
||||
golang.org/x/sys v0.40.0
|
||||
r00t2.io/sysutils v1.16.1
|
||||
golang.org/x/sys v0.46.0
|
||||
r00t2.io/sysutils v1.16.2
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.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/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/goccy/go-json v0.10.6 // 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/reflectwalk v1.0.2 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.3.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -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/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
|
||||
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -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/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
|
||||
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
@@ -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/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=
|
||||
github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
@@ -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/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
r00t2.io/sysutils v1.16.2 h1:wI01UwZ/bXn/lzBiCpqDmzZCOWiK87kz04SB4xRw+W0=
|
||||
r00t2.io/sysutils v1.16.2/go.mod h1:iXK+ALOwIdRKjAJIE5USlkZ669SVDHBNNuYhunsznH8=
|
||||
|
||||
@@ -26,6 +26,10 @@ func (c *CtxIO) GetChunkLen() (size uint) {
|
||||
return c.l.GetChunkLen()
|
||||
}
|
||||
|
||||
func (c *CtxIO) Len() (unread int) {
|
||||
return c.buf.Len()
|
||||
}
|
||||
|
||||
func (c *CtxIO) Read(p []byte) (n int, err error) {
|
||||
|
||||
var nr int64
|
||||
|
||||
@@ -147,6 +147,17 @@ type (
|
||||
ContextReader
|
||||
ContextWriter
|
||||
}
|
||||
|
||||
/*
|
||||
LenReader allows one to read bytes (conforming to [io.Reader]),
|
||||
and also provides a Len() method which returns an int of remaining unread bytes.
|
||||
[bytes.Buffer] and [bytes.Reader] conforms to this, but see [CtxIO] for
|
||||
a package-provided type.
|
||||
*/
|
||||
LenReader interface {
|
||||
io.Reader
|
||||
Len() (unread int)
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -172,6 +183,7 @@ type (
|
||||
* [ChunkReadWriter]
|
||||
* [ContextReadWriter]
|
||||
* [SizedCopyBufferer]
|
||||
* [LenReader]
|
||||
|
||||
Unlike [XIO], it must be non-nil (see [NewCtxIO]) since it maintains state
|
||||
(though technically, one does not need to call [NewCtxIO] if they call
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
- ScopedLogger, take an io.Writer for each log level
|
||||
|
||||
- logging probably needs mutexes
|
||||
|
||||
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System".
|
||||
@@ -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.
|
||||
-- ZeroLog seems to be able to do it, take a peek there.
|
||||
|
||||
- StdLogger2; where stdout and stderr are both logged to depending on severity level.
|
||||
- StdLvlLogger; where stdout and stderr are both logged to depending on severity level.
|
||||
- make configurable via OR bitmask
|
||||
|
||||
- Suport remote loggers? (eventlog, syslog, journald)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package netx
|
||||
|
||||
import (
|
||||
`net/netip`
|
||||
)
|
||||
|
||||
var (
|
||||
ip4In6Legacy netip.Prefix = netip.MustParsePrefix("::/96")
|
||||
ip4In6Modern netip.Prefix = netip.MustParsePrefix("::ffff:0:0/96")
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
`errors`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadChars error = errors.New("netx/dnsx: invalid characters/encoding were encountered")
|
||||
ErrBadLabelLen error = errors.New("netx/dnsx: a label with invalid length was encountered")
|
||||
ErrBadPtrLen error = errors.New("netx/dnsx: a PTR record with invalid length was encountered")
|
||||
ErrBadPtrRoot error = errors.New("netx/dnsx: a PTR record with invalid root encountered")
|
||||
)
|
||||
@@ -0,0 +1,571 @@
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`encoding/base32`
|
||||
`fmt`
|
||||
`math`
|
||||
`net`
|
||||
`net/netip`
|
||||
`strings`
|
||||
|
||||
`go4.org/netipx`
|
||||
`r00t2.io/goutils/stringsx`
|
||||
)
|
||||
|
||||
/*
|
||||
AddrFromPtr returns a [net/netip.Addr] from a PTR record.
|
||||
|
||||
It is the inverse of [AddrToPtr].
|
||||
|
||||
See also [IpFromPtr].
|
||||
*/
|
||||
func AddrFromPtr(s string) (ip netip.Addr, err error) {
|
||||
|
||||
var idx int
|
||||
var ipStr string
|
||||
var tmpStr string
|
||||
var spl []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
switch len(spl) {
|
||||
case 6:
|
||||
if strings.Join(spl[4:], ".") != "in-addr.arpa" {
|
||||
err = ErrBadPtrRoot
|
||||
return
|
||||
}
|
||||
ipStr = fmt.Sprintf("%s.%s.%s.%s", spl[3], spl[2], spl[1], spl[0])
|
||||
case 34:
|
||||
if strings.Join(spl[32:], ".") != "ip6.arpa" {
|
||||
err = ErrBadPtrRoot
|
||||
return
|
||||
}
|
||||
tmpStr = stringsx.Reverse(strings.ReplaceAll(strings.Join(spl[:32], ""), ".", ""))
|
||||
for idx = 0; idx < len(tmpStr); idx++ {
|
||||
if idx%4 == 0 && idx != 0 {
|
||||
ipStr += ":"
|
||||
}
|
||||
ipStr += string(rune(tmpStr[idx]))
|
||||
}
|
||||
default:
|
||||
err = ErrBadPtrLen
|
||||
return
|
||||
}
|
||||
|
||||
if ip, err = netip.ParseAddr(ipStr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
AddrToPtr returns a PTR record from ip.
|
||||
|
||||
It is the inverse of [AddrFromPtr].
|
||||
|
||||
It includes the root label at the end (the trailing period).
|
||||
*/
|
||||
func AddrToPtr(ip netip.Addr) (s string) {
|
||||
|
||||
var idx int
|
||||
var b []byte
|
||||
var ipStr string
|
||||
|
||||
if ip.Is6() {
|
||||
ipStr = stringsx.Reverse(strings.ReplaceAll(ip.StringExpanded(), ":", ""))
|
||||
ipStr = strings.Join(
|
||||
strings.Split(ipStr, ""),
|
||||
".",
|
||||
)
|
||||
s = fmt.Sprintf("%s.ip6.arpa.", ipStr)
|
||||
} else {
|
||||
b = make([]byte, 4)
|
||||
copy(b, ip.AsSlice())
|
||||
for idx = len(b) - 1; idx >= 0; idx-- {
|
||||
ipStr += fmt.Sprintf("%d.", b[idx])
|
||||
}
|
||||
s = fmt.Sprintf("%s.in-addr.arpa.", ipStr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
DnsStrToWire returns a wire-format of a DNS name.
|
||||
|
||||
No validation or conversion (other than to wire format) is performed,
|
||||
and it is expected that any IDN(A)/Punycode translation has
|
||||
*already been performed* such that recordNm is in the ASCII form.
|
||||
(See [IsFqdn] for more information on IDN(A)/Punycode.)
|
||||
|
||||
For encoding reasons, if any given label/segment has a length of 0 or greater than 255 ([math.MaxUint8]),
|
||||
[ErrBadLabelLen] will be returned.
|
||||
|
||||
See [DnsWireToStr] for the inverse.
|
||||
*/
|
||||
func DnsStrToWire(recordNm string) (recordNmBytes []byte, err error) {
|
||||
|
||||
var cLen int
|
||||
var c []byte
|
||||
var cStr string
|
||||
var spl []string
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
spl = strings.Split(strings.TrimSuffix(recordNm, "."), ".")
|
||||
for _, cStr = range spl {
|
||||
c = []byte(cStr)
|
||||
cLen = len(c)
|
||||
if !(cLen > 0 && cLen <= math.MaxUint8) {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
buf.Write(append([]byte{uint8(cLen)}, c...))
|
||||
}
|
||||
|
||||
recordNmBytes = append(buf.Bytes(), 0x00)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
DnsWireToStr is the inverse of [DnsStrToWire]. A trailing . is not included.
|
||||
|
||||
For decoding reasons, it will exit with [ErrBadLabelLen] if recordNmBytes is nil/empty or
|
||||
if no terminating nullbyte is found after 256 label characters have been encountered.
|
||||
*/
|
||||
func DnsWireToStr(recordNmBytes []byte) (recordNm string, err error) {
|
||||
|
||||
var c []byte
|
||||
var cLen uint8
|
||||
var arrLen int
|
||||
var numChars int
|
||||
var labels []string
|
||||
var buf *bytes.Buffer
|
||||
|
||||
if recordNmBytes == nil || len(recordNmBytes) == 0 {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
buf = bytes.NewBuffer(recordNmBytes)
|
||||
labels = make([]string, 0)
|
||||
|
||||
arrLen = len(recordNmBytes)
|
||||
for {
|
||||
if cLen, err = buf.ReadByte(); err != nil {
|
||||
return
|
||||
}
|
||||
if cLen == 0x00 {
|
||||
break
|
||||
}
|
||||
numChars += int(cLen)
|
||||
if numChars > 255 {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
if numChars > arrLen {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
c = buf.Next(int(cLen))
|
||||
labels = append(labels, string(c))
|
||||
}
|
||||
|
||||
recordNm = strings.Join(labels, ".")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpFromPtr is like [AddrFromPtr] but with a [net.IP] instead.
|
||||
|
||||
It is the inverse of [IpToPtr].
|
||||
*/
|
||||
func IpFromPtr(s string) (ip net.IP, err error) {
|
||||
|
||||
var a netip.Addr
|
||||
|
||||
if a, err = AddrFromPtr(s); err != nil {
|
||||
return
|
||||
}
|
||||
ip = net.IP(a.AsSlice())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpToPtr is like [AddrToPtr] but with a [net.IP] instead.
|
||||
|
||||
It is the inverse of [IpFromPtr].
|
||||
*/
|
||||
func IpToPtr(ip net.IP) (s string) {
|
||||
|
||||
var a netip.Addr
|
||||
|
||||
a, _ = netipx.FromStdIP(ip)
|
||||
s = AddrToPtr(a)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdn returns a boolean indicating if s is an FQDN that strictly adheres to RFC format requirements.
|
||||
It performs no lookups/resolution attempts or network operations otherwise.
|
||||
|
||||
It will return true for the "apex record" (e.g. the "naked domain"), as this is a valid assignable FQDN.
|
||||
|
||||
It will return false for wildcard records (see [IsFqdnWildcard]).
|
||||
|
||||
s may or may not end in a period (the root zone; "absolute" FQDNs) (0x00 in wire format).
|
||||
|
||||
# TLDs
|
||||
|
||||
Because valid TLDs are fairly dynamic and can change frequently,
|
||||
validation is *not* performed against the TLD itself.
|
||||
This only ensures that s has a TLD label conforming to the character rules in the referenced RFCs.
|
||||
See [golang.org/x/net/publicsuffix] if precise TLD validation is required (though true TLD validation generally
|
||||
requires fetching the current TLD lists from IANA at runtime like [github.com/bombsimon/tld-validator]).
|
||||
|
||||
# Special RFC-Defined Accommodations
|
||||
|
||||
RFC 2181 [§ 11] specifies that site-local DNS software may accommodate non-RFC-conforming rules.
|
||||
This function may and likely will return false for these site-local deviations.
|
||||
The Lookup* functions/mthods in [net] should be used to validate in these casts
|
||||
if that accommodation is necessary.
|
||||
|
||||
Note that underscores are not valid for "true" FQDNs as they are only valid for e.g. SRV record names,
|
||||
TXT records, etc. - not A/AAAA/CNAME, etc. - see RFC 8553 for details.
|
||||
|
||||
See the following functions for allowing additional syntax/rule validation
|
||||
that have record-type-specific accommodations made:
|
||||
|
||||
* [IsFqdnDefinedTxt]
|
||||
* [IsFqdnNsec3]
|
||||
* [IsFqdnSrv]
|
||||
* [IsFqdnWildcard]
|
||||
|
||||
# RFC Coverage
|
||||
|
||||
This function should conform properly to:
|
||||
|
||||
* RFC 952
|
||||
* RFC 1034 and RFC 1035
|
||||
* RFC 1123
|
||||
* RFC 2181 (selectively, see above)
|
||||
|
||||
preferring the most up-to-date rules where relevant (e.g. labels may start with digits, as per RFC 1123).
|
||||
It enforces/checks label and overall length limits as defined by RFC.
|
||||
|
||||
# IDN(A) and Punycode
|
||||
|
||||
Note that it expects the ASCII-only/presentation form of a record name and
|
||||
will not perform any IDN/IDNA nor Punycode translation.
|
||||
If a caller anticipates FQDNs in their localized format,
|
||||
the caller must perform translation first
|
||||
(via e.g. [gitlab.com/golang-commonmark/puny], [golang.org/x/net/idna], etc.).
|
||||
|
||||
To reiterate, IDN/IDNA:
|
||||
|
||||
* RFC 3490
|
||||
* RFC 5890
|
||||
* RFC 5891
|
||||
* RFC 5892
|
||||
* RFC 5893
|
||||
* RFC 5894
|
||||
|
||||
and Punycode (RFC 3492) *MUST* use their ASCII forms, NOT the localized/Unicode formats.
|
||||
|
||||
[§ 11]: https://datatracker.ietf.org/doc/html/rfc2181#section-11
|
||||
*/
|
||||
func IsFqdn(s string) (fqdn bool) {
|
||||
|
||||
var lbl string
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
for _, lbl = range lbls {
|
||||
if !IsLabel(lbl) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fqdn = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnDefinedTxt is like [IsFqdn] but explicitly *only* allows fully-qualified
|
||||
RFC-defined TXT "subtypes":
|
||||
|
||||
* ACME DNS-01 (RFC 8555)
|
||||
* BIMI (RFC draft [bimi])
|
||||
* DKIM (RFC 6376, RFC 8301, RFC 8463)
|
||||
* DKIM ATPS (RFC 6541)
|
||||
* DMARC (RFC 7489, RFC 9091, RFC 9616)
|
||||
* MTA-STS (RFC 8461)
|
||||
* TLSRPT (RFC 8460)
|
||||
|
||||
Note that the following TXT "subtypes" do not have special formatting in labels/name,
|
||||
and thus are not covered by this function:
|
||||
|
||||
* SPF (RFC 4408, RFC 7208)
|
||||
|
||||
[bimi]: https://datatracker.ietf.org/doc/html/draft-brand-indicators-for-message-identification
|
||||
*/
|
||||
func IsFqdnDefinedTxt(fqdn string) (isOk bool) {
|
||||
|
||||
var lbls []string = strings.Split(fqdn, ".")
|
||||
|
||||
if lbls == nil || len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
switch lbls[0] {
|
||||
case "_dmarc", "_mta-sts", "_acme-challenge":
|
||||
if len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
isOk = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
case "_smtp":
|
||||
if len(lbls) <= 2 {
|
||||
return
|
||||
}
|
||||
if lbls[1] != "_tls" {
|
||||
return
|
||||
}
|
||||
isOk = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
default:
|
||||
if !IsLabel(lbls[0]) {
|
||||
return
|
||||
}
|
||||
switch lbls[1] {
|
||||
case "_domainkey", "_atps", "_bimi":
|
||||
isOk = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnNsec3 confirms (partially) that s is a valid NSEC3 record name.
|
||||
|
||||
Note that due to the record name being a base32 encoding of a *hash*, the validity
|
||||
can't be 100% confirmed with certainty - only basic checks can be done.
|
||||
|
||||
NSEC3 can be found via:
|
||||
|
||||
* RFC 5155
|
||||
* RFC 6840
|
||||
* RFC 6944
|
||||
* RFC 7129
|
||||
* RFC 8198
|
||||
* RFC 9077
|
||||
* RFC 9157
|
||||
* RFC 9276
|
||||
* RFC 9905
|
||||
|
||||
At the time of writing, only one hashing algorithm (SHA-1) has been specified.
|
||||
However, because this function does not check against the IANA registration at runtime,
|
||||
it's possible that this changes but the library may not immediately reflect this.
|
||||
*/
|
||||
func IsFqdnNsec3(s string) (maybeNsec3 bool) {
|
||||
|
||||
var h []byte
|
||||
var err error
|
||||
var isAscii bool
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
if len(lbls) <= 2 {
|
||||
return
|
||||
}
|
||||
if len(lbls[0]) != 32 { // SHA1 is 160 bits/20 bytes digest, which is always 32 chars in base32(hex)
|
||||
return
|
||||
}
|
||||
if h, err = base32.StdEncoding.DecodeString(strings.ToUpper(lbls[0])); err != nil {
|
||||
return
|
||||
}
|
||||
if isAscii, err = stringsx.IsAsciiSpecial(
|
||||
strings.ToLower(lbls[0]),
|
||||
false, false, false, false,
|
||||
[]byte{
|
||||
// Normally, Base32 goes A-Z, 2-7
|
||||
// but NSEC3 uses Base32Hex (RFC 4648 § 7),
|
||||
// which is 0-9A-V
|
||||
'0', '1', '2', '3', '4', '5',
|
||||
'6', '7', '8', '9', 'a', 'b',
|
||||
'c', 'd', 'e', 'f', 'g', 'h',
|
||||
'j', 'k', 'm', 'n', 'p', 'q',
|
||||
'r', 's', 't', 'u', 'v',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if isAscii {
|
||||
return
|
||||
}
|
||||
if len(h) != 20 {
|
||||
return
|
||||
}
|
||||
maybeNsec3 = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnSrv is like [IsFqdn], but explicitly *only* allows fully-qualified SRV records
|
||||
(i.e. underscores must start the first two labels, and there must be at least two additional
|
||||
labels after these labels).
|
||||
|
||||
Note that the protocol is not checked for validity, as that would require runtime
|
||||
validation against a resource liable to change and would need to be fetched dynamically - see
|
||||
the [IANA Protocol Numbers registry].
|
||||
|
||||
[IANA Protocol Numbers registry]: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
||||
*/
|
||||
func IsFqdnSrv(s string) (srv bool) {
|
||||
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
if len(lbls) <= 4 {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(lbls[0], "_") {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(lbls[1], "_") {
|
||||
return
|
||||
}
|
||||
srv = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsFqdnWildcard is like [IsFqdn] but explicitly *only* allows fully-qualified wildcard records.
|
||||
func IsFqdnWildcard(s string) (wildcard bool) {
|
||||
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
if lbls[0] != "*" {
|
||||
return
|
||||
}
|
||||
wildcard = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsLabel returns true if s is a valid DNS label for standard records.
|
||||
func IsLabel(s string) (isLbl bool) {
|
||||
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(s, "-") {
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(s, "-") {
|
||||
return
|
||||
}
|
||||
if isLbl, err = stringsx.IsAsciiSpecial(
|
||||
s,
|
||||
false, false, false, false,
|
||||
[]byte{
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l',
|
||||
'm', 'n', 'o', 'p', 'q', 'r',
|
||||
's', 't', 'u', 'v', 'w', 'x',
|
||||
'y', 'z', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9',
|
||||
'-',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsPtr returns true if s is a PTR (also called an "rDNS" or "reverse DNS" record) name.
|
||||
|
||||
If true, the IP is returned as well (otherwise it will be nil).
|
||||
*/
|
||||
func IsPtr(s string) (isPtr bool, addr net.IP) {
|
||||
|
||||
var err error
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if len(lbls) < 6 {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = AddrFromPtr(s); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
isPtr = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// commonFqdn is used to validate some rules common to all record names.
|
||||
func commonFqdn(s string) (isOk bool) {
|
||||
|
||||
var err error
|
||||
var lbl string
|
||||
var isAscii bool
|
||||
var labels []string
|
||||
var domstr string = strings.ToLower(strings.TrimSuffix(s, "."))
|
||||
|
||||
if isAscii, err = stringsx.IsAsciiSpecial(
|
||||
domstr, false, false, false, false,
|
||||
[]byte{
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l',
|
||||
'm', 'n', 'o', 'p', 'q', 'r',
|
||||
's', 't', 'u', 'v', 'w', 'x',
|
||||
'y', 'z', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9',
|
||||
'-', '_', '.',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if !isAscii {
|
||||
return
|
||||
}
|
||||
|
||||
if (len(domstr) + 1) > 255 { // +1 for root label
|
||||
return
|
||||
}
|
||||
labels = strings.Split(domstr, ".")
|
||||
|
||||
for _, lbl = range labels {
|
||||
if len(lbl) < 1 || len(lbl) > 63 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO?
|
||||
|
||||
isOk = true
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
`net/netip`
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPtr(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var ptr string
|
||||
var ip netip.Addr
|
||||
var ipStr string = "::ffff:192.168.0.1"
|
||||
var ptrStr string = "1.0.0.0.8.a.0.c.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa."
|
||||
|
||||
if ip, err = AddrFromPtr(ptrStr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("PTR -> Addr: %s -> %s", ptrStr, ip.String())
|
||||
if ip.String() != ipStr {
|
||||
t.Fatalf("expect IP %v, got %v", ipStr, ip.String())
|
||||
}
|
||||
|
||||
ptr = AddrToPtr(ip)
|
||||
if ptr != ptrStr {
|
||||
t.Fatalf("expect PTR %v, got %v", ptrStr, ptr)
|
||||
}
|
||||
t.Logf("Addr -> PTR: %s -> %s", ip.String(), ptr)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package netx includes extensions to the stdlib [net] module.
|
||||
*/
|
||||
package netx
|
||||
@@ -1,4 +0,0 @@
|
||||
/*
|
||||
Package netx includes extensions to the stdlib `net` module.
|
||||
*/
|
||||
package netx
|
||||
+296
@@ -102,11 +102,37 @@ func Cidr4ToStr(cidr uint8) (maskStr string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
FamilyToVer returns a more "human-friendly" IP version from a system/lower-level IP family
|
||||
([AFUnspec], [AFInet], [AFInet6]).
|
||||
|
||||
ipVer will be int(4) for [AFInet], int(6) for [AFInet6], int(0) for [AFUnspec], or
|
||||
int(-1) for an unknown family.
|
||||
*/
|
||||
func FamilyToVer(family uint16) (ipVer int) {
|
||||
|
||||
switch family {
|
||||
case AFInet:
|
||||
ipVer = 4
|
||||
case AFInet6:
|
||||
ipVer = 6
|
||||
case AFUnspec:
|
||||
ipVer = 0
|
||||
default:
|
||||
ipVer = -1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetAddrFamily returns the network family of a [net/netip.Addr].
|
||||
|
||||
See also [GetIpFamily].
|
||||
|
||||
Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6).
|
||||
(See [FamilyToVer] to get the associated higher-level value.)
|
||||
|
||||
If addr is not a "valid" IP address or the version can't be determined, family will be AFUnspec (usually 0x00/0).
|
||||
*/
|
||||
func GetAddrFamily(addr netip.Addr) (family uint16) {
|
||||
@@ -131,6 +157,9 @@ func GetAddrFamily(addr netip.Addr) (family uint16) {
|
||||
/*
|
||||
GetIpFamily returns the network family of a [net.IP].
|
||||
|
||||
Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6).
|
||||
(See [FamilyToVer] to get the associated higher-level value.)
|
||||
|
||||
See also [GetAddrFamily].
|
||||
|
||||
If ip is not a "valid" IP address or the version can't be determined,
|
||||
@@ -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 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) {
|
||||
|
||||
@@ -170,6 +201,56 @@ func IpRfc(ip net.IP) (rfcStr string) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpRfcStr implements [IpRfc]/[AddrRfc] for string representations of an IP address s.
|
||||
|
||||
If s is an IPv6 address already in the bracketed RFC format,
|
||||
then rfcStr will be equal to s.
|
||||
|
||||
If s is not a string representation of an IP address, rfcStr will be empty.
|
||||
|
||||
See [IpStripRfcStr] for the inverse (removing any brackets from s if present).
|
||||
*/
|
||||
func IpRfcStr(s string) (rfcStr string) {
|
||||
|
||||
var ip net.IP
|
||||
|
||||
if !IsIpAddr(s) {
|
||||
return
|
||||
}
|
||||
if IsBracketedIp6(s) {
|
||||
rfcStr = s
|
||||
return
|
||||
}
|
||||
ip = net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return
|
||||
}
|
||||
rfcStr = IpRfc(ip)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpStripRfcStr returns IP address string s without any brackets.
|
||||
|
||||
If s is not a valid IP address, stripStr will be empty.
|
||||
*/
|
||||
func IpStripRfcStr(s string) (stripStr string) {
|
||||
|
||||
if !IsIpAddr(s) {
|
||||
return
|
||||
}
|
||||
if !IsBracketedIp6(s) {
|
||||
stripStr = s
|
||||
return
|
||||
}
|
||||
stripStr = strings.TrimPrefix(s, "[")
|
||||
stripStr = strings.TrimSuffix(stripStr, "]")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask].
|
||||
|
||||
@@ -257,6 +338,199 @@ func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) {
|
||||
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.
|
||||
|
||||
@@ -408,3 +682,25 @@ func Mask4StrToMask(maskStr string) (mask uint32, err error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -7,6 +7,29 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFuncsDns(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var domBin []byte
|
||||
var domStr string
|
||||
var domEx string = "foo.r00t2.io"
|
||||
|
||||
if domBin, err = DnsStrToWire(domEx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Domain %s to wire: %#x\n", domEx, domBin)
|
||||
if domStr, err = DnsWireToStr(domBin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Domain wire %#x to string: %s\n", domBin, domStr)
|
||||
if domStr != domEx {
|
||||
t.Fatalf("DNS str wrong (%s != %s)\n)", domStr, domEx)
|
||||
}
|
||||
t.Logf("Domain string %s matches %s", domStr, domEx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestFuncsIP(t *testing.T) {
|
||||
|
||||
var err error
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
This is mostly just for generating some reference. It's not really intended for public consumption.
|
||||
@@ -0,0 +1,20 @@
|
||||
module r00t2.io/goutils/netx/internal
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require github.com/olekukonko/tablewriter v1.1.4
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.3.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
|
||||
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
||||
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
||||
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
@@ -0,0 +1,295 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
const (
|
||||
sectBordChar string = "#"
|
||||
sectBordWdth int = 2
|
||||
sectSpcPad int = 1
|
||||
|
||||
sectSidePad int = sectBordWdth + sectSpcPad
|
||||
sectBothPad int = sectSidePad * 2
|
||||
)
|
||||
|
||||
type (
|
||||
IpInfo struct {
|
||||
// Desc string
|
||||
Index int
|
||||
// IP netip.Addr
|
||||
Is4 bool
|
||||
Is4In6 bool
|
||||
Is6 bool
|
||||
IsGlobalUnicast bool
|
||||
IsLinkLocalUnicast bool
|
||||
IsInterfaceLocalMulticast bool
|
||||
IsLinkLocalMulticast bool
|
||||
IsMulticast bool
|
||||
IsLoopback bool
|
||||
IsPrivate bool
|
||||
IsUnspecified bool
|
||||
IsValid bool
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
datHdrsFunc []string = []string{
|
||||
"-",
|
||||
".Is4", ".Is4In6()", ".Is6()",
|
||||
".IsGlobalUnicast()", ".IsLinkLocalUnicast()",
|
||||
".IsInterfaceLocalMulticast()", ".IsLinkLocalMulticast()", ".IsMulticast()",
|
||||
".IsLoopback()",
|
||||
".IsPrivate()",
|
||||
".IsUnspecified()", ".IsValid()",
|
||||
}
|
||||
|
||||
datHdrsKey []string = []string{
|
||||
"Address Index (See Fig. 3)",
|
||||
"IPv4", "4-in-6", "IPv6",
|
||||
"Global Unicast", "Link-Local Unicast",
|
||||
"Interface Local Multicast", "Link-Local Multicast", "Multicast",
|
||||
"Loopback",
|
||||
"Private/LAN",
|
||||
"\"Unspecified\" Address", "Valid Address",
|
||||
}
|
||||
|
||||
datHdrsShort = []string{
|
||||
"IDX",
|
||||
"4", "6(4)", "6",
|
||||
"GU", "LLU",
|
||||
"ILM", "LLM", "M",
|
||||
"LO",
|
||||
"P",
|
||||
"U", "V",
|
||||
}
|
||||
|
||||
exampleAddrs [][2]string = [][2]string{
|
||||
[2]string{"IPv4", "203.0.113.10"}, // RFC 5737 address (https://datatracker.ietf.org/doc/html/rfc5737)
|
||||
[2]string{"IPv4 Unspecified", "0.0.0.0"}, // Generally used to represent all of IPv4 address space
|
||||
[2]string{"IPv4 Global Unicast", "173.230.132.76"}, // r00t2.io
|
||||
[2]string{"IPv4 Multicast (All) (RFC 1112 § 4)", "224.0.0.1"}, // Should encompass all the multicast below. 224.0.0.0 is reserved.
|
||||
[2]string{"IPv4 Multicast (Reserved) (RFC 1112 § 4)", "224.0.0.0"}, // Reserved per RFC but it should still report multicast.
|
||||
[2]string{"IPv4 Multicast (Link-Local Multicast/Local Network Control Block) (RFC 5771 § 4)", "224.0.0.18"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-4 (VRRP multicast addr)
|
||||
[2]string{"IPv4 Multicast (Internetwork Control Block) (RFC 5771 § 5)", "224.0.1.68"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-5 (mdhcpdiscover, RFC 2730)
|
||||
[2]string{"IPv4 Multicast (AD-HOC I) (RFC 5771 § 6)", "224.0.2.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-6
|
||||
[2]string{"IPv4 Multicast (SDP/SAP) (RFC 5771 § 7)", "224.2.0.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-7
|
||||
[2]string{"IPv4 Multicast (AD-HOC II) (RFC 5771 § 6)", "224.3.0.10"}, // (above)
|
||||
[2]string{"IPv4 Multicast (Source-Specific) (RFC 5771 § 8)", "232.0.0.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-8
|
||||
[2]string{"IPv4 Multicast (GLOP) (RFC 5771 § 9)", "233.0.0.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-9
|
||||
[2]string{"IPv4 Multicast (AD-HOC III) (RFC 5771 § 6)", "233.252.0.10"}, // (above)
|
||||
[2]string{"IPv4 Multicast (Administrative) (RFC 5771 § 10)", "239.0.0.10"}, // https://datatracker.ietf.org/doc/html/rfc5771#section-10
|
||||
[2]string{"IPv4 Link-Local Unicast (RFC 3927 § 2.1)", "169.254.1.10"}, // https://datatracker.ietf.org/doc/html/rfc3927#section-2.1
|
||||
[2]string{"IPv4 Loopback", "127.0.1.10"}, // It's actually 127/8. Cannot believe how many people do not know this.
|
||||
[2]string{"IPv4 Private (RFC 1918)", "10.0.0.10"}, // https://datatracker.ietf.org/doc/html/rfc1918
|
||||
[2]string{"4-in-6 (RFC 4291 § 2.5.5.1)", "::203.0.113.10"}, // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1
|
||||
[2]string{"4-in-6 (RFC 4291 § 2.5.5.1) (Native)", "::cb00:710a"}, // ""
|
||||
[2]string{"4-in-6 (RFC 4291 § 2.5.5.2)", "::ffff:203.0.113.10"}, // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
|
||||
[2]string{"4-in-6 (RFC 4291 § 2.5.5.2) (Native)", "::ffff:cb00:710a"}, // ""
|
||||
[2]string{"IPv6", "2001:db8::cb00:710a"}, // RFC 3849 (https://datatracker.ietf.org/doc/html/rfc3849) / RFC 9637 (https://datatracker.ietf.org/doc/html/rfc9637) address
|
||||
[2]string{"IPv6 Unspecified", "::"}, // Generally used to represent all of IPv6 address space
|
||||
[2]string{"IPv6 Global Unicast", "2600:3c02::f03c:91ff:fe93:c0a7"}, // r00t2.io
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (Reserved Net)", "ff00::"}, // https://datatracker.ietf.org/doc/html/rfc4291#section-2.7.1
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (Reserved)", "ff00::1"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Nodes) (Interface-Local)", "ff01::1"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Nodes) (Link-Local)", "ff02::1"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Interface-Local)", "ff01::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Link-Local)", "ff02::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Admin-Local)", "ff04::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Site-Local)", "ff05::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Org-Local)", "ff08::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Internet/Global)", "ff0e::2"}, // ""
|
||||
[2]string{"IPv6 Multicast (RFC 4291 § 2.7.1) (Solicited Node)", "ff02::1:ff00:10"}, // ""
|
||||
[2]string{"IPv6 Link-Local Unicast (RFC 4291 § 2.5.6)", "fe80::cb00:710a"}, // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.6
|
||||
[2]string{"IPv6 Loopback", "::1"}, // It's explicitly always a /128 with the address ::1 per RFC 4291 § 2.5.3.
|
||||
[2]string{"IPv6 Private (Unique-Local Addresses) (RFC 4193) (Reserved)", "fc00::10"}, // https://datatracker.ietf.org/doc/html/rfc4193
|
||||
[2]string{"IPv6 Private (Unique-Local Addresses) (RFC 4193) (Valid)", "fd00::10"}, // ""
|
||||
}
|
||||
|
||||
descs []string = make([]string, len(exampleAddrs))
|
||||
ips []netip.Addr = make([]netip.Addr, len(exampleAddrs))
|
||||
|
||||
sectSep string = "\n" + strings.Repeat("-", 80) + "\n"
|
||||
)
|
||||
|
||||
func genHdr(sectNm string) (s string) {
|
||||
|
||||
// Wish I finished stringsx.Banner at time of writing this...
|
||||
var fillLen int = sectBothPad + len(sectNm)
|
||||
|
||||
s = fmt.Sprintf(
|
||||
"%s\n"+ // top border
|
||||
// begin title line
|
||||
"%-*s"+ // left-justify/right-pad
|
||||
"%s"+ // sectNm text
|
||||
"%[2]*[3]s\n"+ // one-indexed; repeat pad num and pad str from left border as right border
|
||||
// end title line
|
||||
"%[1]s\n", // bottom border
|
||||
strings.Repeat(sectBordChar, fillLen), // top and bottom (bottom uses index)
|
||||
sectSidePad, strings.Repeat(sectBordChar, sectBordWdth), // title left and right borders (right uses index)
|
||||
sectNm, // title text
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var idx int
|
||||
var s string
|
||||
var desc string
|
||||
var pair [2]string
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
var tbl *tablewriter.Table = tablewriter.NewTable(
|
||||
buf,
|
||||
tablewriter.WithConfig(
|
||||
tablewriter.Config{
|
||||
Header: tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoFormat: tw.Off,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
// requires .Batch(), and the autoheader forces all caps.
|
||||
// https://github.com/olekukonko/tablewriter/issues/143
|
||||
// https://github.com/olekukonko/tablewriter/issues/190
|
||||
/*
|
||||
tablewriter.WithBehavior(
|
||||
tw.Behavior{
|
||||
Structs: tw.Struct{
|
||||
AutoHeader: tw.On,
|
||||
},
|
||||
},
|
||||
),
|
||||
*/
|
||||
tablewriter.WithRendition(
|
||||
tw.Rendition{
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{BetweenRows: tw.On},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
var tErr error
|
||||
if tbl != nil {
|
||||
if tErr = tbl.Close(); tErr != nil {
|
||||
log.Printf("Error closing table: %v", tErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
buf.WriteString(genHdr("Fig. 1: Address Evaluations"))
|
||||
buf.WriteString("(See Fig. 2 for a key of header symbols to names)\n\n")
|
||||
|
||||
tbl.Header(datHdrsShort)
|
||||
for idx, pair = range exampleAddrs {
|
||||
desc, s = pair[0], pair[1]
|
||||
descs[idx] = desc
|
||||
if ips[idx], err = netip.ParseAddr(s); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
// Currently no way to skip cols etc. https://github.com/olekukonko/tablewriter/issues/317
|
||||
// rows[idx] = IpInfo{
|
||||
if err = tbl.Append(
|
||||
IpInfo{
|
||||
// Desc: desc,
|
||||
Index: idx,
|
||||
// IP: ip,
|
||||
Is4: ips[idx].Is4(),
|
||||
Is4In6: ips[idx].Is4In6(),
|
||||
Is6: ips[idx].Is6(),
|
||||
IsGlobalUnicast: ips[idx].IsGlobalUnicast(),
|
||||
IsLinkLocalUnicast: ips[idx].IsLinkLocalUnicast(),
|
||||
IsInterfaceLocalMulticast: ips[idx].IsInterfaceLocalMulticast(),
|
||||
IsLinkLocalMulticast: ips[idx].IsLinkLocalMulticast(),
|
||||
IsMulticast: ips[idx].IsMulticast(),
|
||||
IsLoopback: ips[idx].IsLoopback(),
|
||||
IsPrivate: ips[idx].IsPrivate(),
|
||||
IsUnspecified: ips[idx].IsUnspecified(),
|
||||
IsValid: ips[idx].IsValid(),
|
||||
},
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
/*
|
||||
if err = tbl.Bulk(rows); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
*/
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
buf.WriteString(sectSep)
|
||||
buf.WriteString(genHdr("Fig. 2: Headers Key for Fig. 1"))
|
||||
tbl.Reset()
|
||||
tbl.Header("Symbol", "Description", "net/netip.Addr Method")
|
||||
for idx = range datHdrsKey {
|
||||
if err = tbl.Append(
|
||||
[]string{
|
||||
datHdrsShort[idx],
|
||||
datHdrsKey[idx],
|
||||
datHdrsFunc[idx],
|
||||
},
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
buf.WriteString(sectSep)
|
||||
buf.WriteString(genHdr("Fig. 3: Test/Example IP Address Reference/Lookup for Fig. 1"))
|
||||
buf.WriteString("(See Fig. 4 for Descriptions/Detailed Information)\n\n")
|
||||
tbl.Reset()
|
||||
tbl.Header("Index", "Address (Raw)", "Address (Parsed)")
|
||||
for idx = range exampleAddrs {
|
||||
if err = tbl.Append(
|
||||
[]string{
|
||||
strconv.Itoa(idx),
|
||||
exampleAddrs[idx][1],
|
||||
ips[idx].String(),
|
||||
},
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
buf.WriteString(sectSep)
|
||||
buf.WriteString(genHdr("Fig. 4: Extended Information for Fig. 3"))
|
||||
tbl.Reset()
|
||||
tbl.Header("Index", "Description")
|
||||
for idx = range exampleAddrs {
|
||||
if err = tbl.Append(
|
||||
[]string{
|
||||
strconv.Itoa(idx),
|
||||
descs[idx],
|
||||
},
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
fmt.Println(buf.String())
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"maps"
|
||||
"net/netip"
|
||||
`os`
|
||||
"slices"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
`github.com/olekukonko/tablewriter/tw`
|
||||
)
|
||||
|
||||
type (
|
||||
IpInfo struct {
|
||||
Desc string `r:"-"`
|
||||
IP netip.Addr `r:"-"`
|
||||
Is4 bool
|
||||
Is4In6 bool
|
||||
Is6 bool
|
||||
IsGlobalUnicast bool
|
||||
IsInterfaceLocalMulticast bool
|
||||
IsLinkLocalMulticast bool
|
||||
IsLinkLocalUnicast bool
|
||||
IsLoopback bool
|
||||
IsMulticast bool
|
||||
IsPrivate bool
|
||||
IsUnspecified bool
|
||||
IsValid bool
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var s string
|
||||
var idx int
|
||||
var desc string
|
||||
var ip netip.Addr
|
||||
var rows []IpInfo
|
||||
/*
|
||||
var typ reflect.Type
|
||||
var val reflect.Value
|
||||
var fieldVal reflect.Value
|
||||
var field reflect.StructField
|
||||
*/
|
||||
var exampleAddrs map[string]string
|
||||
var tbl *tablewriter.Table = tablewriter.NewTable(
|
||||
os.Stdout,
|
||||
tablewriter.WithBehavior(
|
||||
tw.Behavior{
|
||||
Structs: tw.Struct{
|
||||
AutoHeader: tw.On,
|
||||
},
|
||||
},
|
||||
),
|
||||
tablewriter.WithRendition(
|
||||
tw.Rendition{
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{BetweenRows: tw.On},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
var tErr error
|
||||
if tbl != nil {
|
||||
if tErr = tbl.Close(); tErr != nil {
|
||||
log.Printf("Error closing table: %v", tErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
exampleAddrs = map[string]string{
|
||||
"IPv4": "203.0.113.10", // RFC 5737 address (https://datatracker.ietf.org/doc/html/rfc5737)
|
||||
"IPv4 Unspecified": "0.0.0.0",
|
||||
"IPv4 Global Unicast": "173.230.132.76", // r00t2.io
|
||||
"IPv4 Multicast (All) (RFC 1112 § 4)": "224.0.0.1", // Should encompass all the multicast below. 224.0.0.0 is reserved.
|
||||
"IPv4 Multicast (Reserved) (RFC 1112 § 4)": "224.0.0.0", // Reserved per RFC but it should still report multicast.
|
||||
"IPv4 Multicast (Link-Local Multicast/Local Network Control Block) (RFC 5771 § 4)": "224.0.0.18", // https://datatracker.ietf.org/doc/html/rfc5771#section-4 (VRRP multicast addr)
|
||||
"IPv4 Multicast (Internetwork Control Block) (RFC 5771 § 5)": "224.0.1.68", // https://datatracker.ietf.org/doc/html/rfc5771#section-5 (mdhcpdiscover, RFC 2730)
|
||||
"IPv4 Multicast (AD-HOC I) (RFC 5771 § 6)": "224.0.2.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-6
|
||||
"IPv4 Multicast (SDP/SAP) (RFC 5771 § 7)": "224.2.0.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-7
|
||||
"IPv4 Multicast (AD-HOC II) (RFC 5771 § 6)": "224.3.0.10", // (above)
|
||||
"IPv4 Multicast (Source-Specific) (RFC 5771 § 8)": "232.0.0.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-8
|
||||
"IPv4 Multicast (GLOP) (RFC 5771 § 9)": "233.0.0.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-9
|
||||
"IPv4 Multicast (AD-HOC III) (RFC 5771 § 6)": "233.252.0.10", // (above)
|
||||
"IPv4 Multicast (Administrative) (RFC 5771 § 10)": "239.0.0.10", // https://datatracker.ietf.org/doc/html/rfc5771#section-10
|
||||
"IPv4 Link-Local Unicast (RFC 3927 § 2.1)": "169.254.1.10", // https://datatracker.ietf.org/doc/html/rfc3927#section-2.1
|
||||
"IPv4 Loopback": "127.0.1.10", // It's actually 127/8. Cannot believe how many people do not know this.
|
||||
"IPv4 Private (RFC 1918)": "10.0.0.10", // https://datatracker.ietf.org/doc/html/rfc1918
|
||||
"4-in-6 (RFC 4291 § 2.5.5.1)": "::203.0.113.10", // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1
|
||||
"4-in-6 (RFC 4291 § 2.5.5.1) (Native)": "::cb00:710a", // ""
|
||||
"4-in-6 (RFC 4291 § 2.5.5.2)": "::ffff:203.0.113.10", // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
|
||||
"4-in-6 (RFC 4291 § 2.5.5.2) (Native)": "::ffff:cb00:710a", // ""
|
||||
"IPv6": "2001:db8::cb00:710a", // RFC 3849 (https://datatracker.ietf.org/doc/html/rfc3849) / RFC 9637 (https://datatracker.ietf.org/doc/html/rfc9637) address
|
||||
"IPv6 Unspecified": "::",
|
||||
"IPv6 Global Unicast": "2600:3c02::f03c:91ff:fe93:c0a7", // r00t2.io
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (Reserved Net)": "ff00::", // https://datatracker.ietf.org/doc/html/rfc4291#section-2.7.1
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (Reserved)": "ff00::1", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Nodes) (Interface-Local)": "ff01::1", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Nodes) (Link-Local)": "ff02::1", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Interface-Local)": "ff01::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Link-Local)": "ff02::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Admin-Local)": "ff04::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Site-Local)": "ff05::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Org-Local)": "ff08::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (All Routers) (Internet/Global)": "ff0e::2", // ""
|
||||
"IPv6 Multicast (RFC 4291 § 2.7.1) (Solicited Node)": "ff02::1:ff00:10", // ""
|
||||
"IPv6 Link-Local Unicast (RFC 4291 § 2.5.6)": "fe80::cb00:710a", // https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.6
|
||||
"IPv6 Loopback": "::1", // It's explicitly always a /128 with the address ::1 per RFC 4291 § 2.5.3.
|
||||
"IPv6 Private (Unique-Local Addresses) (RFC 4193) (Reserved)": "fc00::10", // https://datatracker.ietf.org/doc/html/rfc4193
|
||||
"IPv6 Private (Unique-Local Addresses) (RFC 4193) (Valid)": "fd00::10", // ""
|
||||
}
|
||||
rows = make([]IpInfo, len(exampleAddrs))
|
||||
|
||||
for idx, desc = range slices.Sorted(maps.Keys(exampleAddrs)) {
|
||||
s = exampleAddrs[desc]
|
||||
if ip, err = netip.ParseAddr(s); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
// Currently no way to skip cols etc. https://github.com/olekukonko/tablewriter/issues/317
|
||||
rows[idx] = IpInfo{
|
||||
Desc: desc,
|
||||
IP: ip,
|
||||
Is4: ip.Is4(),
|
||||
Is4In6: ip.Is4In6(),
|
||||
Is6: ip.Is6(),
|
||||
IsGlobalUnicast: ip.IsGlobalUnicast(),
|
||||
IsInterfaceLocalMulticast: ip.IsInterfaceLocalMulticast(),
|
||||
IsLinkLocalMulticast: ip.IsLinkLocalMulticast(),
|
||||
IsLinkLocalUnicast: ip.IsLinkLocalUnicast(),
|
||||
IsLoopback: ip.IsLoopback(),
|
||||
IsMulticast: ip.IsMulticast(),
|
||||
IsPrivate: ip.IsPrivate(),
|
||||
IsUnspecified: ip.IsUnspecified(),
|
||||
IsValid: ip.IsValid(),
|
||||
}
|
||||
}
|
||||
if err = tbl.Bulk(rows); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if err = tbl.Render(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
- write a flag parser/converter for github.com/scorpionknifes/go-pcre (and github.com/GRbit/go-pcre) :
|
||||
----
|
||||
// i CASELESS
|
||||
// m MULTILINE
|
||||
// s DOTALL
|
||||
// x EXTENDED
|
||||
// U UNGREEDY
|
||||
// 8 UTF8 (non-standard flag letter; standard PCRE uses (*UTF8) outside (?...) syntax)
|
||||
//
|
||||
// Flags after a '-' inside group disable associated flag and are ignored
|
||||
// (applies inside the regex engine once prefix is stripped).
|
||||
// pattern, flags := ParsePrefixFlags(`(?im)^hello`)
|
||||
// re := pcre.MustCompile(pattern, flags)
|
||||
func ParsePrefixFlags(pattern string) (string, int) {
|
||||
if !strings.HasPrefix(pattern, "(?") {
|
||||
return pattern, 0
|
||||
}
|
||||
|
||||
end := strings.IndexByte(pattern, ')')
|
||||
if end < 0 {
|
||||
return pattern, 0
|
||||
}
|
||||
|
||||
inner := pattern[2:end] // everything between "(?" and ")"
|
||||
|
||||
for _, ch := range inner {
|
||||
if !strings.ContainsRune("imsxU8-", ch) {
|
||||
return pattern, 0
|
||||
}
|
||||
}
|
||||
|
||||
positive := inner
|
||||
if dash := strings.IndexByte(inner, '-'); dash >= 0 {
|
||||
positive = inner[:dash]
|
||||
}
|
||||
|
||||
var flags int
|
||||
for _, ch := range positive {
|
||||
switch ch {
|
||||
case 'i':
|
||||
flags |= pcre.CASELESS
|
||||
case 'm':
|
||||
flags |= pcre.MULTILINE
|
||||
case 's':
|
||||
flags |= pcre.DOTALL
|
||||
case 'x':
|
||||
flags |= pcre.EXTENDED
|
||||
case 'U':
|
||||
flags |= pcre.UNGREEDY
|
||||
case '8':
|
||||
flags |= pcre.UTF8
|
||||
}
|
||||
}
|
||||
|
||||
return pattern[end+1:], flags
|
||||
}
|
||||
----
|
||||
@@ -2,4 +2,14 @@
|
||||
-- draw border around multiline s
|
||||
-- i have a version in python somewhere that does this, should dig that up
|
||||
|
||||
- Tokenize() (new function)
|
||||
-- PosixFilename() (new function) -- https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
|
||||
-- strings.ToLower()
|
||||
-- compact consecutive:
|
||||
--- whitespace
|
||||
--- .
|
||||
--- ,
|
||||
-- set custom replacement string (defaults to "_")
|
||||
-- replace whitespace (after/during compact) with _ (customizable?)
|
||||
|
||||
- create bytesx package that duplicates the functions here?
|
||||
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -1,11 +1,212 @@
|
||||
package stringsx
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`errors`
|
||||
`fmt`
|
||||
`io`
|
||||
`slices`
|
||||
`strings`
|
||||
`unicode`
|
||||
)
|
||||
|
||||
/*
|
||||
HasBookend returns true if string s both begins AND ends with sym.
|
||||
|
||||
It is more strict than [HasBoundary] (which only requires
|
||||
that s has sym at the beginning OR the end.)
|
||||
|
||||
Examples:
|
||||
HasBookend("|foo|", "|") → true
|
||||
HasBookend("|foo", "|") → false
|
||||
HasBookend("foo|", "|") → false
|
||||
HasBookend("fo|o", "|") → false
|
||||
HasBookend("|foo| ", "|") → false // Whitespace prevents match
|
||||
HasBookend(" |foo| ", "|") → false
|
||||
|
||||
sym may be a multi-rune string.
|
||||
If sym is empty, HasBookend will *always* return true.
|
||||
*/
|
||||
func HasBookend(s, sym string) (bounded bool) {
|
||||
|
||||
bounded = strings.HasPrefix(s, sym) && strings.HasSuffix(s, sym)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
HasBoundary returns true if string s starts OR ends with symbol sym.
|
||||
|
||||
Examples:
|
||||
HasBoundary("|foo|", "|") → true
|
||||
HasBoundary("|foo", "|") → true
|
||||
HasBoundary("foo|", "|") → true
|
||||
HasBoundary("fo|o", "|") → false
|
||||
HasBoundary("|foo| ", "|") → true
|
||||
HasBoundary(" |foo| ", "|") → false // Whitespace prevents match
|
||||
|
||||
sym may be a multi-rune string.
|
||||
If sym is empty, HasBoundary will *always* return true.
|
||||
|
||||
If you instead require string s to be *enclosed* by sym, see [HasBookend].
|
||||
*/
|
||||
func HasBoundary(s, sym string) (bounded bool) {
|
||||
|
||||
bounded = strings.HasPrefix(s, sym) || strings.HasSuffix(s, sym)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsAscii returns true if all characters in string s are ASCII.
|
||||
|
||||
This simply wraps [IsAsciiSpecial]:
|
||||
|
||||
isAscii, err = IsAsciiSpecial(s, allowCtl, true, allowExt, true, nil, nil)
|
||||
*/
|
||||
func IsAscii(s string, allowCtl, allowExt bool) (isAscii bool, err error) {
|
||||
|
||||
if isAscii, err = IsAsciiSpecial(
|
||||
s, allowCtl, true, allowExt, true, nil, nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsAsciiBuf returns true if all of buffer buf is valid ASCII.
|
||||
|
||||
Note that the buffer will be consumed/read by this function.
|
||||
|
||||
This simply wraps [IsAsciiBufSpecial]:
|
||||
|
||||
isAscii, err = IsAsciiBufSpecial(r, allowCtl, true, allowExt, true, nil, nil)
|
||||
*/
|
||||
func IsAsciiBuf(r io.RuneReader, allowCtl, allowExt bool) (isAscii bool, err error) {
|
||||
|
||||
if isAscii, err = IsAsciiBufSpecial(
|
||||
r, allowCtl, true, allowExt, true, nil, nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsAsciiSpecial allows for specifying specific ASCII ranges.
|
||||
|
||||
allowCtl, if true, will allow control characters (0x00 to 0x1f inclusive).
|
||||
|
||||
allowPrint, if true, will allow printable characters (what most people think of
|
||||
when they say "ASCII") (0x20 to 0x7f inclusive).
|
||||
|
||||
allowExt, if true, will allow for "extended ASCII" - some later dialects expand
|
||||
to a full 8-bit ASCII range (0x80 to 0xff inclusive).
|
||||
|
||||
wsCtl, if true, "shifts" the "whitespace control characters" (\t, \n, \r) to the "printable" space
|
||||
(such that allowPrint controls their validation). Thus:
|
||||
|
||||
IsAsciiSpecial(s, false, true, false, true, nil, nil)
|
||||
|
||||
has the same effect as specifying:
|
||||
|
||||
IsAsciiSpecial(s, false, true, false, (-), []byte("\t\n\r"), nil)
|
||||
|
||||
incl, if non-nil and non-empty, allows *additional* characters to be specified as included
|
||||
that would normally *not* be allowed.
|
||||
|
||||
excl, if non-nil and non-empty, invalidates on additional characters that would normally be allowed.
|
||||
|
||||
excl, if specified, takes precedence over incl if specified.
|
||||
|
||||
An [AsciiInvalidError] will be returned on the first encountered invalid character.
|
||||
*/
|
||||
func IsAsciiSpecial(s string, allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (isAscii bool, err error) {
|
||||
|
||||
var buf *bytes.Buffer = bytes.NewBufferString(s)
|
||||
|
||||
if isAscii, err = IsAsciiBufSpecial(buf, allowCtl, allowPrint, allowExt, allowWs, incl, excl); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsAsciiBufSpecial is the same as [IsAsciiSpecial] but operates on an [io.RuneReader].
|
||||
|
||||
Note that the buffer will be consumed/read by this function.
|
||||
|
||||
It will not return an [io.EOF] if encountered, but any other errors encountered will be returned.
|
||||
It is expected that r will return an [io.EOF] when exhausted.
|
||||
|
||||
An [AsciiInvalidError] will be returned on the first encountered invalid character.
|
||||
*/
|
||||
func IsAsciiBufSpecial(r io.RuneReader, allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (isAscii bool, err error) {
|
||||
|
||||
var b rune
|
||||
var bLen int
|
||||
var nextNewline bool
|
||||
var tmpErr *AsciiInvalidError = new(AsciiInvalidError)
|
||||
// I know, I know. This is essentually a lookup table. Keeps it speedy.
|
||||
var allowed [256]bool = getAsciiCharMap(allowCtl, allowPrint, allowExt, allowWs, incl, excl)
|
||||
|
||||
for {
|
||||
if b, bLen, err = r.ReadRune(); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
err = nil
|
||||
isAscii = true
|
||||
}
|
||||
return
|
||||
}
|
||||
// Set these *before* OK
|
||||
if nextNewline {
|
||||
tmpErr.Line++
|
||||
tmpErr.LineByte = 0
|
||||
tmpErr.LineChar = 0
|
||||
nextNewline = false
|
||||
} else {
|
||||
tmpErr.LineChar++
|
||||
}
|
||||
tmpErr.Char++
|
||||
|
||||
if b == '\n' {
|
||||
nextNewline = true
|
||||
}
|
||||
if b == rune(0xfffd) {
|
||||
// not even valid unicode
|
||||
tmpErr.BadChar = b
|
||||
tmpErr.BadBytes = []byte(string(b))
|
||||
err = tmpErr
|
||||
return
|
||||
}
|
||||
if bLen > 2 || b > 0xff {
|
||||
// ASCII only occupies a single byte, ISO-8859-1 occupies 2
|
||||
tmpErr.BadChar = b
|
||||
tmpErr.BadBytes = []byte(string(b))
|
||||
err = tmpErr
|
||||
return
|
||||
}
|
||||
if !allowed[byte(b)] {
|
||||
tmpErr.BadChar = b
|
||||
tmpErr.BadBytes = []byte{byte(b)}
|
||||
err = tmpErr
|
||||
return
|
||||
}
|
||||
|
||||
// Set these *after* OK
|
||||
tmpErr.LineByte += uint64(bLen)
|
||||
tmpErr.Byte += uint64(bLen)
|
||||
}
|
||||
|
||||
isAscii = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
LenSplit formats string `s` to break at, at most, every `width` characters.
|
||||
|
||||
@@ -252,6 +453,18 @@ func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted
|
||||
return
|
||||
}
|
||||
|
||||
// Reverse reverses string s. (It's absolutely insane that this isn't in stdlib.)
|
||||
func Reverse(s string) (revS string) {
|
||||
|
||||
var rsl []rune = []rune(s)
|
||||
|
||||
slices.Reverse(rsl)
|
||||
|
||||
revS = string(rsl)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
|
||||
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
|
||||
@@ -313,6 +526,58 @@ func TrimSpaceRight(s string) (trimmed string) {
|
||||
return
|
||||
}
|
||||
|
||||
// getAsciiCharMap returns a lookup "table" for ASCII characters.
|
||||
func getAsciiCharMap(allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (charmap [256]bool) {
|
||||
|
||||
var idx uint8
|
||||
|
||||
if allowCtl {
|
||||
for idx < 0x1f {
|
||||
charmap[idx] = true
|
||||
idx++
|
||||
}
|
||||
} else {
|
||||
idx = 0x1f
|
||||
}
|
||||
if allowPrint {
|
||||
for idx < 0x7f {
|
||||
charmap[idx] = true
|
||||
idx++
|
||||
}
|
||||
} else {
|
||||
idx = 0x7f
|
||||
}
|
||||
if allowExt {
|
||||
for {
|
||||
charmap[idx] = true
|
||||
if idx == 0xff {
|
||||
break
|
||||
}
|
||||
idx++
|
||||
}
|
||||
} else {
|
||||
idx = 0xff
|
||||
}
|
||||
if allowWs {
|
||||
charmap['\t'] = true
|
||||
charmap['\n'] = true
|
||||
charmap['\r'] = true
|
||||
}
|
||||
|
||||
if incl != nil && len(incl) > 0 {
|
||||
for _, idx = range incl {
|
||||
charmap[idx] = true
|
||||
}
|
||||
}
|
||||
if excl != nil && len(excl) > 0 {
|
||||
for _, idx = range excl {
|
||||
charmap[idx] = false
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getNewLine is too unpredictable/nuanced to be used as part of a public API promise so it isn't exported.
|
||||
func getNewLine(s string) (nl string) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
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) {
|
||||
|
||||
var out string
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
+1331
-666
File diff suppressed because it is too large
Load Diff
+2196
-620
File diff suppressed because it is too large
Load Diff
+3572
-1079
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
- base64, base16, base32, hex, pem encoding/decoding tpl funcs
|
||||
-- template hashing functions?
|
||||
|
||||
- os env vars
|
||||
@@ -0,0 +1,5 @@
|
||||
[source,subs="+attributes,+post_replacements",opts="novalidate"]
|
||||
.Function Signature
|
||||
----
|
||||
func {func}{sig}
|
||||
----
|
||||
+97
-1
@@ -1,6 +1,8 @@
|
||||
package sprigx
|
||||
|
||||
import (
|
||||
`net`
|
||||
`net/netip`
|
||||
`os`
|
||||
`os/user`
|
||||
`path`
|
||||
@@ -17,6 +19,10 @@ import (
|
||||
psnet `github.com/shirou/gopsutil/v4/net`
|
||||
`github.com/shirou/gopsutil/v4/process`
|
||||
`github.com/shirou/gopsutil/v4/sensors`
|
||||
`go4.org/netipx`
|
||||
`r00t2.io/goutils/netx`
|
||||
`r00t2.io/goutils/netx/dnsx`
|
||||
`r00t2.io/goutils/stringsx`
|
||||
`r00t2.io/goutils/timex`
|
||||
`r00t2.io/sysutils`
|
||||
)
|
||||
@@ -30,6 +36,81 @@ var (
|
||||
"Meta"/Template-Helpers
|
||||
*/
|
||||
"metaIsNil": metaIsNil,
|
||||
/*
|
||||
Networking (net)
|
||||
*/
|
||||
"netCidrMask": net.CIDRMask,
|
||||
"netExtractAddr": netExtractAddr,
|
||||
"netExtractHost": netExtractHost,
|
||||
"netExtractIpnet": netExtractIpnet,
|
||||
"netExtractPort": netExtractPort,
|
||||
"netIfaces": net.Interfaces,
|
||||
"netIp4Mask": netIp4Mask,
|
||||
"netJoinHostPort": net.JoinHostPort,
|
||||
"netParseIP": net.ParseIP,
|
||||
/*
|
||||
Networking (net/netip)
|
||||
*/
|
||||
"netipAddrPort": netip.AddrPortFrom,
|
||||
"netipParseAddr": netip.ParseAddr,
|
||||
"netipParseAddrPort": netip.ParseAddrPort,
|
||||
"netipParsePrefix": netip.ParsePrefix,
|
||||
"netipPrefix": netip.PrefixFrom,
|
||||
/*
|
||||
Networking (go4.org/netipx)
|
||||
*/
|
||||
"netipxAddrIpNet": netipx.AddrIPNet,
|
||||
"netipxCmpPfx": netipx.ComparePrefix,
|
||||
"netipxFromStdAddr": netipxFromStdAddr,
|
||||
"netipxFromIp": netipxFromIp,
|
||||
"netipxFromIpNet": netipxFromIpNet,
|
||||
"netipxParseRange": netipx.ParseIPRange,
|
||||
"netipxPfxAddr": netipx.ParsePrefixOrAddr,
|
||||
"netipxPfxIpNet": netipx.PrefixIPNet,
|
||||
"netipxPfxLast": netipx.PrefixLastIP,
|
||||
"netipxPfxRange": netipx.RangeOfPrefix,
|
||||
"netipxRange": netipx.IPRangeFrom,
|
||||
/*
|
||||
Networking (r00t.io/goutils/netx)
|
||||
*/
|
||||
"netxAddrRfc": netx.AddrRfc,
|
||||
"netxCidr4IpMask": netx.Cidr4ToIPMask,
|
||||
"netxCidr4Mask": netx.Cidr4ToMask,
|
||||
"netxCidr4Str": netx.Cidr4ToStr,
|
||||
"netxFamilyVer": netx.FamilyToVer,
|
||||
"netxGetAddrFam": netx.GetAddrFamily,
|
||||
"netxGetIpFam": netx.GetIpFamily,
|
||||
"netxIpRfc": netx.IpRfc,
|
||||
"netxIpRfcStr": netx.IpRfcStr,
|
||||
"netxIpStripRfc": netx.IpStripRfcStr,
|
||||
"netxIp4MaskCidr": netx.IPMask4ToCidr,
|
||||
"netxIp4MaskMask": netx.IPMask4ToMask,
|
||||
"netxIp4MaskStr": netx.IPMask4ToStr,
|
||||
"netxIpVerStr": netx.IpVerStr,
|
||||
"netxIsBrktd6": netx.IsBracketedIp6,
|
||||
"netxIsIp": netx.IsIpAddr,
|
||||
"netxIsPfx": netx.IsPrefixNet,
|
||||
"netxMask4Cidr": netx.Mask4ToCidr,
|
||||
"netxMask4StrCidr": netx.Mask4StrToCidr,
|
||||
"netxMask4StrIpMask": netx.Mask4StrToIPMask,
|
||||
"netxMask4StrMask": netx.Mask4StrToMask,
|
||||
"netxVerFamily": netx.VerToFamily,
|
||||
/*
|
||||
Networking (r00t.io/goutils/netx/dnsx)
|
||||
*/
|
||||
"dnsxPtrAddr": dnsx.AddrFromPtr,
|
||||
"dnsxAddrPtr": dnsx.AddrToPtr,
|
||||
"dnsxStrWire": dnsx.DnsStrToWire,
|
||||
"dnsxWireStr": dnsx.DnsWireToStr,
|
||||
"dnsxPtrIp": dnsx.IpFromPtr,
|
||||
"dnsxIpPtr": dnsx.IpToPtr,
|
||||
"dnsxIsFqdn": dnsx.IsFqdn,
|
||||
"dnsxIsTxt": dnsx.IsFqdnDefinedTxt,
|
||||
"dnsxIsNsec3": dnsx.IsFqdnNsec3,
|
||||
"dnsxIsSrv": dnsx.IsFqdnSrv,
|
||||
"dnsxIsWild": dnsx.IsFqdnWildcard,
|
||||
"dnsxIsLbl": dnsx.IsLabel,
|
||||
"dnsxIsPtr": dnsx.IsPtr,
|
||||
/*
|
||||
Numbers/Math
|
||||
*/
|
||||
@@ -118,9 +199,24 @@ var (
|
||||
// .../sensors
|
||||
"psSensorTemps": sensors.SensorsTemperatures,
|
||||
/*
|
||||
Strings
|
||||
Strings (Standalone)
|
||||
*/
|
||||
"extIndent": extIndent, // PR in: https://github.com/Masterminds/sprig/pull/468
|
||||
/*
|
||||
Strings (r00t.io/goutils/stringsx)
|
||||
*/
|
||||
"strsxIsAscii": stringsx.IsAscii,
|
||||
"strsxIsAsciiBuf": stringsx.IsAsciiBuf,
|
||||
"strsxIsAsciiSpcl": stringsx.IsAsciiSpecial,
|
||||
"strsxIsAsciiBufSpcl": stringsx.IsAsciiBufSpecial,
|
||||
"strsxLenSpl": stringsx.LenSplit,
|
||||
"strsxLenSplStr": stringsx.LenSplitStr,
|
||||
"strsxPad": stringsx.Pad,
|
||||
"strsxRedact": stringsx.Redact,
|
||||
"strsxRev": stringsx.Reverse,
|
||||
"strsxTrimLns": stringsx.TrimLines,
|
||||
"strsxTrimSpcLft": stringsx.TrimSpaceLeft,
|
||||
"strsxTrimSpcRt": stringsx.TrimSpaceRight,
|
||||
/*
|
||||
System/Platform
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadAddr error = errors.New("invalid/bad address")
|
||||
ErrBadAddrPort error = errors.New("invalid/bad address/port")
|
||||
ErrBadMonth error = errors.New("could not determine/parse month")
|
||||
ErrBadNet error = errors.New("invalid/bad network")
|
||||
ErrOverflow error = errors.New("integer/buffer overflow")
|
||||
ErrBadType error = errors.New("an invalid/unknown type was passed")
|
||||
ErrNilVal error = errors.New("a nil value was passed")
|
||||
ErrUnderflow error = errors.New("integer/buffer underflow")
|
||||
)
|
||||
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func numFloat64Str(f float64) (s string) {
|
||||
var bf *big.Float
|
||||
|
||||
bf = big.NewFloat(f)
|
||||
s = bf.String()
|
||||
s = bf.Text('f', -1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package uuidx
|
||||
|
||||
const (
|
||||
RfcNone RfcGen = iota
|
||||
Rfc4122
|
||||
Rfc9562
|
||||
)
|
||||
|
||||
const (
|
||||
MsGuidThreshold int = 4
|
||||
)
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package uuidx
|
||||
|
||||
type (
|
||||
RfcGen uint8
|
||||
)
|
||||
Reference in New Issue
Block a user