Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ec35990a9
|
||
|
|
67a5c8a3fb
|
||
|
|
cd9b967306
|
||
|
|
f002766c74
|
||
|
|
67bd30907e
|
||
|
|
4e7d474fb3
|
||
|
|
cbfaaddf34
|
||
|
|
5108b09df5
|
||
|
|
72af113204
|
||
|
|
c9f3e7a639
|
||
|
|
42c0e48081
|
||
|
|
58556d7281
|
||
|
|
c6fc692f5e
|
||
|
|
4770052b52
|
||
|
|
1eea0c2672
|
||
|
|
67c7faf449
|
||
|
|
82c69ec542
|
||
|
|
07e0e587fa
|
||
|
|
1bd6e1256c
|
||
|
|
64a7648fbc
|
||
|
|
9cce861b2e
|
||
|
|
927ad08057
|
||
|
|
2edbc9306d
|
||
|
|
bb71be187f
|
||
|
|
834395c050
|
||
|
|
ef56898d6b
|
||
|
|
006cf39fa1
|
||
|
|
145c32268e
|
||
|
|
6ddfcdb416
|
||
|
|
79f10b7611
|
||
|
|
01adbfc605
|
||
|
|
b1d8ea34a6
|
||
|
|
e101758187
|
||
|
|
3c49a5b70a
|
||
|
|
965657d1b2
|
||
|
|
970acd0ee4
|
||
|
|
2222cea7fb
|
||
|
|
688abd0874
|
||
|
|
a1f87d6b51
|
||
|
|
07951f1f03
|
||
|
|
bae0abe960
|
||
|
|
368ae0cb8e
|
||
|
|
154170c0e5
|
||
|
|
d9bd928edb
|
||
|
|
dc2ed32352
|
||
|
|
e734e847c4
|
||
|
|
2203de4e32
|
||
|
|
a0c6df14aa
|
||
|
|
fd720f2b34
|
||
|
|
3c543a05e7
|
||
|
|
e5191383a7
|
||
|
|
ae49f42c0c
|
||
|
|
b87934e8a9
|
||
|
|
70d6c2cbb3
|
||
|
|
a445a51c0d
|
||
|
|
a2a849600b
|
||
|
|
94145fb4c7
|
||
|
|
81a2d308f0
|
||
|
|
c4b3c6441a
|
||
|
|
1c5abd4083
|
No files matched your search
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Package bit aims to provide feature parity with stdlib's [encoding/hex].
|
||||||
|
|
||||||
|
It's a ludicrous tragedy that hex/base16, base32, base64 all have libraries for converting
|
||||||
|
to/from string representations... but there's nothing for binary ('01010001' etc.) whatsoever.
|
||||||
|
|
||||||
|
This package also provides some extra convenience functions and types in an attempt to provide
|
||||||
|
an abstracted bit-level fidelity in Go. A [Bit] is a bool type, in which that underlying bool
|
||||||
|
being false represents a 0 and that underlying bool being true represents a 1.
|
||||||
|
|
||||||
|
Note that a [Bit] or arbitrary-length or non-octal-aligned [][Bit] may take up more bytes in memory
|
||||||
|
than expected; a [Bit] will actually always occupy a single byte -- thus representing
|
||||||
|
`00000000 00000000` as a [][Bit] or [16][Bit] will actually occupy *sixteen bytes* in memory,
|
||||||
|
NOT 2 bytes (nor, obviously, [2][Byte])!
|
||||||
|
It is recommended instead to use a [Bits] instead of a [Bit] slice or array, as it will try to properly align to the
|
||||||
|
smallest memory allocation possible (at the cost of a few extra CPU cycles on adding/removing one or more [Bit]).
|
||||||
|
It will properly retain any appended, prepended, leading, or trailing bits that do not currently align to a byte.
|
||||||
|
*/
|
||||||
|
package bit
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package bit
|
||||||
|
|
||||||
|
// TODO: Provide analogues of encoding/hex, encoding/base64, etc. functions etc.
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: Also provide interfaces for the following:
|
||||||
|
|
||||||
|
* https://pkg.go.dev/encoding#BinaryAppender
|
||||||
|
* https://pkg.go.dev/encoding#BinaryMarshaler
|
||||||
|
* https://pkg.go.dev/encoding#BinaryUnmarshaler
|
||||||
|
* https://pkg.go.dev/encoding#TextAppender
|
||||||
|
* https://pkg.go.dev/encoding#TextMarshaler
|
||||||
|
* https://pkg.go.dev/encoding#TextUnmarshaler
|
||||||
|
*/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package bit
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Bit aims to provide a native-like type for a single bit (Golang operates on the smallest fidelity level of *byte*/uint8).
|
||||||
|
Bit bool
|
||||||
|
|
||||||
|
// Bits is an arbitrary length of bits.
|
||||||
|
Bits struct {
|
||||||
|
/*
|
||||||
|
leading is a series of Bit that do not cleanly align to the beginning of Bits.b.
|
||||||
|
They will always be the bits at the *beginning* of the sequence.
|
||||||
|
len(Bits.leading) will *never* be more than 7;
|
||||||
|
it's converted into a byte, prepended to Bits.b, and cleared if it reaches that point.
|
||||||
|
*/
|
||||||
|
leading []Bit
|
||||||
|
// b is the condensed/memory-aligned alternative to an [][8]Bit (or []Bit, or [][]Bit, etc.).
|
||||||
|
b []byte
|
||||||
|
/*
|
||||||
|
remaining is a series of Bit that do not cleanly align to the end of Bits.b.
|
||||||
|
They will always be the bits at the *end* of the sequence.
|
||||||
|
len(Bits.remaining) will *never* be more than 7;
|
||||||
|
it's converted into a byte, appended to Bits.b, and cleared if it reaches that point.
|
||||||
|
*/
|
||||||
|
remaining []Bit
|
||||||
|
// fixedLen, if 0, represents a "slice". If >= 1, it represents an "array".
|
||||||
|
fixedLen uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte is this package's representation of a byte. It's primarily for convenience.
|
||||||
|
Byte byte
|
||||||
|
|
||||||
|
// Bytes is defined as a type for convenience single-call functions.
|
||||||
|
Bytes []Byte
|
||||||
|
)
|
||||||
Executable
+85
@@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
orig="${PWD}"
|
||||||
|
|
||||||
|
if ! command -v asciidoctor &> /dev/null;
|
||||||
|
then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
for f in $(find . -type f -iname "README.adoc"); do
|
||||||
|
filename=$(basename -- "${f}")
|
||||||
|
docsdir=$(dirname -- "${f}")
|
||||||
|
nosuffix="${filename%.*}"
|
||||||
|
pfx="${docsdir}/${nosuffix}"
|
||||||
|
|
||||||
|
# Render HTML, include in commit
|
||||||
|
newf="${pfx}.html"
|
||||||
|
asciidoctor -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
|
||||||
|
echo "Generated ${newf} from ${f}"
|
||||||
|
git add "${newf}"
|
||||||
|
|
||||||
|
# If asciidoctor-pdf is installed, render as PDF for local use
|
||||||
|
# (Does not get added to commit, and *.pdf is in .gitignore for a reason)
|
||||||
|
if command -v asciidoctor-pdf &> /dev/null;
|
||||||
|
then
|
||||||
|
newf="${pfx}.pdf"
|
||||||
|
|
||||||
|
asciidoctor-pdf -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If pandoc is installed, render to "GitHub-flavored Markdown" for better rendering on forks/mirrors
|
||||||
|
# and marginally better rendering on https://pkg.go.dev/ and add to commit.
|
||||||
|
#
|
||||||
|
# <rant>
|
||||||
|
# There is no such thing as "Markdown".
|
||||||
|
# The closest thing you have to any sort of standard is https://daringfireball.net/projects/markdown/
|
||||||
|
# but everybody and their mother adds their own "extensions"/"flavor", and sometimes even
|
||||||
|
# change how formatting works compared to the Daring Fireball/John Gruber spec (the original creator of the "syntax").
|
||||||
|
# Ergo "Markdown" inherently has no meaning.
|
||||||
|
# It's one of the worst formatting languages out there - just because it's popular doesn't mean it's good.
|
||||||
|
#
|
||||||
|
# If you're writing docs, you should stick to one of these which have defined, canonical, standardized
|
||||||
|
# syntax:
|
||||||
|
# * AsciiDoc/AsciiDoctor
|
||||||
|
# * Supports much more extensive formatting than any Markdown flavor I've seen
|
||||||
|
# * Source/raw/unrendered still *quite* readable by human eyes
|
||||||
|
# * Somewhat limited parsers/renderers
|
||||||
|
# * https://asciidoc.org/
|
||||||
|
# * https://asciidoctor.org/
|
||||||
|
# * DocBook
|
||||||
|
# * Supports even more extensive and flexible but exact formatting
|
||||||
|
# * Great for publishing, though - especially if you need control over formatting/layout
|
||||||
|
# * XML-based
|
||||||
|
# * Harder to read in plaintext, but fairly doable (XML lends to decent mental rendering)
|
||||||
|
# * Very wide support for parsing/rendering
|
||||||
|
# * https://docbook.org/
|
||||||
|
# * LaTex
|
||||||
|
# * Allows for *very* extensive domain-specific ligature/representation (very common in mathematic/scientific literature)
|
||||||
|
# * But nigh unreadable by human eyes unless you're rather familiar with it
|
||||||
|
# * Parsing/rendering support about on-par with DocBook
|
||||||
|
# * https://www.latex-project.org/
|
||||||
|
# </rant>
|
||||||
|
if command -v pandoc &> /dev/null;
|
||||||
|
then
|
||||||
|
newf="${pfx}.md"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
# --wrap=preserve or --wrap=none is required to avoid it reflowing the section titles to newlines.
|
||||||
|
#asciidoctor -a ROOTDIR="${orig}/" -b docbook -o - "${f}" | pandoc -f docbook -t markdown_strict -o "${newf}" --wrap=none
|
||||||
|
asciidoctor -a ROOTDIR="${orig}/" -b html -o - "${f}" | pandoc -f html -t gfm -o "${newf}" --wrap=none
|
||||||
|
if [ $? -eq 0 ];
|
||||||
|
then
|
||||||
|
echo "Generated ${newf} from ${f}"
|
||||||
|
git add "${newf}"
|
||||||
|
else
|
||||||
|
echo "Failed to generate ${newf} from ${f}"
|
||||||
|
git rm "${newf}" 2>/dev/null
|
||||||
|
fi
|
||||||
|
set -e
|
||||||
|
fi
|
||||||
|
cd ${orig}
|
||||||
|
done
|
||||||
|
echo "Regenerated docs"
|
||||||
+2
-1
@@ -19,12 +19,13 @@
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# https://github.com/github/gitignore/blob/master/Go.gitignore
|
# https://github.com/github/gitignore/blob/master/Go.gitignore
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins and other data
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
*.pdf
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- validx: validator functions for https://pkg.go.dev/github.com/go-playground/validator/v10
|
||||||
|
|
||||||
|
- hashx?
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package bitmask
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/bits"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaskBit is a flag container.
|
||||||
|
type MaskBit uint
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewMaskBit is a convenience function.
|
||||||
|
It will return a MaskBit with a (referenced) value of 0, so set your consts up accordingly.
|
||||||
|
|
||||||
|
It is highly recommended to set this default as a "None" flag (separate from your iotas!)
|
||||||
|
as shown in the example.
|
||||||
|
*/
|
||||||
|
func NewMaskBit() (m *MaskBit) {
|
||||||
|
|
||||||
|
m = new(MaskBit)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMaskBitExplicit is like NewMaskBit, but allows you to specify a non-zero (0x0) value.
|
||||||
|
func NewMaskBitExplicit(value uint) (m *MaskBit) {
|
||||||
|
|
||||||
|
var v MaskBit = MaskBit(value)
|
||||||
|
|
||||||
|
m = &v
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
HasFlag is true if m has MaskBit flag set/enabled.
|
||||||
|
|
||||||
|
See the "Composite (OR'd) Flags" section in this module's
|
||||||
|
documentation for important caveats.
|
||||||
|
*/
|
||||||
|
func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {
|
||||||
|
|
||||||
|
var b MaskBit = *m
|
||||||
|
|
||||||
|
if b&flag == flag {
|
||||||
|
r = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
HasOneOf is like a "looser" form of [MaskBit.HasFlag]
|
||||||
|
in that it allows for testing composite membership.
|
||||||
|
|
||||||
|
See [MaskBit.HasFlag] and the "Composite (OR'd) Flags"
|
||||||
|
section in this module's documentation for more details.
|
||||||
|
|
||||||
|
If composite is *not* an OR'd MaskBit (i.e.
|
||||||
|
it falls directly on a boundary -- 0, 1, 2, 4, 8, 16, etc.),
|
||||||
|
then HasOneOf will behave exactly like [MaskBit.HasFlag].
|
||||||
|
|
||||||
|
If m is a composite [MaskBit] (it usually is) and composite is ALSO a composite MaskBit,
|
||||||
|
HasOneOf will return true if ANY of the flags set in composite is set in m.
|
||||||
|
*/
|
||||||
|
func (m *MaskBit) HasOneOf(composite MaskBit) (r bool) {
|
||||||
|
|
||||||
|
var b MaskBit = *m
|
||||||
|
|
||||||
|
if b&composite != 0 {
|
||||||
|
r = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IsOneOf is the old name for [MaskBit.HasOneOf].
|
||||||
|
|
||||||
|
DEPRECATED: This method will be removed sometime in the future. Use [MaskBit.HasOneOf] instead.
|
||||||
|
*/
|
||||||
|
func (m *MaskBit) IsOneOf(composite MaskBit) (r bool) { return m.HasOneOf(composite) }
|
||||||
|
|
||||||
|
// AddFlag adds MaskBit flag to m.
|
||||||
|
func (m *MaskBit) AddFlag(flag MaskBit) {
|
||||||
|
|
||||||
|
*m |= flag
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearFlag removes MaskBit flag from m.
|
||||||
|
func (m *MaskBit) ClearFlag(flag MaskBit) {
|
||||||
|
|
||||||
|
*m &^= flag
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToggleFlag switches MaskBit flag in m to its inverse; if true, it is now false and vice versa.
|
||||||
|
func (m *MaskBit) ToggleFlag(flag MaskBit) {
|
||||||
|
|
||||||
|
*m ^= flag
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Bytes returns the current value of a MasBit as a byte slice (big-endian).
|
||||||
|
|
||||||
|
If trim is false, b will (probably) be 4 bytes long if you're on a 32-bit size system,
|
||||||
|
and b will (probably) be 8 bytes long if you're on a 64-bit size system. You can determine
|
||||||
|
the size of the resulting slice via (math/)bits.UintSize / 8.
|
||||||
|
|
||||||
|
If trim is true, it will trim leading null bytes (if any). This will lead to an unpredictable
|
||||||
|
byte slice length in b, but is most likely preferred for byte operations.
|
||||||
|
*/
|
||||||
|
func (m *MaskBit) Bytes(trim bool) (b []byte) {
|
||||||
|
|
||||||
|
var b2 []byte
|
||||||
|
var size int = bits.UintSize / 8
|
||||||
|
var err error
|
||||||
|
|
||||||
|
b2 = make([]byte, size)
|
||||||
|
|
||||||
|
switch s := bits.UintSize; s {
|
||||||
|
case 32:
|
||||||
|
binary.BigEndian.PutUint32(b2[:], uint32(*m))
|
||||||
|
case 64:
|
||||||
|
binary.BigEndian.PutUint64(b2[:], uint64(*m))
|
||||||
|
default:
|
||||||
|
err = errors.New("unsupported Uint/system bit size")
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if trim {
|
||||||
|
b = bytes.TrimLeft(b2, "\x00")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
b = b2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a pointer to a (new) copy of a MaskBit.
|
||||||
|
func (m *MaskBit) Copy() (newM *MaskBit) {
|
||||||
|
|
||||||
|
newM = new(MaskBit)
|
||||||
|
*newM = *m
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the current raw uint value of a MaskBit.
|
||||||
|
func (m *MaskBit) Value() (v uint) {
|
||||||
|
|
||||||
|
v = uint(*m)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package bitmask
|
|
||||||
|
|
||||||
// MaskBit is a flag container.
|
|
||||||
type MaskBit uint8
|
|
||||||
|
|
||||||
/*
|
|
||||||
NewMaskBit is a convenience function.
|
|
||||||
It will return a MaskBit with a (referenced) value of 0, so set your consts up accordingly.
|
|
||||||
It is highly recommended to set this default as a "None" flag (separate from your iotas!)
|
|
||||||
as shown in the example.
|
|
||||||
*/
|
|
||||||
func NewMaskBit() (m *MaskBit) {
|
|
||||||
|
|
||||||
m = new(MaskBit)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasFlag is true if m has MaskBit flag set/enabled.
|
|
||||||
func (m *MaskBit) HasFlag(flag MaskBit) (r bool) {
|
|
||||||
|
|
||||||
var b MaskBit = *m
|
|
||||||
|
|
||||||
if b&flag != 0 {
|
|
||||||
r = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddFlag adds MaskBit flag to m.
|
|
||||||
func (m *MaskBit) AddFlag(flag MaskBit) {
|
|
||||||
*m |= flag
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearFlag removes MaskBit flag from m.
|
|
||||||
func (m *MaskBit) ClearFlag(flag MaskBit) {
|
|
||||||
*m &= flag
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToggleFlag switches MaskBit flag in m to its inverse; if true, it is now false and vice versa.
|
|
||||||
func (m *MaskBit) ToggleFlag(flag MaskBit) {
|
|
||||||
*m ^= flag
|
|
||||||
return
|
|
||||||
}
|
|
||||||
+203
-13
@@ -1,45 +1,235 @@
|
|||||||
/*
|
/*
|
||||||
Package bitmask handles a flag-like opt/bitmask system.
|
Package bitmask handles a flag-like opt/bitmask system.
|
||||||
|
|
||||||
See https://yourbasic.org/golang/bitmask-flag-set-clear/ for more information.
|
See https://yourbasic.org/golang/bitmask-flag-set-clear/ for basic information on what bitmasks are and why they're useful.
|
||||||
|
|
||||||
To use this, set constants like thus:
|
Specifically, in the case of Go, they allow you to essentially manage many, many, many "booleans" as part of a single value.
|
||||||
|
|
||||||
|
A single bool value in Go takes up 8 bits/1 byte, unavoidably.
|
||||||
|
|
||||||
|
However, a [bitmask.MaskBit] is backed by a uint which (depending on your platform) is either 32 bits/4 bytes or 64 bits/8 bytes.
|
||||||
|
|
||||||
|
"But wait, that takes up more memory though!"
|
||||||
|
|
||||||
|
Yep - compared to a *single boolean*. But bitmasking lets you store a "boolean" AT EACH BIT boundary - it operates on
|
||||||
|
whether a bit in a byte/set of bytes at a given position is 0 or 1.
|
||||||
|
|
||||||
|
In other words:
|
||||||
|
|
||||||
|
type (
|
||||||
|
OptStruct struct {
|
||||||
|
A bool
|
||||||
|
B bool
|
||||||
|
C bool
|
||||||
|
D bool
|
||||||
|
E bool
|
||||||
|
F bool
|
||||||
|
G bool
|
||||||
|
H bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
One instance of OptStruct takes up *8 bytes* (*64 bits*).
|
||||||
|
|
||||||
|
As a uint8 bitmask, however, it takes up exactly *one byte/8 bits*:
|
||||||
|
|
||||||
|
type (
|
||||||
|
Opt uint8
|
||||||
|
)
|
||||||
|
|
||||||
|
const OptNone Opt = 0
|
||||||
|
const (
|
||||||
|
A Opt = 1 << iota // 1
|
||||||
|
B // 2
|
||||||
|
C // 4
|
||||||
|
D // 8
|
||||||
|
E // 16
|
||||||
|
F // 32
|
||||||
|
G // 64
|
||||||
|
H // 128
|
||||||
|
// Would overflow to 0:
|
||||||
|
// I
|
||||||
|
)
|
||||||
|
|
||||||
|
As shown, the size of an unsigned integer determines the number of bit-boundaried flags for that bitmask.
|
||||||
|
|
||||||
|
Which means on 32-bit platforms, a [MaskBit] can have up to 32 different flags but only occupies 4 bytes of memory.
|
||||||
|
|
||||||
|
On 64-bit platforms, a [MaskBit] can have up to 64 different flags but only occupies 8 bytes of memory.
|
||||||
|
|
||||||
|
If you tried to do that with a struct of booleans instead, that'd occupy 32 *bytes* and 64 *bytes* respectively.
|
||||||
|
|
||||||
|
In summary, a bitmask set occupies 1/8th the amount of memory as a set of equal number of booleans in Go.
|
||||||
|
|
||||||
|
You can, of course, extend this even further by defining meaningful "composites",
|
||||||
|
or OR'd-combination of flags (see the "Composite Flags" section below):
|
||||||
|
|
||||||
|
type Perms uint8
|
||||||
|
const NoPerms Perms = 0
|
||||||
|
const (
|
||||||
|
PermsRead Perms = 1 << iota
|
||||||
|
PermsList
|
||||||
|
PermsUpdate
|
||||||
|
PermsCreate
|
||||||
|
PermsDelete
|
||||||
|
PermsUpdateBulk
|
||||||
|
PermsCreateBulk
|
||||||
|
PermsDeleteBulk
|
||||||
|
|
||||||
|
// This makes it easy to add multiple permissions at once.
|
||||||
|
PermsCRUD Perms = PermsCreate | PermsRead | PermsUpdate | PermsDelete
|
||||||
|
PermsCRUDL Perms = PermsCRUD | PermsList
|
||||||
|
// And so forth.
|
||||||
|
)
|
||||||
|
|
||||||
|
"But that has to be so slow to unpack that!"
|
||||||
|
|
||||||
|
Nope. It's not using compression or anything, the CPU is just comparing bit "A" vs. bit "B" 32/64 times.
|
||||||
|
That's super easy work for a CPU.
|
||||||
|
|
||||||
|
There's a reason ZDoom used bitmasking for the [dmflags] value in its server configs,
|
||||||
|
and why *NIX platforms uses them for permission/type modes.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
To use this library, set constants like thus:
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"r00t2.io/goutils/bitmask"
|
"r00t2.io/goutils/bitmask"
|
||||||
)
|
)
|
||||||
|
|
||||||
const OPTNONE types.MaskBit = 0
|
const OPTNONE bitmask.MaskBit = 0
|
||||||
const (
|
const (
|
||||||
OPT1 types.MaskBit = 1 << iota
|
OPT1 bitmask.MaskBit = 1 << iota
|
||||||
OPT2
|
OPT2
|
||||||
OPT3
|
OPT3
|
||||||
// ...
|
// ...
|
||||||
)
|
)
|
||||||
|
|
||||||
var MyMask *MaskBit
|
var (
|
||||||
|
MyMask *bitmask.MaskBit = bitmask.NewMaskBit()
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
MyMask = types.NewMaskBit
|
|
||||||
|
|
||||||
MyMask.AddFlag(OPT1)
|
MyMask.AddFlag(OPT1)
|
||||||
MyMask.AddFlag(OPT3)
|
MyMask.AddFlag(OPT3)
|
||||||
|
|
||||||
_ = MyMask
|
// This would print true.
|
||||||
|
fmt.Println(MyMask.HasFlag(OPT1))
|
||||||
|
|
||||||
|
// As would this:
|
||||||
|
fmt.Println(MyMask.HasFlag(OPT3))
|
||||||
|
|
||||||
|
// But this would print false:
|
||||||
|
fmt.Println(MyMask.HasFlag(OPT2))
|
||||||
}
|
}
|
||||||
|
|
||||||
This would return true:
|
# Technical Caveats
|
||||||
|
|
||||||
MyMask.HasFlag(OPT1)
|
Targeting
|
||||||
|
|
||||||
As would this:
|
When implementing, you should always set a "source" mask (e.g. MyMask, from Usage section above)
|
||||||
|
as the actual value.
|
||||||
|
|
||||||
MyMask.HasFlag(OPT3)
|
For example, if you are checking a permissions set for a user that has the value, say, 6
|
||||||
|
|
||||||
But this would return false:
|
var userPerms uint = 6 // 0x0000000000000006
|
||||||
|
|
||||||
MyMask.HasFlag(OPT2)
|
and your library has the following permission bits defined:
|
||||||
|
|
||||||
|
const PermsNone bitmask.MaskBit = 0
|
||||||
|
const (
|
||||||
|
PermsList bitmask.MaskBit = 1 << iota // 1
|
||||||
|
PermsRead // 2
|
||||||
|
PermsWrite // 4
|
||||||
|
PermsExec // 8
|
||||||
|
PermsAdmin // 16
|
||||||
|
)
|
||||||
|
|
||||||
|
and you want to see if the user has the PermsRead flag set, you would do:
|
||||||
|
|
||||||
|
userPermMask = bitmask.NewMaskBitExplicit(userPerms)
|
||||||
|
if userPermMask.HasFlag(PermsRead) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
NOT:
|
||||||
|
|
||||||
|
userPermMask = bitmask.NewMaskBitExplicit(PermsRead)
|
||||||
|
userPermMask.HasFlag(bitmask.MaskBit(userPerms))
|
||||||
|
|
||||||
|
NOR:
|
||||||
|
userPermMask = PermsRead
|
||||||
|
if userPermMask.HasFlag(bitmask.MaskBit(userPerms)) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
This will be terribly, horribly wrong, cause incredibly unexpected results,
|
||||||
|
and quite possibly cause massive security issues. Don't do it.
|
||||||
|
|
||||||
|
Remember, [MaskBit.HasFlag] (and other methods) are for comparing/modifying an
|
||||||
|
*authoritative/concrete value* with one or more *attributes*, not the other way around.
|
||||||
|
|
||||||
|
Composite Flags
|
||||||
|
|
||||||
|
If you want to define a set of flags that are a combination
|
||||||
|
(a "composite", or OR'd set) of other flags, your inclination
|
||||||
|
would be to bitwise-OR them together:
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagA bitmask.MaskBit = 1 << iota // 1
|
||||||
|
flagB // 2
|
||||||
|
flagC // 4
|
||||||
|
// ...
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagAB bitmask.MaskBit = flagA | flagB // 3
|
||||||
|
)
|
||||||
|
|
||||||
|
Which is fine, and the correct approach.
|
||||||
|
But if you then have:
|
||||||
|
|
||||||
|
var myMask *bitmask.MaskBit = bitmask.NewMaskBit()
|
||||||
|
|
||||||
|
myMask.AddFlag(flagA)
|
||||||
|
|
||||||
|
you may expect this call to [MaskBit.HasFlag]:
|
||||||
|
|
||||||
|
myMask.HasFlag(flagAB)
|
||||||
|
|
||||||
|
to be true, since flagA is "in" flagAB.
|
||||||
|
|
||||||
|
It will return false - HasFlag does strict comparisons.
|
||||||
|
|
||||||
|
It would only return true if you did:
|
||||||
|
|
||||||
|
// ...
|
||||||
|
myMask.AddFlag(flagAB)
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
// ...
|
||||||
|
myMask.AddFlag(flagA)
|
||||||
|
myMask.AddFlag(flagB)
|
||||||
|
|
||||||
|
Instead, if you want to see if a mask has *at least one flag of* a composite flag,
|
||||||
|
you can use [MaskBit.HasOneOf].
|
||||||
|
|
||||||
|
# Other Options
|
||||||
|
|
||||||
|
If you need something with more flexibility (as always, at the cost of complexity),
|
||||||
|
you may be interested in one of the following libraries:
|
||||||
|
|
||||||
|
* [github.com/alvaroloes/enumer]
|
||||||
|
* [github.com/abice/go-enum]
|
||||||
|
* [github.com/jeffreyrichter/enum/enum]
|
||||||
|
|
||||||
|
[dmflags]: https://doomwiki.org/wiki/DMFlags
|
||||||
*/
|
*/
|
||||||
package bitmask
|
package bitmask
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
Package bytesx aims to extend functionality of the stdlib [bytes] module.
|
||||||
|
|
||||||
|
TODO.
|
||||||
|
*/
|
||||||
|
package bytesx
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# go tool dist list for all valid GOOS/GOARCH targets.
|
||||||
|
|
||||||
|
for tgt in $(go tool dist list);
|
||||||
|
do
|
||||||
|
o="$(echo ${tgt} | cut -f1 -d '/')"
|
||||||
|
a="$(echo ${tgt} | cut -f2 -d '/')"
|
||||||
|
out="$(env GOOS=${o} GOARCH=${a} go build ./... 2>&1)"
|
||||||
|
ret=${?}
|
||||||
|
if [ $ret -ne 0 ];
|
||||||
|
then
|
||||||
|
echo "OS: ${o}"
|
||||||
|
echo "ARCH: ${a}"
|
||||||
|
echo "${out}"
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
I need to test this still; I can't recall if it's complete or not
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
const (
|
||||||
|
/*
|
||||||
|
bufSz is the size of the buffer/chunk for encoders/decords to use when unspecified by the method caller.
|
||||||
|
|
||||||
|
The below is 64 KiB, which should be relatively reasonable.
|
||||||
|
*/
|
||||||
|
bufSz int = 65536
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
Package binarystr parses and renders string (and other) representations of binary data.
|
||||||
|
To distinguish this package from [encoding/binary], this package refers to this string encoding format
|
||||||
|
as "bit encoding" or "bitstrings".
|
||||||
|
|
||||||
|
For example, the original data:
|
||||||
|
|
||||||
|
[]byte("test")
|
||||||
|
|
||||||
|
// Or:
|
||||||
|
// []byte{0x74, 0x65, 0x73, 0x74}
|
||||||
|
|
||||||
|
can be encoded to a bitstring via e.g. [BytesToStr]:
|
||||||
|
|
||||||
|
01110100011001010111001101110100
|
||||||
|
|
||||||
|
And even formatted via [FmtBinStr]:
|
||||||
|
|
||||||
|
// For example, FmtBinStr("01110100011001010111001101110100", "", "", " "):
|
||||||
|
01110100 01100101 01110011 01110100
|
||||||
|
|
||||||
|
// Or FmtBinStr("01110100011001010111001101110100", "", " ", "\n"):
|
||||||
|
0111 0100
|
||||||
|
0110 0101
|
||||||
|
0111 0011
|
||||||
|
0111 0100
|
||||||
|
|
||||||
|
Or can be converted to a series of "binary booleans" via e.g. [BytesToBool]:
|
||||||
|
|
||||||
|
[]bool{
|
||||||
|
false, true, true, true,
|
||||||
|
false, true, false, false,
|
||||||
|
|
||||||
|
false, true, true, false,
|
||||||
|
false, true, false, true,
|
||||||
|
|
||||||
|
false, true, true, true,
|
||||||
|
false, false, true, true,
|
||||||
|
|
||||||
|
false, true, true, true,
|
||||||
|
false, true, false, false,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
package binarystr
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
import (
|
||||||
|
`errors`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBadNumOct error = errors.New("bad number of bits for even octet split")
|
||||||
|
ErrNoChunk error = errors.New("empty/nil chunk")
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
BadBinCharErr struct {
|
||||||
|
posByte uint64
|
||||||
|
posRune uint64
|
||||||
|
r rune
|
||||||
|
}
|
||||||
|
|
||||||
|
BadNumOctErr struct {
|
||||||
|
posByte uint64
|
||||||
|
posRune uint64
|
||||||
|
rem string
|
||||||
|
}
|
||||||
|
|
||||||
|
BadReadErr struct {
|
||||||
|
numBytes uint64
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
import (
|
||||||
|
`bytes`
|
||||||
|
`errors`
|
||||||
|
`io`
|
||||||
|
`strconv`
|
||||||
|
`strings`
|
||||||
|
`unicode`
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
BoolToBytes provides the inverse of [BytesToBool].
|
||||||
|
|
||||||
|
Each boolean in bools indicates a 0 (false) or 1 (true).
|
||||||
|
|
||||||
|
e.g.:
|
||||||
|
|
||||||
|
[]bool{
|
||||||
|
false, true, true, true,
|
||||||
|
false, true, false, false,
|
||||||
|
|
||||||
|
false, true, true, false,
|
||||||
|
false, true, false, true,
|
||||||
|
|
||||||
|
false, true, true, true,
|
||||||
|
false, false, true, true,
|
||||||
|
|
||||||
|
false, true, true, true,
|
||||||
|
false, true, false, false,
|
||||||
|
}
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
[]byte{
|
||||||
|
0x74, 0x65, 0x73, 0x74,
|
||||||
|
}
|
||||||
|
|
||||||
|
If bools is nil, then b, rem, and err will also be nil and this function will no-op.
|
||||||
|
|
||||||
|
If len(bools) % 8 != 0, then err will be [ErrBadNumOct].
|
||||||
|
The remaining booleans can be found in rem.
|
||||||
|
*/
|
||||||
|
func BoolToBytes(bools []bool) (b []byte, rem []bool, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var loc []bool
|
||||||
|
var sb *strings.Builder = new(strings.Builder)
|
||||||
|
|
||||||
|
if bools == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(bools) == 0 {
|
||||||
|
b = make([]byte, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loc = make([]bool, len(bools))
|
||||||
|
copy(loc, bools)
|
||||||
|
|
||||||
|
for idx = 0; idx < len(loc); idx += 8 {
|
||||||
|
|
||||||
|
if len(loc) < 8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
writeBoolChunk(loc[idx:idx+8], sb)
|
||||||
|
|
||||||
|
loc = loc[idx+8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if b, err = StrToBytes(sb.String()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(loc) > 1 {
|
||||||
|
rem = append(rem, loc...)
|
||||||
|
err = ErrBadNumOct
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
BytesToStr converts bytes to a bitstring.
|
||||||
|
|
||||||
|
e.g.:
|
||||||
|
|
||||||
|
[]byte{
|
||||||
|
0x74, 0x65, 0x73, 0x74,
|
||||||
|
}
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
"01110100011001010111001101110100"
|
||||||
|
|
||||||
|
For the inverse, see [StrToBytes].
|
||||||
|
To format the bitstring, see [FmtBinStr].
|
||||||
|
*/
|
||||||
|
func BytesToStr(b []byte) (binStr string) {
|
||||||
|
|
||||||
|
var cb byte
|
||||||
|
var sb *strings.Builder
|
||||||
|
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sb = new(strings.Builder)
|
||||||
|
|
||||||
|
for _, cb = range b {
|
||||||
|
// This will never actually return an error.
|
||||||
|
_, _ = sb.WriteString(
|
||||||
|
strconv.FormatUint(
|
||||||
|
uint64(uint8(cb)),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binStr = sb.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
BytesToBool returns a slice of "binary booleans".
|
||||||
|
|
||||||
|
For each bit of data in b, a 0 is false and a 1 is true (as one would expect).
|
||||||
|
|
||||||
|
For the inverse of this, see [BoolToBytes].
|
||||||
|
*/
|
||||||
|
func BytesToBool(b []byte) (bools []bool) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var r rune
|
||||||
|
var rSz int
|
||||||
|
var idx int
|
||||||
|
var s *strings.Reader
|
||||||
|
|
||||||
|
if len(b) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// s will always be a valid bitstring.
|
||||||
|
s = strings.NewReader(BytesToStr(b))
|
||||||
|
|
||||||
|
bools = make([]bool, s.Len())
|
||||||
|
|
||||||
|
for idx = 0; idx < s.Len(); idx++ {
|
||||||
|
r, rSz, err = s.ReadRune()
|
||||||
|
|
||||||
|
if rSz == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// It'll only be an io.EOF if anything at all.
|
||||||
|
err = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r {
|
||||||
|
case '0':
|
||||||
|
bools[idx] = false
|
||||||
|
case '1':
|
||||||
|
bools[idx] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FmtBinStr formats bitstring s to more comfortable representations.
|
||||||
|
|
||||||
|
bitSep is a string to separate each bit boundary.
|
||||||
|
|
||||||
|
nybSep is a string to separate each "nibble"/"nybble" (4-bit "word") boundary.
|
||||||
|
|
||||||
|
bytSep is a string to separate each byte boundary.
|
||||||
|
|
||||||
|
Note that if you only explicitly use whitespace separators, then the output
|
||||||
|
can still be parsed back to native []byte{} data via [StrToBytes].
|
||||||
|
|
||||||
|
Note that NO checking is done against the validity of s being a bitstring;
|
||||||
|
the only content-checking done in s is if a given rune is a whitespace character or not
|
||||||
|
(whitespace present in s is skipped over, which lets you run FmtBinStr to reformat a bitstring).
|
||||||
|
*/
|
||||||
|
func FmtBinStr(s, bitSep, nybSep, bytSep string) (out string) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var lastIdx int
|
||||||
|
var sb *strings.Builder
|
||||||
|
|
||||||
|
if strings.TrimSpace(s) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sb = new(strings.Builder)
|
||||||
|
|
||||||
|
lastIdx = len(s) - 1
|
||||||
|
|
||||||
|
for idx = 0; idx < len(s); idx++ {
|
||||||
|
_ = sb.WriteByte(s[idx])
|
||||||
|
if idx != lastIdx {
|
||||||
|
if (idx+1)%8 == 0 {
|
||||||
|
if bytSep != "" {
|
||||||
|
_, _ = sb.WriteString(bytSep)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if (idx+1)%4 == 0 {
|
||||||
|
if nybSep != "" {
|
||||||
|
_, _ = sb.WriteString(nybSep)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bitSep != "" {
|
||||||
|
_, _ = sb.WriteString(bitSep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = sb.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewDecoder returns a [Decoder] from [io.Reader] r.
|
||||||
|
|
||||||
|
Data from r is expected to be BITSTRING data (strings);
|
||||||
|
doing read operations from a [Decoder] returns PLAIN data.
|
||||||
|
*/
|
||||||
|
func NewDecoder(r io.Reader) (e *Decoder) {
|
||||||
|
|
||||||
|
e = &Decoder{
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewEncoder returns an [Encoder] from [io.Reader] r.
|
||||||
|
|
||||||
|
Data from r is expected to be PLAIN data;
|
||||||
|
doing read operations from an [Encoder] returns BITSTRING data (strings).
|
||||||
|
*/
|
||||||
|
func NewEncoder(r io.Reader) (e *Encoder) {
|
||||||
|
|
||||||
|
e = &Encoder{
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StrToBytes converts a bitstring to bytes.
|
||||||
|
|
||||||
|
Whitespace is ignored.
|
||||||
|
|
||||||
|
e.g.:
|
||||||
|
|
||||||
|
"01110100011001010111001101110100"
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
[]byte("test")
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
[]byte{0x74, 0x65, 0x73, 0x74}
|
||||||
|
|
||||||
|
For the inverse, see [BytesToStr].
|
||||||
|
|
||||||
|
If the number of characters in binStr is not modulo 8 (`len() % 8 != 0`),
|
||||||
|
then err will be of type [BadNumOctErr].
|
||||||
|
|
||||||
|
If there are any characters encountered in binStr that are not "0" or "1"
|
||||||
|
(other than whitespace), then err will be of type [BadBinCharErr].
|
||||||
|
*/
|
||||||
|
func StrToBytes(binStr string) (b []byte, err error) {
|
||||||
|
|
||||||
|
var r rune
|
||||||
|
var cb byte
|
||||||
|
var rSz int
|
||||||
|
var pos uint64
|
||||||
|
var rErr error
|
||||||
|
var posR uint64
|
||||||
|
var chunk []rune
|
||||||
|
var buf *strings.Reader
|
||||||
|
var bytBuf *bytes.Buffer = new(bytes.Buffer)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
b = bytBuf.Bytes()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if strings.TrimSpace(binStr) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf = strings.NewReader(binStr)
|
||||||
|
|
||||||
|
chunk = make([]rune, 0, 8)
|
||||||
|
for {
|
||||||
|
r, rSz, rErr = buf.ReadRune()
|
||||||
|
if rSz == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos += uint64(rSz)
|
||||||
|
if rSz != 0 {
|
||||||
|
posR++
|
||||||
|
}
|
||||||
|
if len(chunk) == 8 {
|
||||||
|
if cb, err = chunkToByte(chunk); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = bytBuf.WriteByte(cb); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chunk = make([]rune, 0, 8)
|
||||||
|
}
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rSz != 0 {
|
||||||
|
switch r {
|
||||||
|
case '0', '1':
|
||||||
|
chunk = append(chunk, r)
|
||||||
|
default:
|
||||||
|
err = &BadBinCharErr{
|
||||||
|
posByte: pos,
|
||||||
|
posRune: posR,
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rErr != nil {
|
||||||
|
if errors.Is(rErr, io.EOF) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
err = rErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rSz == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chunk) != 0 {
|
||||||
|
if len(chunk) == 8 {
|
||||||
|
if cb, err = chunkToByte(chunk); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = bytBuf.WriteByte(cb); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = &BadNumOctErr{
|
||||||
|
posByte: pos,
|
||||||
|
posRune: posR,
|
||||||
|
rem: string(chunk),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func chunkToByte(chunk []rune) (b byte, err error) {
|
||||||
|
|
||||||
|
var u8 uint64
|
||||||
|
|
||||||
|
if len(chunk) == 0 {
|
||||||
|
err = ErrNoChunk
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if u8, err = strconv.ParseUint(string(chunk), 2, 8); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b = byte(uint8(u8))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBoolChunk(chunk []bool, sb io.StringWriter) {
|
||||||
|
|
||||||
|
var b bool
|
||||||
|
|
||||||
|
if len(chunk) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, b = range chunk {
|
||||||
|
switch b {
|
||||||
|
case false:
|
||||||
|
sb.WriteString("0")
|
||||||
|
case true:
|
||||||
|
sb.WriteString("1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// BytePos returns the byte position index immediately leading up to the error.
|
||||||
|
func (b *BadBinCharErr) BytePos() (pos uint64) { return b.posByte }
|
||||||
|
|
||||||
|
// Rune returns the offending rune that caused the error.
|
||||||
|
func (b *BadBinCharErr) Rune() (r rune) { return b.r }
|
||||||
|
|
||||||
|
// RunePos returns the rune (character) position index immediately leading up to the error.
|
||||||
|
func (b *BadBinCharErr) RunePos() (pos uint64) { return b.posRune }
|
||||||
|
|
||||||
|
// Error returns the error message for a BadBinCharError. It conforms to an [error] interface.
|
||||||
|
func (b *BadBinCharErr) Error() (err string) {
|
||||||
|
|
||||||
|
err = fmt.Sprintf(
|
||||||
|
"rune/character after position index #%d (%#v) (byte #%d) is not \"0\" or \"1\"",
|
||||||
|
b.posRune, b.r, b.posByte,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is an alias for the [BadBinCharErr.Error] method to conform explicitly to a [fmt.Stringer] interface.
|
||||||
|
func (b *BadBinCharErr) String() (s string) { return b.Error() }
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// BytePos returns the byte position index immediately leading up to the error.
|
||||||
|
func (b *BadNumOctErr) BytePos() (pos uint64) { return b.posByte }
|
||||||
|
|
||||||
|
// Error returns the error message for a BadNumOctError. It conforms to an [error] interface.
|
||||||
|
func (b *BadNumOctErr) Error() (err string) {
|
||||||
|
|
||||||
|
err = fmt.Sprintf(
|
||||||
|
"only able to read %d complete octets/bytes (%#d runes/characters); "+
|
||||||
|
"%d remaining characters are less than 8 bits",
|
||||||
|
b.posByte, b.posRune,
|
||||||
|
len(b.rem),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Rem returns the remaining unparsed string of characters; this should ALWAYS be a length of 0 <= len(rem) < 8.
|
||||||
|
(It will only be an empty string if [Decoder.Read] encountered the end of its source reader before the buffer was filled,
|
||||||
|
but the available data cleanly broke into a last octet.)
|
||||||
|
*/
|
||||||
|
func (b *BadNumOctErr) Rem() (rem string) { return b.rem }
|
||||||
|
|
||||||
|
// RunePos returns the rune (character) position index immediately leading up to the error.
|
||||||
|
func (b *BadNumOctErr) RunePos() (pos uint64) { return b.posRune }
|
||||||
|
|
||||||
|
// String is an alias for the [BadNumOctErr.Error] method to conform explicitly to a [fmt.Stringer] interface.
|
||||||
|
func (b *BadNumOctErr) String() (s string) { return b.Error() }
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
import (
|
||||||
|
`fmt`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error conforms a [BadReadErr] to an [error].
|
||||||
|
func (b *BadReadErr) Error() (err string) {
|
||||||
|
|
||||||
|
err = fmt.Sprintf("only read %d bytes from internal read source before encountering a read error", b.numBytes)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumBytesRead returns the number of bytes read (BEFORE mutation/encoding/decoding) before the error was hit.
|
||||||
|
func (b *BadReadErr) NumBytesRead() (u64 uint64) { return b.numBytes }
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
import (
|
||||||
|
`errors`
|
||||||
|
`io`
|
||||||
|
`strings`
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Read conforms to an [io.Reader].
|
||||||
|
It returns plain data decoded from bitstring encoding in e's read source.
|
||||||
|
|
||||||
|
IMPORTANT: Read will return a [BadNumOctErr] if the amount of data in e's read source is not at least len(b) * 8!
|
||||||
|
ONE BYTE in b equals EIGHT BYTES from e's read source!
|
||||||
|
Likewise, n will ALWAYS be len(b), but the number of bytes actually read from e's internal reader
|
||||||
|
will ALWAYS be n*8!
|
||||||
|
In this case, b will still be populated with the available decoded data, and err ([BadNumOctErr])'s
|
||||||
|
[BadNumOctErr.Rem] method will contain the remaining bitstring (if there is any left over) after decoding
|
||||||
|
the octets that were returned (if any).
|
||||||
|
|
||||||
|
IMPORTANT: Read will return a [BadBinCharErr] if it encounters any character in e's read source that is not '0' or '1'!
|
||||||
|
THIS INCLUDES WHITESPACE due to the buffer size calculation (unlike [StrToBytes]); it is NOT skipped!
|
||||||
|
*/
|
||||||
|
func (e *Decoder) Read(b []byte) (n int, err error) {
|
||||||
|
|
||||||
|
var inN int
|
||||||
|
var s string
|
||||||
|
var endIdx int
|
||||||
|
var rem string
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if b != nil && buf != nil {
|
||||||
|
s = BytesToStr(buf)
|
||||||
|
copy(b, []byte(s))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(b) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = make([]byte, len(b)*8)
|
||||||
|
if inN, err = e.r.Read(buf); err != nil {
|
||||||
|
// This is caught/handled below.
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
err = nil
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inN < len(buf) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if inN != len(buf) {
|
||||||
|
err = &BadNumOctErr{
|
||||||
|
posByte: uint64(inN),
|
||||||
|
posRune: uint64(inN),
|
||||||
|
rem: rem, // We hit the end too "early" so there is no "remaining".
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ReadString returns a bitstring-encoded [string] from the entirety of the data
|
||||||
|
in the source reader of e.
|
||||||
|
|
||||||
|
IMPORTANT: If e's source reader is a non-terminating stream (e.g. a [math/rand.Rand]),
|
||||||
|
this function will NEVER RETURN (and will likely consume all memory).
|
||||||
|
YOU HAVE BEEN WARNED.
|
||||||
|
|
||||||
|
n is the number of bytes read from e's source reader.
|
||||||
|
*/
|
||||||
|
func (e *Decoder) ReadString() (s string, n uint64, err error) {
|
||||||
|
|
||||||
|
var inN int
|
||||||
|
var buf []byte
|
||||||
|
var rErr error
|
||||||
|
var sb *strings.Builder = new(strings.Builder)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if sb != nil {
|
||||||
|
s = sb.String()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
buf = make([]byte, bufSz)
|
||||||
|
inN, rErr = e.r.Read(buf)
|
||||||
|
n += uint64(inN)
|
||||||
|
|
||||||
|
if inN != 0 {
|
||||||
|
if inN < bufSz {
|
||||||
|
buf = buf[:inN]
|
||||||
|
}
|
||||||
|
sb.WriteString(BytesToStr(buf))
|
||||||
|
} else {
|
||||||
|
if errors.Is(rErr, io.EOF) {
|
||||||
|
rErr = nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if rErr != nil {
|
||||||
|
err = rErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
import (
|
||||||
|
`errors`
|
||||||
|
`io`
|
||||||
|
`strings`
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Read conforms to an [io.Reader].
|
||||||
|
It returns bitstring encoding of the data in e's read source.
|
||||||
|
|
||||||
|
(To conform to [io.Reader], this is returned as bytes - e.g. []byte(string()).)
|
||||||
|
|
||||||
|
IMPORTANT: Read will return [ErrBadNumOct] if len(b) % 8 != 0! EIGHT BYTES in b equal ONE BYTE
|
||||||
|
from e's read source! Keep this in mind and size b accordingly for your desired decoded data size!
|
||||||
|
Likewise, n will ALWAYS be len(b), but the number of bytes actually read from e's internal reader
|
||||||
|
will ALWAYS be n/8! (If for some bizarre case it isn't, err will be [BadReadErr].)
|
||||||
|
*/
|
||||||
|
func (e *Encoder) Read(b []byte) (n int, err error) {
|
||||||
|
|
||||||
|
var inN int
|
||||||
|
var s string
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if b != nil && buf != nil {
|
||||||
|
s = BytesToStr(buf)
|
||||||
|
copy(b, []byte(s))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(b) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(b)%8 != 0 {
|
||||||
|
err = ErrBadNumOct
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = make([]byte, len(b)/8)
|
||||||
|
if inN, err = e.r.Read(buf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if inN != len(buf) {
|
||||||
|
if len(buf) > inN {
|
||||||
|
buf = buf[:inN]
|
||||||
|
}
|
||||||
|
err = &BadReadErr{
|
||||||
|
numBytes: uint64(inN),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ReadString returns a bitstring-encoded [string] from the entirety of the data
|
||||||
|
in the source reader of e.
|
||||||
|
|
||||||
|
IMPORTANT: If e's source reader is a non-terminating stream (e.g. a [math/rand.Rand]),
|
||||||
|
this function will NEVER RETURN (and will likely consume all memory).
|
||||||
|
YOU HAVE BEEN WARNED.
|
||||||
|
|
||||||
|
n is the number of bytes read from e's source reader.
|
||||||
|
*/
|
||||||
|
func (e *Encoder) ReadString() (s string, n uint64, err error) {
|
||||||
|
|
||||||
|
var inN int
|
||||||
|
var buf []byte
|
||||||
|
var rErr error
|
||||||
|
var sb *strings.Builder = new(strings.Builder)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if sb != nil {
|
||||||
|
s = sb.String()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
buf = make([]byte, bufSz)
|
||||||
|
inN, rErr = e.r.Read(buf)
|
||||||
|
n += uint64(inN)
|
||||||
|
|
||||||
|
if inN != 0 {
|
||||||
|
if inN < bufSz {
|
||||||
|
buf = buf[:inN]
|
||||||
|
}
|
||||||
|
sb.WriteString(BytesToStr(buf))
|
||||||
|
} else {
|
||||||
|
if errors.Is(rErr, io.EOF) {
|
||||||
|
rErr = nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if rErr != nil {
|
||||||
|
err = rErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package binarystr
|
||||||
|
|
||||||
|
import (
|
||||||
|
`io`
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Encoder struct {
|
||||||
|
r io.Reader
|
||||||
|
cur uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
Decoder struct {
|
||||||
|
r io.Reader
|
||||||
|
cur uint64
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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,10 +1,60 @@
|
|||||||
module r00t2.io/goutils
|
module r00t2.io/goutils
|
||||||
|
|
||||||
go 1.16
|
go 1.27
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/Masterminds/sprig/v3 v3.3.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/coreos/go-systemd/v22 v22.7.0
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
github.com/davecgh/go-spew v1.1.1
|
||||||
r00t2.io/sysutils v1.1.1
|
github.com/deckarep/golang-set/v2 v2.9.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/miekg/dns v1.1.73
|
||||||
|
github.com/olekukonko/tablewriter v1.1.4
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.9
|
||||||
|
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb
|
||||||
|
golang.org/x/sys v0.49.0
|
||||||
|
r00t2.io/subnetter v0.3.1
|
||||||
|
r00t2.io/sysutils v1.16.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
github.com/Masterminds/semver/v3 v3.5.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.11.1 // indirect
|
||||||
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.2 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.2 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.5 // indirect
|
||||||
|
github.com/go-resty/resty/v2 v2.17.2 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/huandu/xstrings v1.6.2 // indirect
|
||||||
|
github.com/leodido/go-urn v1.5.0 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // 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-20260916203055-22a1a467d9f0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.5.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.10 // indirect
|
||||||
|
golang.org/x/crypto v0.58.0 // indirect
|
||||||
|
golang.org/x/net v0.61.0 // indirect
|
||||||
|
golang.org/x/sync v0.24.0 // indirect
|
||||||
|
golang.org/x/text v0.43.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,188 @@
|
|||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
r00t2.io/sysutils v1.1.1 h1:q2P5u50HIIRk6muCPo1Gpapy6sNT4oaB1l2O/C/mi3A=
|
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||||
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
|
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||||
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI=
|
||||||
|
github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM=
|
||||||
|
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/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||||
|
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
|
||||||
|
github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||||
|
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/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
|
||||||
|
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/go-playground/locales v0.14.2 h1:d8UmcrM6Nip0hfGZKLGpAvZH37XB4TS0xzK9B56YNCY=
|
||||||
|
github.com/go-playground/locales v0.14.2/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.2 h1:LCsMLC9RzmbUMNUPVYD15dmcjwYAJhmX8mPZRW4rAVU=
|
||||||
|
github.com/go-playground/universal-translator v0.18.2/go.mod h1:67VZIMp5lQpDWlnStOct22q1bkdJGJqHghbOtmkawxk=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.5 h1:YyCXvVShZbs2Sm3Mb53eNOlhRXctSOzW5QJAouCTZL4=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.5/go.mod h1:wEqiaov48pXX1kjhc3Da8y0M0Dtg/BK7gurFBLgwFrQ=
|
||||||
|
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
|
||||||
|
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||||
|
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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/huandu/xstrings v1.6.2 h1:+X5X6N46b40cmDw7FFJFU6Eoq0yJS8lbYigT2EFau4c=
|
||||||
|
github.com/huandu/xstrings v1.6.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
|
||||||
|
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
|
||||||
|
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/lufia/plan9stats v0.0.0-20260627054121-477a66015f15 h1:YkjVPl/YH5XlJ+/NiwzJtPYXXKRcyjmEUhsDci6YK3c=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/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/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
|
||||||
|
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||||
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||||
|
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
|
||||||
|
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||||
|
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||||
|
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||||
|
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||||
|
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0 h1:XA01Vk/wv9YikCi1V51yRzIHPMT5of9+cMpZoZvDn/M=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.9 h1:CaBo/hBFqvlJoLvAQEKdHdf5GD97/MN2ACfyv63+2Ig=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.9/go.mod h1:nKH+8wX2zxr/mDbsR+AAy6Qd2z4ZOQ7wfQaEp+yrYmg=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/shopspring/decimal v1.5.0 h1:HxSWk31RmJv0EyY4oLiGQB9u7WPludUwPBdvQZnYFPc=
|
||||||
|
github.com/shopspring/decimal v1.5.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
|
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=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.10 h1:kdAgQvu8TROXZpSkJQd5wzfaNCCrMbpZyKFtQ6qkPCE=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.10/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
|
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb h1:XBM4hvfwGAttkkiTIFfeigdfcL1xIfdKXqFdgiHGtDs=
|
||||||
|
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb/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/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
|
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||||
|
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||||
|
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||||
|
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||||
|
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/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||||
|
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
|
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=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||||
|
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
|
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||||
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||||
|
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/subnetter v0.3.1 h1:I/Uoit7Id+QaFK2W2GwUdkr6WopcagFWxCur1gUnTdc=
|
||||||
|
r00t2.io/subnetter v0.3.1/go.mod h1:nyOpA8S1IU91gcaP8DMaq/VBUHJ28luGp+ulQFsv6YM=
|
||||||
|
r00t2.io/sysutils v1.16.2 h1:wI01UwZ/bXn/lzBiCpqDmzZCOWiK87kz04SB4xRw+W0=
|
||||||
|
r00t2.io/sysutils v1.16.2/go.mod h1:iXK+ALOwIdRKjAJIE5USlkZ669SVDHBNNuYhunsznH8=
|
||||||
|
r00t2.io/sysutils v1.16.5 h1:0v/HWTLOl8UN8B6koXM3e+TWN1N3eAoUM+xGK+itwq0=
|
||||||
|
r00t2.io/sysutils v1.16.5/go.mod h1:GAVZuZeLWiFgzfVVL/6oHgtGDm+h/tT5z11DwPS+fSU=
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
Package iox includes extensions to the stdlib `io` module.
|
||||||
|
|
||||||
|
Not everything in here is considered fully stabilized yet,
|
||||||
|
but it should be usable.
|
||||||
|
*/
|
||||||
|
package iox
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package iox
|
||||||
|
|
||||||
|
import (
|
||||||
|
`errors`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBufTooSmall error = errors.New("buffer too small; buffer size must be > 0")
|
||||||
|
ErrChunkTooBig error = errors.New("chunk too big for method")
|
||||||
|
ErrChunkTooSmall error = errors.New("chunk too small for buffer")
|
||||||
|
ErrInvalidChunkSize error = errors.New("an invalid chunk size was passed")
|
||||||
|
ErrNilCtx error = errors.New("a nil context was passed")
|
||||||
|
ErrNilReader error = errors.New("a nil reader was passed")
|
||||||
|
ErrNilWriter error = errors.New("a nil writer was passed")
|
||||||
|
ErrShortRead error = errors.New("a read was cut short with no EOF")
|
||||||
|
ErrShortWrite error = errors.New("a write was cut short with no error")
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
Package fsx provides some enhancements to [io/fs].
|
||||||
|
*/
|
||||||
|
package fsx
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package fsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/types"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: FilterFsPaths
|
||||||
|
|
||||||
|
/*
|
||||||
|
FsPaths returns all paths in [io/fs.FS] `fsys`.
|
||||||
|
|
||||||
|
If `fsys` is nil, both `fsPaths` and `err` will be nil.
|
||||||
|
|
||||||
|
`t` should be an OR'd [io/fs.FileMode] containing all filetypes that should be included.
|
||||||
|
Use [io/fs.ModeType] to match any non-regular-file object, or 0 to explicitly match regular files.
|
||||||
|
If `t` is non-zero but you wish to ALSO include regular files, then specify `inclFiles` as true.
|
||||||
|
(`inclFiles` is ignored if `t` == 0, as this inherently matches regular files only/explicitly.)
|
||||||
|
See [HasType] for more information on this filtering.
|
||||||
|
|
||||||
|
`maxDepth` can be used to limit recursion depth.
|
||||||
|
|
||||||
|
- If `maxDepth` == 0, only direct [io/fs.DirEntry] descendants of `fsys`
|
||||||
|
will be included in `fsPaths`.
|
||||||
|
- If `maxDepth` < 0, ALL [io/fs.DirEntry] descendants of `fsys`
|
||||||
|
will be included in `fsPaths`.
|
||||||
|
- If `maxDepth` > 0, then only children at a depth of this level or lower
|
||||||
|
("higher" in a hierarchy) will be included in `fsPaths`.
|
||||||
|
*/
|
||||||
|
func FsPaths(fsys fs.FS, t fs.FileMode, inclFiles bool, maxDepth int) (fsPaths map[string]fs.DirEntry, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var entries []fs.DirEntry
|
||||||
|
var t fs.FileMode = types.Type()
|
||||||
|
|
||||||
|
if fsys == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fsPaths, err = getEntries(fsys, t, inclFiles, maxDepth, "."); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
HasType is just a convenience wrapper for:
|
||||||
|
|
||||||
|
hasType = m & t != 0
|
||||||
|
|
||||||
|
The type(s) will be explicitly extracted from `m` and `t` first.
|
||||||
|
|
||||||
|
Note that if `t` is an OR'd set of filetypes, then HasType will return true if ANY of the types
|
||||||
|
in `t` match `m`'s type (see [r00t2.io/goutils/bitmask] for more information on bitmasking).
|
||||||
|
If you wish to check if `m` is A SINGLE type, either ensure that `t` is not OR'd
|
||||||
|
(i.e. is a single fs.FileMode type) or use [IsType] instead.
|
||||||
|
*/
|
||||||
|
func HasType(m, t fs.FileMode) (hasType bool) { return m.Type()&t.Type() != 0 }
|
||||||
|
|
||||||
|
/*
|
||||||
|
IsType is just a convenience wrapper for:
|
||||||
|
|
||||||
|
isType = m & t == t
|
||||||
|
|
||||||
|
The type(s) will be explicitly extracted from `m` and `t` first.
|
||||||
|
|
||||||
|
Note that if `t` is an OR'd set of filetypes, then IsType will *never* return true
|
||||||
|
as its type is single-valued/not bitmasked (see [r00t2.io/goutils/bitmask] for more information
|
||||||
|
on bitmasking).
|
||||||
|
If you wish to check if `m` is ONE OF multiple types in `t`, use [HasType] instead.
|
||||||
|
*/
|
||||||
|
func IsType(m, t fs.FileMode) (isType bool) { return m&t == t }
|
||||||
|
|
||||||
|
/*
|
||||||
|
getEntries is used to recursively fetch entries from `fsys`.
|
||||||
|
|
||||||
|
It mostly serves as the recursion for [FsPaths], and the arguments
|
||||||
|
retain their exact meaning except that it requires `relPath`
|
||||||
|
and `maxDepth` (if > 0) will be decremanted in each recursion.
|
||||||
|
*/
|
||||||
|
func getEntries(fsys fs.FS, t fs.FileMode, inclFiles bool, maxDepth int, relPath string) (fsPaths map[string]fs.DirEntry, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var p string
|
||||||
|
var k string
|
||||||
|
var v fs.DirEntry
|
||||||
|
var e []fs.DirEntry
|
||||||
|
var subs map[string]fs.DirEntry = make(map[string]fs.DirEntry)
|
||||||
|
|
||||||
|
if e, err = fs.ReadDir(fsys, relPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fsPaths = make(map[string]fs.DirEntry)
|
||||||
|
|
||||||
|
for idx = range e {
|
||||||
|
p = filepath.Join(relPath, e[idx].Name())
|
||||||
|
if e[idx].IsDir() {
|
||||||
|
if subs, err = getEntries(fsys, t, inclFiles, maxDepth, p); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v = range subs {
|
||||||
|
fsPaths[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t != 0 {
|
||||||
|
if HasType(t, e[idx].Type()) {
|
||||||
|
fsPaths[p] = e[idx]
|
||||||
|
}
|
||||||
|
} else if inclFiles && e[idx].Type() == 0 {
|
||||||
|
fsPaths[p] = e[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/deckarep/golang-set/v2"
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewEmbedFsMember returns an embedFsMember from embedFs.
|
||||||
|
func NewEmbedFsMember(embedFs *embed.FS) (m *embedFsMember) {
|
||||||
|
|
||||||
|
m = &embedFsMember{
|
||||||
|
FS: embedFs,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewMergedFS returns a MergedFS.
|
||||||
|
|
||||||
|
The last filesystem in a MergedFS is always embedTplFS.
|
||||||
|
If fsPaths is empty, mfs will only contain embedTplFS.
|
||||||
|
|
||||||
|
Nonexisting paths in fsPaths will be skipped (and added to the noExist slice).
|
||||||
|
|
||||||
|
Order will be retained but duplicates will be skipped and added to the dupes slice.
|
||||||
|
|
||||||
|
noExist and dupes will be nil if no non-existing or duplicate paths are encountereed.
|
||||||
|
*/
|
||||||
|
func NewMergedFS(fsPaths ...string) (mfs *MergedFS, noExist, dupes []string, err error) {
|
||||||
|
|
||||||
|
var m MergedFS
|
||||||
|
var exists bool
|
||||||
|
var fsPath string
|
||||||
|
var fsMem mergedFsMember
|
||||||
|
|
||||||
|
m = MergedFS{
|
||||||
|
fses: make([]mergedFsMember, 0, len(fsPaths)+1),
|
||||||
|
overridePaths: make([]string, 0, len(fsPaths)+1),
|
||||||
|
uniq: mapset.NewSet[string](),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fsPath = range fsPaths {
|
||||||
|
if exists, err = paths.RealPathExists(&fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
noExist = append(noExist, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !m.uniq.Add(fsPath) {
|
||||||
|
dupes = append(dupes, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fsMem, err = NewRealFsMember(fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.fses = append(m.fses, fsMem)
|
||||||
|
m.overridePaths = append(m.overridePaths, fsPath)
|
||||||
|
}
|
||||||
|
// embedded fs last
|
||||||
|
m.fses = append(m.fses, NewEmbedFsMember(&embedTplFS))
|
||||||
|
m.overridePaths = append(m.overridePaths, "")
|
||||||
|
|
||||||
|
m.fses = slices.Clip(m.fses)
|
||||||
|
m.overridePaths = slices.Clip(m.overridePaths)
|
||||||
|
|
||||||
|
mfs = &m
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRealFsMember returns a realFsMember (which properly conforms to mergedFsMember) from a real filesystem path.
|
||||||
|
func NewRealFsMember(fsPath string) (fsm *realFsMember, err error) {
|
||||||
|
|
||||||
|
var fsp string
|
||||||
|
var m realFsMember
|
||||||
|
|
||||||
|
if err = paths.RealPath(&fsp); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Root, err = os.OpenRoot(fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fsm = &m
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Close is a no-op, but used to conform to mergedFsMember.
|
||||||
|
func (e *embedFsMember) Close() (err error) { return }
|
||||||
|
|
||||||
|
// Stat wraps fs.Stat on the embeded filesystem.
|
||||||
|
func (e *embedFsMember) Stat(fnm string) (stat fs.FileInfo, err error) {
|
||||||
|
|
||||||
|
if stat, err = fs.Stat(e.FS, fnm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Append adds fsPaths as fs.FS to the MergedFS (before the embed.FS).
|
||||||
|
|
||||||
|
If fsPaths is empty, Append is a NO-OP.
|
||||||
|
|
||||||
|
noExist and dupes have the same meaning as in NewMergedFS.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) Append(fsPaths ...string) (noExist, dupes []string, err error) {
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
var fsPath string
|
||||||
|
var newPaths []string
|
||||||
|
var fsMem *realFsMember
|
||||||
|
var newFs []mergedFsMember
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
newPaths = make([]string, 0, len(fsPaths))
|
||||||
|
newFs = make([]mergedFsMember, 0, len(fsPaths))
|
||||||
|
|
||||||
|
for _, fsPath = range fsPaths {
|
||||||
|
if exists, err = paths.RealPathExists(&fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
noExist = append(noExist, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fsMem, err = NewRealFsMember(fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !m.uniq.Add(fsPath) {
|
||||||
|
dupes = append(dupes, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newFs = append(newFs, fsMem)
|
||||||
|
newPaths = append(newPaths, fsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newFs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.overridePaths = append(m.overridePaths, newPaths...)
|
||||||
|
m.fses = append(m.fses[:len(m.fses)-1], append(newFs, m.fses[len(m.fses)-1])...)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all realFsMember members. This MergedFS will not be able to be used after this is called.
|
||||||
|
func (m *MergedFS) Close() (err error) {
|
||||||
|
|
||||||
|
var fsMem mergedFsMember
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
for _, fsMem = range m.fses {
|
||||||
|
if err = fsMem.(io.Closer).Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Insert adds fsPaths as fs.FS to a MergedFS starting at index idx.
|
||||||
|
|
||||||
|
If fsPaths is empty, Insert is a NO-OP.
|
||||||
|
|
||||||
|
If idx is 0 or negative, they will be prepended.
|
||||||
|
If idx is larger than the last index of real directories (excluding the last embed.FS),
|
||||||
|
it will be reduced to the highest valid index.
|
||||||
|
|
||||||
|
noExist and dupes have the same meaning as in NewMergedFS.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) Insert(idx int, fsPaths ...string) (noExist, dupes []string, err error) {
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
var fsPath string
|
||||||
|
var newPaths []string
|
||||||
|
var fsMem *realFsMember
|
||||||
|
var newFs []mergedFsMember
|
||||||
|
|
||||||
|
if len(fsPaths) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
newPaths = make([]string, 0, len(fsPaths))
|
||||||
|
newFs = make([]mergedFsMember, 0, len(fsPaths))
|
||||||
|
|
||||||
|
for _, fsPath = range fsPaths {
|
||||||
|
if exists, err = paths.RealPathExists(&fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
noExist = append(noExist, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fsMem, err = NewRealFsMember(fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !m.uniq.Add(fsPath) {
|
||||||
|
dupes = append(dupes, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newPaths = append(newPaths, fsPath)
|
||||||
|
newFs = append(newFs, fsMem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newFs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx < 0 {
|
||||||
|
m.fses = append(newFs, m.fses...)
|
||||||
|
m.overridePaths = append(newPaths, m.overridePaths...)
|
||||||
|
return
|
||||||
|
} else if idx >= len(m.fses)-1 {
|
||||||
|
idx = len(m.fses) - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
m.fses = append(m.fses[:idx], append(newFs, m.fses[idx:]...)...)
|
||||||
|
m.overridePaths = append(m.overridePaths[:idx], append(newPaths, m.overridePaths[idx:]...)...)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens filename fnm and returns it as file handler fh.
|
||||||
|
func (m *MergedFS) Open(fnm string) (fh fs.File, err error) {
|
||||||
|
|
||||||
|
var fsref fs.FS
|
||||||
|
|
||||||
|
m.mut.RLock()
|
||||||
|
defer m.mut.RUnlock()
|
||||||
|
|
||||||
|
// Iterate until a match is found.
|
||||||
|
for _, fsref = range m.fses {
|
||||||
|
if fh, err = fsref.Open(fnm); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match found.
|
||||||
|
err = &fs.PathError{
|
||||||
|
Op: "Open",
|
||||||
|
Path: fnm,
|
||||||
|
Err: fs.ErrNotExist,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Paths returns a slice of real filesystem paths that are used by this MergedFS.
|
||||||
|
|
||||||
|
If sorted is true, they will be returned as a sorted slice -
|
||||||
|
otherwise they will be returned in the order they are checked.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) Paths(sorted bool) (fsPaths []string) {
|
||||||
|
|
||||||
|
m.mut.RLock()
|
||||||
|
defer m.mut.RUnlock()
|
||||||
|
|
||||||
|
if sorted {
|
||||||
|
fsPaths = m.uniq.ToSlice()
|
||||||
|
slices.Sort(fsPaths)
|
||||||
|
} else {
|
||||||
|
fsPaths = make([]string, len(m.overridePaths)-1)
|
||||||
|
copy(fsPaths, m.overridePaths[:len(m.overridePaths)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ReadDir returns a listing of path matcher fpath.
|
||||||
|
|
||||||
|
The result is a combination of all filesystems; the first encountered matching
|
||||||
|
name is added.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) ReadDir(fpath string) (entries []fs.DirEntry, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var e fs.DirEntry
|
||||||
|
var tmpEntries []fs.DirEntry
|
||||||
|
var seen map[string]bool = make(map[string]bool)
|
||||||
|
|
||||||
|
m.mut.RLock()
|
||||||
|
defer m.mut.RUnlock()
|
||||||
|
|
||||||
|
// merge real and embedded, preferring first-seen real paths
|
||||||
|
for idx, _ = range m.fses {
|
||||||
|
if m.overridePaths[idx] == "" {
|
||||||
|
if tmpEntries, err = m.fses[idx].ReadDir(filepath.FromSlash(fpath)); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if tmpEntries, err = os.ReadDir(filepath.Join(m.overridePaths[idx], filepath.FromSlash(fpath))); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e = range tmpEntries {
|
||||||
|
if seen[e.Name()] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[e.Name()] = true
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ReadFile returns the bytes for the first found fpath.
|
||||||
|
|
||||||
|
It wraps MergedFS.Open.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) ReadFile(fpath string) (b []byte, err error) {
|
||||||
|
|
||||||
|
var fh fs.File
|
||||||
|
|
||||||
|
if fh, err = m.Open(fpath); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if fh != nil {
|
||||||
|
if err = fh.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if b, err = io.ReadAll(fh); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Stat returns the fs.FileInfo for the first found fpath.
|
||||||
|
|
||||||
|
It wraps MergedFS.Open.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) Stat(fpath string) (stat fs.FileInfo, err error) {
|
||||||
|
|
||||||
|
var fh fs.File
|
||||||
|
|
||||||
|
if fh, err = m.Open(fpath); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if fh != nil {
|
||||||
|
if err = fh.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if stat, err = fh.Stat(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
RemoveFsByIdx removes the filesystem found at index idx.
|
||||||
|
|
||||||
|
An attempt to remove the final embed.FS filesystem will result in an ErrFsIdxOOB.
|
||||||
|
|
||||||
|
If idx is larger than the last index of "real" filesystems, an ErrFsIdxOOB will be returned.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) RemoveFsByIdx(idx uint) (err error) {
|
||||||
|
|
||||||
|
var fsPath string
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
if idx+1 == uint(len(m.fses)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx > uint(len(m.overridePaths)-1) {
|
||||||
|
err = ErrFsIdxOOB
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fsPath = m.overridePaths[idx]
|
||||||
|
if m.uniq.Contains(fsPath) {
|
||||||
|
m.uniq.Remove(fsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.fses = append(m.fses[:idx], m.fses[idx+1:]...)
|
||||||
|
m.overridePaths = append(m.overridePaths[:idx], m.overridePaths[idx+1:]...)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
RemoveFsByPath removes the fileststem with path dirPath.
|
||||||
|
|
||||||
|
If dirpath is empty, this function NOOPs.
|
||||||
|
|
||||||
|
If dirpath does not exist in this MergedFS, an ErrFsPathNoExists will be returned.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) RemoveFsByPath(dirPath string) (err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var fsp string
|
||||||
|
var p string = dirPath
|
||||||
|
|
||||||
|
if p == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = paths.RealPath(&p); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
if !m.uniq.Contains(p) {
|
||||||
|
err = ErrFsPathNoExists
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.uniq.Remove(p)
|
||||||
|
|
||||||
|
for idx, fsp = range m.overridePaths {
|
||||||
|
if fsp == p {
|
||||||
|
m.fses = append(m.fses[:idx], m.fses[idx+1:]...)
|
||||||
|
m.overridePaths = append(m.overridePaths[:idx], m.overridePaths[idx+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ErrFsIdxOOB
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open opens file fnm.
|
||||||
|
func (r *realFsMember) Open(fnm string) (fh fs.File, err error) {
|
||||||
|
|
||||||
|
if fh, err = r.Root.Open(fnm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir returns directory entries at fpath joined with the realFsMember's root.
|
||||||
|
func (r *realFsMember) ReadDir(fpath string) (entries []fs.DirEntry, err error) {
|
||||||
|
|
||||||
|
var rel string
|
||||||
|
var fsp string = fpath
|
||||||
|
|
||||||
|
if !filepath.IsAbs(fsp) {
|
||||||
|
fsp = filepath.Join(
|
||||||
|
r.Root.Name(),
|
||||||
|
filepath.FromSlash(fsp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if err = paths.RealPath(&fsp); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rel, err = filepath.Rel(r.Root.Name(), fsp); err != nil {
|
||||||
|
// Not relative to this root, so skip.
|
||||||
|
err = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rel, "..") {
|
||||||
|
// Absolute but not relative to this root, so skip.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entries, err = os.ReadDir(fsp); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type (
|
||||||
|
mergedFsMember interface {
|
||||||
|
fs.ReadDirFS
|
||||||
|
fs.ReadFileFS
|
||||||
|
fs.StatFS
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
realFsMember struct {
|
||||||
|
*os.Root
|
||||||
|
}
|
||||||
|
embedFsMember struct {
|
||||||
|
*embed.FS
|
||||||
|
}
|
||||||
|
MergedFS struct {
|
||||||
|
// always appended with embedTplFs
|
||||||
|
fses []mergedFsMember
|
||||||
|
overridePaths []string
|
||||||
|
uniq mapset.Set[string]
|
||||||
|
mut sync.RWMutex
|
||||||
|
}
|
||||||
|
)
|
||||||
+247
@@ -0,0 +1,247 @@
|
|||||||
|
package iox
|
||||||
|
|
||||||
|
import (
|
||||||
|
`context`
|
||||||
|
`io`
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
CopyBufN is a mix between [io.CopyN] and [io.CopyBuffer].
|
||||||
|
|
||||||
|
Despite what the docs may suggest, [io.CopyN] does NOT *read* n bytes from src AND write n bytes to dst.
|
||||||
|
Instead, it always reads 32 KiB from src, and writes n bytes to dst.
|
||||||
|
|
||||||
|
There are cases where this is dreadfully undesired.
|
||||||
|
|
||||||
|
One can, of course, use [io.CopyBuffer], but this is a bit annoying since you then have to provide a buffer yourself.
|
||||||
|
|
||||||
|
This convenience-wraps [io.CopyBuffer] to have a similar signature to [io.CopyN] but properly uses n for both reading and writing.
|
||||||
|
*/
|
||||||
|
func CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
|
||||||
|
if n <= 0 {
|
||||||
|
err = ErrBufTooSmall
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b = make([]byte, n)
|
||||||
|
|
||||||
|
written, err = io.CopyBuffer(dst, src, b)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyCtxBufN copies from `src` to `dst`, `n` bytes at a time, interruptible by `ctx`.
|
||||||
|
func CopyCtxBufN(ctx context.Context, dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||||
|
|
||||||
|
var nr int
|
||||||
|
var nw int
|
||||||
|
var end bool
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
if ctx == nil {
|
||||||
|
err = ErrNilCtx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n <= 0 {
|
||||||
|
err = ErrBufTooSmall
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endCopy:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
err = ctx.Err()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
buf = make([]byte, n)
|
||||||
|
nr, err = src.Read(buf)
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
end = true
|
||||||
|
} else if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf = buf[:nr]
|
||||||
|
|
||||||
|
if nw, err = dst.Write(buf); err != nil {
|
||||||
|
written += int64(nw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
written += int64(nw)
|
||||||
|
if len(buf) != nw {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end {
|
||||||
|
break endCopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CopyBufWith allows for specifying a buffer allocator function, otherwise acts as [CopyBufN].
|
||||||
|
|
||||||
|
bufFunc *MUST NOT* return a nil or len == 0 buffer. [ErrBufTooSmall] will be returned if it does.
|
||||||
|
|
||||||
|
This uses a fixed buffer size from a single call to `bufFunc`.
|
||||||
|
If you need something with dynamic buffer sizing according to some state, use [CopyBufWithDynamic] instead.
|
||||||
|
(Note that CopyBufWithDynamic is generally a little slower, but it should only be noticeable on very large amounts of data.)
|
||||||
|
*/
|
||||||
|
func CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||||
|
|
||||||
|
var buf []byte = bufFunc()
|
||||||
|
|
||||||
|
if buf == nil || len(buf) == 0 {
|
||||||
|
err = ErrBufTooSmall
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
written, err = io.CopyBuffer(dst, src, buf)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CopyBufWithDynamic is like [CopyBufWith] except it will call bufFunc after each previous buffer is written.
|
||||||
|
|
||||||
|
That is to say (using a particularly contrived example):
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
func dynBuf() (b []byte) {
|
||||||
|
|
||||||
|
var t time.Time = time.Now()
|
||||||
|
|
||||||
|
b = make([]byte, t.Seconds())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
CopyBufWithDynamic(w, r, dynBuf)
|
||||||
|
|
||||||
|
will use a buffer sized to the seconds of the time it reads in/writes out the next buffer, whereas with [CopyBufWith]:
|
||||||
|
|
||||||
|
CopyBufWith(w, r, dynBuf)
|
||||||
|
|
||||||
|
would use a *fixed* buffer size of whatever the seconds was equal to at the time of the *first call* to dynBuf.
|
||||||
|
|
||||||
|
`src` MUST return an [io.EOF] when its end is reached, but (as per e.g. [io.CopyBuffer]) the io.EOF error will not
|
||||||
|
be returned from CopyBufWithDynamic. (Any/all other errors encountered will be returned, however, and copying will
|
||||||
|
immediately cease.)
|
||||||
|
*/
|
||||||
|
func CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||||
|
|
||||||
|
var nr int
|
||||||
|
var nw int
|
||||||
|
var end bool
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
for {
|
||||||
|
buf = bufFunc()
|
||||||
|
if buf == nil || len(buf) == 0 {
|
||||||
|
err = ErrBufTooSmall
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nr, err = src.Read(buf)
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
end = true
|
||||||
|
} else if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf = buf[:nr]
|
||||||
|
|
||||||
|
if nw, err = dst.Write(buf); err != nil {
|
||||||
|
written += int64(nw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
written += int64(nw)
|
||||||
|
if len(buf) != nw {
|
||||||
|
err = ErrShortWrite
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChunker returns a [ChunkLocker] ready to use.
|
||||||
|
func NewChunker(chunkSize uint) (c *ChunkLocker, err error) {
|
||||||
|
|
||||||
|
c = &ChunkLocker{}
|
||||||
|
err = c.SetChunkLen(chunkSize)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCtxIO returns a [CtxIO].
|
||||||
|
func NewCtxIO(ctx context.Context, r io.Reader, w io.Writer, chunkSize uint) (c *CtxIO, err error) {
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
err = ErrNilReader
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if w == nil {
|
||||||
|
err = ErrNilWriter
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if chunkSize == 0 {
|
||||||
|
err = ErrInvalidChunkSize
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx == nil {
|
||||||
|
err = ErrNilCtx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c = &CtxIO{
|
||||||
|
r: r,
|
||||||
|
w: w,
|
||||||
|
l: ChunkLocker{
|
||||||
|
chunkLen: chunkSize,
|
||||||
|
},
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewXIO returns a nil [XIO].
|
||||||
|
|
||||||
|
A weird "feature" of Golang is that a nil XIO is perfectly fine to use;
|
||||||
|
it's completely stateless and only has pointer receivers that only work with passed in
|
||||||
|
values so `new(XIO)` is completely unnecessary (as is NewXCopier).
|
||||||
|
In other words, this works fine:
|
||||||
|
|
||||||
|
var xc *iox.XIO
|
||||||
|
|
||||||
|
if n, err = xc.Copy(w, r); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
This function is just to maintain cleaner-looking code if you should so need it,
|
||||||
|
or want an XIO without declaring one:
|
||||||
|
|
||||||
|
if n, err = iox.NewXCopier().Copy(w, r); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
func NewXIO() (x *XIO) {
|
||||||
|
// No-op lel
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package iox
|
||||||
|
|
||||||
|
// GetChunkLen returns the current chunk size/length in bytes.
|
||||||
|
func (c *ChunkLocker) GetChunkLen() (size uint) {
|
||||||
|
|
||||||
|
c.lock.RLock()
|
||||||
|
defer c.lock.RUnlock()
|
||||||
|
|
||||||
|
size = c.chunkLen
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChunkLen sets the current chunk size/length in bytes.
|
||||||
|
func (c *ChunkLocker) SetChunkLen(size uint) (err error) {
|
||||||
|
|
||||||
|
if size == 0 {
|
||||||
|
err = ErrInvalidChunkSize
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
c.chunkLen = size
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package iox
|
||||||
|
|
||||||
|
import (
|
||||||
|
`bytes`
|
||||||
|
`context`
|
||||||
|
`io`
|
||||||
|
`math`
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CtxIO) Copy(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||||
|
if c.l.chunkLen > math.MaxInt64 {
|
||||||
|
err = ErrChunkTooBig
|
||||||
|
}
|
||||||
|
return CopyCtxBufN(c.ctx, dst, src, int64(c.l.chunkLen))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||||
|
if n <= 0 {
|
||||||
|
err = ErrBufTooSmall
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return CopyCtxBufN(c.ctx, dst, src, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) GetChunkLen() (size uint) {
|
||||||
|
return c.l.GetChunkLen()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) Len() (unread int) {
|
||||||
|
return c.buf.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) Read(p []byte) (n int, err error) {
|
||||||
|
|
||||||
|
var nr int64
|
||||||
|
|
||||||
|
if nr, err = c.ReadWithContext(c.ctx, p); err != nil {
|
||||||
|
if nr > math.MaxInt {
|
||||||
|
n = math.MaxInt
|
||||||
|
} else {
|
||||||
|
n = int(nr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nr > math.MaxInt {
|
||||||
|
n = math.MaxInt
|
||||||
|
} else {
|
||||||
|
n = int(nr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) ReadWithContext(ctx context.Context, p []byte) (n int64, err error) {
|
||||||
|
|
||||||
|
var nr int
|
||||||
|
var off int
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
if p == nil || len(p) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.buf.Len() == 0 {
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.l.chunkLen > uint(len(p)) {
|
||||||
|
// Would normally be a single chunk, so one-shot it.
|
||||||
|
nr, err = c.buf.Read(p)
|
||||||
|
n = int64(nr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk over it.
|
||||||
|
endRead:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
err = ctx.Err()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
/*
|
||||||
|
off(set) is the index of the *next position* to write to.
|
||||||
|
Therefore the last offset == len(p),
|
||||||
|
therefore:
|
||||||
|
|
||||||
|
* if off == len(p), "done" (return no error, do *not* read from buf)
|
||||||
|
* if off + c.l.chunkLen > len(p), buf should be len(p) - off instead
|
||||||
|
*/
|
||||||
|
if off == len(p) {
|
||||||
|
break endRead
|
||||||
|
}
|
||||||
|
if uint(off)+c.l.chunkLen > uint(len(p)) {
|
||||||
|
buf = make([]byte, len(p)-off)
|
||||||
|
} else {
|
||||||
|
buf = make([]byte, c.l.chunkLen)
|
||||||
|
}
|
||||||
|
nr, err = c.buf.Read(buf)
|
||||||
|
n += int64(nr)
|
||||||
|
if nr > 0 {
|
||||||
|
off += nr
|
||||||
|
copy(p[off:], buf[:nr])
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break endRead
|
||||||
|
} else if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) SetChunkLen(size uint) (err error) {
|
||||||
|
return c.l.SetChunkLen(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) SetContext(ctx context.Context) (err error) {
|
||||||
|
|
||||||
|
if ctx == nil {
|
||||||
|
err = ErrNilCtx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ctx = ctx
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) Write(p []byte) (n int, err error) {
|
||||||
|
|
||||||
|
var nw int64
|
||||||
|
|
||||||
|
if c.l.chunkLen > math.MaxInt64 {
|
||||||
|
err = ErrChunkTooBig
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if nw, err = c.WriteNWithContext(c.ctx, p, int64(c.l.chunkLen)); err != nil {
|
||||||
|
if nw > math.MaxInt {
|
||||||
|
n = math.MaxInt
|
||||||
|
} else {
|
||||||
|
n = int(nw)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nw > math.MaxInt {
|
||||||
|
n = math.MaxInt
|
||||||
|
} else {
|
||||||
|
n = int(nw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) WriteNWithContext(ctx context.Context, p []byte, n int64) (written int64, err error) {
|
||||||
|
return CopyCtxBufN(ctx, &c.buf, bytes.NewReader(p), n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) WriteRune(r rune) (n int, err error) {
|
||||||
|
|
||||||
|
// We don't even bother listening for the ctx.Done because it's a single rune.
|
||||||
|
n, err = c.buf.WriteRune(r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CtxIO) WriteWithContext(ctx context.Context, p []byte) (n int64, err error) {
|
||||||
|
if c.l.chunkLen > math.MaxInt64 {
|
||||||
|
err = ErrChunkTooBig
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return CopyCtxBufN(ctx, &c.buf, bytes.NewReader(p), int64(c.l.chunkLen))
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package iox
|
||||||
|
|
||||||
|
import (
|
||||||
|
`io`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy copies [io.Reader] `src` to [io.Writer] `dst`. It implements [Copier].
|
||||||
|
func (x *XIO) Copy(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||||
|
return io.Copy(dst, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyBuffer copies [io.Reader] `src` to [io.Writer] `dst` using buffer `buf`. It implements [CopyBufferer].
|
||||||
|
func (x *XIO) CopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) {
|
||||||
|
return io.CopyBuffer(dst, src, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyBufWith copies [io.Reader] `src` to [io.Writer] `dst` using buffer returner `bufFunc`. It implements [SizedCopyBufferInvoker].
|
||||||
|
func (x *XIO) CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||||
|
return CopyBufWith(dst, src, bufFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyBufWithDynamic copies [io.Reader] `src` to [io.Writer] `dst` using buffer returner `bufFunc` for each chunk. It implements [DynamicSizedCopyBufferInvoker].
|
||||||
|
func (x *XIO) CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error) {
|
||||||
|
return CopyBufWithDynamic(dst, src, bufFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CopyBufN reads buffered bytes from [io.Reader] `src` and copies to [io.Writer] `dst`
|
||||||
|
using the synchronous buffer size `n`.
|
||||||
|
|
||||||
|
It implements [SizedCopyBufferer].
|
||||||
|
*/
|
||||||
|
func (x *XIO) CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||||
|
return CopyBufN(dst, src, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyN copies from [io.Reader] `src` to [io.Writer] `w`, `n` bytes at a time. It implements [SizedCopier].
|
||||||
|
func (x *XIO) CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
|
||||||
|
return io.CopyN(dst, src, n)
|
||||||
|
}
|
||||||
+221
@@ -0,0 +1,221 @@
|
|||||||
|
package iox
|
||||||
|
|
||||||
|
import (
|
||||||
|
`bytes`
|
||||||
|
`context`
|
||||||
|
`io`
|
||||||
|
`sync`
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
/*
|
||||||
|
RuneWriter matches the behavior of [bytes.Buffer.WriteRune] and [bufio.Writer.WriteRune].
|
||||||
|
|
||||||
|
(Note that this package does not have a "RuneReader"; see [io.RuneReader] instead.)
|
||||||
|
*/
|
||||||
|
RuneWriter interface {
|
||||||
|
WriteRune(r rune) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copier matches the signature/behavior of [io.Copy]. Implemented by [XIO].
|
||||||
|
Copier interface {
|
||||||
|
Copy(dst io.Writer, src io.Reader) (written int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyBufferer matches the signature/behavior of [io.CopyBuffer]. Implemented by [XIO].
|
||||||
|
CopyBufferer interface {
|
||||||
|
CopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizedCopier matches the signature/behavior of [io.CopyN]. Implemented by [XIO].
|
||||||
|
SizedCopier interface {
|
||||||
|
CopyN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizedCopyBufferer matches the signature/behavior of [CopyBufN]. Implemented by [XIO].
|
||||||
|
SizedCopyBufferer interface {
|
||||||
|
CopyBufN(dst io.Writer, src io.Reader, n int64) (written int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizedCopyBufferInvoker matches the signature/behavior of [CopyBufWith]. Implemented by [XIO].
|
||||||
|
SizedCopyBufferInvoker interface {
|
||||||
|
CopyBufWith(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicSizedCopyBufferInvoker matches the signature/behavior of [CopyBufWithDynamic]. Implemented by [XIO].
|
||||||
|
DynamicSizedCopyBufferInvoker interface {
|
||||||
|
CopyBufWithDynamic(dst io.Writer, src io.Reader, bufFunc func() (b []byte)) (written int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Chunker is used by both [ContextReader] and [ContextWriter] to set/get the current chunk size.
|
||||||
|
Chunking is inherently required to be specified in order to interrupt reads/writes/copies with a [context.Context].
|
||||||
|
|
||||||
|
Implementations *must* use a [sync.RWMutex] to get (RLock) and set (Lock) the chunk size.
|
||||||
|
The chunk size *must not* be directly accessible to maintain concurrency safety assumptions.
|
||||||
|
*/
|
||||||
|
Chunker interface {
|
||||||
|
// GetChunkLen returns the current chunk size/length in bytes.
|
||||||
|
GetChunkLen() (size uint)
|
||||||
|
// SetChunkLen sets the current chunk size/length in bytes.
|
||||||
|
SetChunkLen(size uint) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ChunkReader implements a chunking reader.
|
||||||
|
Third-party implementations *must* respect the chunk size locking (see [Chunker]).
|
||||||
|
|
||||||
|
The Read method should read in chunks of the internal chunk size.
|
||||||
|
*/
|
||||||
|
ChunkReader interface {
|
||||||
|
io.Reader
|
||||||
|
Chunker
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ChunkWriter implements a chunking writer.
|
||||||
|
Third-party implementations *must* respect the chunk size locking (see [Chunker]).
|
||||||
|
|
||||||
|
The Write method should write out in chunks of the internal chunk size.
|
||||||
|
*/
|
||||||
|
ChunkWriter interface {
|
||||||
|
io.Writer
|
||||||
|
Chunker
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkReadWriter implements a chunking reader/writer.
|
||||||
|
ChunkReadWriter interface {
|
||||||
|
ChunkReader
|
||||||
|
ChunkWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ContextSetter allows one to set an internal context.
|
||||||
|
|
||||||
|
A nil context should return an error.
|
||||||
|
*/
|
||||||
|
ContextSetter interface {
|
||||||
|
SetContext(context context.Context) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ContextCopier is defined to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||||
|
|
||||||
|
The Copy method should use an internal context and chunk size
|
||||||
|
(and thus wrap [CopyCtxBufN] internally on an external call to Copy, etc.).
|
||||||
|
*/
|
||||||
|
ContextCopier interface {
|
||||||
|
Copier
|
||||||
|
Chunker
|
||||||
|
ContextSetter
|
||||||
|
SizedCopyBufferer
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ContextReader is primarily here to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||||
|
|
||||||
|
The Read method should use an internal context and chunk size.
|
||||||
|
|
||||||
|
The ReadWithContext method should use an internal chunk size.
|
||||||
|
*/
|
||||||
|
ContextReader interface {
|
||||||
|
ChunkReader
|
||||||
|
ContextSetter
|
||||||
|
ReadWithContext(ctx context.Context, p []byte) (n int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ContextWriter is primarily here to allow for consumer-provided types. See [CtxIO] for a package-provided type.
|
||||||
|
|
||||||
|
The Write method should use an internal context.
|
||||||
|
|
||||||
|
The WriteWithContext should use an internal chunk size.
|
||||||
|
*/
|
||||||
|
ContextWriter interface {
|
||||||
|
ChunkWriter
|
||||||
|
ContextSetter
|
||||||
|
WriteWithContext(ctx context.Context, p []byte) (n int64, err error)
|
||||||
|
WriteNWithContext(ctx context.Context, p []byte, n int64) (written int64, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ContextReadWriter is primarily here to allow for consumer-provided types.
|
||||||
|
|
||||||
|
See [CtxIO] for a package-provided type.
|
||||||
|
*/
|
||||||
|
ContextReadWriter interface {
|
||||||
|
ContextReader
|
||||||
|
ContextWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
LenReader allows one to read bytes (conforming to [io.Reader]),
|
||||||
|
and also provides a Len() method which returns an int of remaining unread bytes.
|
||||||
|
[bytes.Buffer] and [bytes.Reader] conforms to this, but see [CtxIO] for
|
||||||
|
a package-provided type.
|
||||||
|
*/
|
||||||
|
LenReader interface {
|
||||||
|
io.Reader
|
||||||
|
Len() (unread int)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ChunkLocker implements [Chunker].
|
||||||
|
ChunkLocker struct {
|
||||||
|
lock sync.RWMutex
|
||||||
|
chunkLen uint
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CtxIO is a type used to demonstrate "stateful" I/O introduced by this package.
|
||||||
|
It implements:
|
||||||
|
|
||||||
|
* [Copier]
|
||||||
|
* [Chunker]
|
||||||
|
* [RuneWriter]
|
||||||
|
* [ChunkReader]
|
||||||
|
* [ChunkWriter]
|
||||||
|
* [ContextCopier]
|
||||||
|
* [ContextSetter]
|
||||||
|
* [ContextReader]
|
||||||
|
* [ContextWriter]
|
||||||
|
* [ChunkReadWriter]
|
||||||
|
* [ContextReadWriter]
|
||||||
|
* [SizedCopyBufferer]
|
||||||
|
* [LenReader]
|
||||||
|
|
||||||
|
Unlike [XIO], it must be non-nil (see [NewCtxIO]) since it maintains state
|
||||||
|
(though technically, one does not need to call [NewCtxIO] if they call
|
||||||
|
[CtxIO.SetChunkLen] and [CtxIO.SetContext] before any other methods).
|
||||||
|
|
||||||
|
[CtxIO.Read] and other Read methods writes to an internal buffer,
|
||||||
|
and [CtxIO.Write] and other Write methods writes out from it.
|
||||||
|
*/
|
||||||
|
CtxIO struct {
|
||||||
|
r io.Reader
|
||||||
|
w io.Writer
|
||||||
|
l ChunkLocker
|
||||||
|
buf bytes.Buffer
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
XIO is a type used to demonstrate "stateless" I/O introduced by this package.
|
||||||
|
It implements:
|
||||||
|
|
||||||
|
* [Copier]
|
||||||
|
* [CopyBufferer]
|
||||||
|
* [SizedCopier]
|
||||||
|
* [SizedCopyBufferer]
|
||||||
|
* [SizedCopyBufferInvoker]
|
||||||
|
* [DynamicSizedCopyBufferInvoker]
|
||||||
|
|
||||||
|
Unlike [CtxIO], the zero-value is ready to use since it holds no state
|
||||||
|
or configuration whatsoever.
|
||||||
|
|
||||||
|
A nil XIO is perfectly usable but if you want something more idiomatic,
|
||||||
|
see [NewXIO].
|
||||||
|
*/
|
||||||
|
XIO struct{}
|
||||||
|
)
|
||||||
+19
-1
@@ -1,9 +1,27 @@
|
|||||||
|
- ScopedLogger, take an io.Writer for each log level
|
||||||
|
|
||||||
|
- logging probably needs mutexes
|
||||||
|
|
||||||
|
- macOS support beyond the legacy NIX stuff. it apparently uses something called "ULS", "Unified Logging System".
|
||||||
|
-- https://developer.apple.com/documentation/os/logging
|
||||||
|
-- https://developer.apple.com/documentation/os/generating-log-messages-from-your-code
|
||||||
|
-- no native Go support (yet)?
|
||||||
|
--- https://developer.apple.com/forums/thread/773369
|
||||||
|
|
||||||
|
- The log destinations for e.g. consts_nix.go et. al. probably should be unexported types.
|
||||||
|
|
||||||
|
- add a `log/slog` logging.Logger?
|
||||||
|
|
||||||
- Implement code line/func/etc. (only for debug?):
|
- Implement code line/func/etc. (only for debug?):
|
||||||
https://stackoverflow.com/a/24809646
|
https://stackoverflow.com/a/24809646
|
||||||
https://golang.org/pkg/runtime/#Caller
|
https://golang.org/pkg/runtime/#Caller
|
||||||
-- log.LlongFile and log.Lshortfile flags don't currently work properly for StdLogger/FileLogger; they refer to the file in logging package rather than the caller.
|
-- 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.
|
||||||
|
|
||||||
- Suport remote loggers? (eventlog, syslog, systemd)
|
- StdLvlLogger; where stdout and stderr are both logged to depending on severity level.
|
||||||
|
- make configurable via OR bitmask
|
||||||
|
|
||||||
|
- Suport remote loggers? (eventlog, syslog, journald)
|
||||||
|
|
||||||
- JSON logger? YAML logger? XML logger?
|
- JSON logger? YAML logger? XML logger?
|
||||||
|
|
||||||
|
|||||||
@@ -12,3 +12,16 @@ const (
|
|||||||
// appendFlags are the flags used for testing the file (and opening/writing).
|
// appendFlags are the flags used for testing the file (and opening/writing).
|
||||||
appendFlags int = os.O_APPEND | os.O_CREATE | os.O_WRONLY
|
appendFlags int = os.O_APPEND | os.O_CREATE | os.O_WRONLY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PriorityNone logPrio = 0
|
||||||
|
const (
|
||||||
|
PriorityEmergency logPrio = 1 << iota
|
||||||
|
PriorityAlert
|
||||||
|
PriorityCritical
|
||||||
|
PriorityError
|
||||||
|
PriorityWarning
|
||||||
|
PriorityNotice
|
||||||
|
PriorityInformational
|
||||||
|
PriorityDebug
|
||||||
|
)
|
||||||
|
const PriorityAll logPrio = PriorityEmergency | PriorityAlert | PriorityCritical | PriorityError | PriorityWarning | PriorityNotice | PriorityInformational | PriorityDebug
|
||||||
@@ -1,32 +1,5 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
|
||||||
`log/syslog`
|
|
||||||
|
|
||||||
`r00t2.io/goutils/bitmask`
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// devlog is the path to the syslog char device.
|
|
||||||
devlog string = "/dev/log"
|
|
||||||
// syslogFacility is the facility to use; it's a little like a context or scope if you think of it in those terms.
|
|
||||||
syslogFacility syslog.Priority = syslog.LOG_USER
|
|
||||||
)
|
|
||||||
|
|
||||||
// Flags for logger configuration. These are used internally.
|
|
||||||
const (
|
|
||||||
// LogUndefined indicates an undefined Logger type.
|
|
||||||
LogUndefined bitmask.MaskBit = 1 << iota
|
|
||||||
// LogJournald flags a SystemDLogger Logger type.
|
|
||||||
LogJournald
|
|
||||||
// LogSyslog flags a SyslogLogger Logger type.
|
|
||||||
LogSyslog
|
|
||||||
// LogFile flags a FileLogger Logger type.
|
|
||||||
LogFile
|
|
||||||
// LogStdout flags a StdLogger Logger type.
|
|
||||||
LogStdout
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// defLogPaths indicates default log paths.
|
// defLogPaths indicates default log paths.
|
||||||
defLogPaths = []string{
|
defLogPaths = []string{
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//go:build !(windows || plan9 || wasip1 || js || ios)
|
||||||
|
|
||||||
|
// I mean maybe it works for plan9 and ios, I don't know.
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/syslog"
|
||||||
|
|
||||||
|
"r00t2.io/goutils/bitmask"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// devlog is the path to the syslog char device.
|
||||||
|
devlog string = "/dev/log"
|
||||||
|
// syslogFacility is the facility to use; it's a little like a context or scope if you think of it in those terms.
|
||||||
|
syslogFacility syslog.Priority = syslog.LOG_USER
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flags for logger configuration. These are used internally.
|
||||||
|
|
||||||
|
// LogUndefined indicates an undefined Logger type.
|
||||||
|
const LogUndefined bitmask.MaskBit = iota
|
||||||
|
const (
|
||||||
|
// LogJournald flags a SystemDLogger Logger type. This will, for hopefully obvious reasons, only work on Linux systemd systems.
|
||||||
|
LogJournald bitmask.MaskBit = 1 << iota
|
||||||
|
// LogSyslog flags a SyslogLogger Logger type.
|
||||||
|
LogSyslog
|
||||||
|
// LogFile flags a FileLogger Logger type.
|
||||||
|
LogFile
|
||||||
|
// LogStdout flags a StdLogger Logger type.
|
||||||
|
LogStdout
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
var (
|
||||||
|
// defLogPaths indicates default log paths.
|
||||||
|
defLogPaths = []string{
|
||||||
|
"/var/log/golang/program.log",
|
||||||
|
"~/Library/Logs/Golang/program.log",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Flags for logger configuration. These are used internally.
|
// Flags for logger configuration. These are used internally.
|
||||||
|
// LogUndefined indicates an undefined Logger type.
|
||||||
|
const LogUndefined bitmask.MaskBit = 0
|
||||||
const (
|
const (
|
||||||
// LogUndefined indicates an undefined Logger type.
|
|
||||||
LogUndefined bitmask.MaskBit = 1 << iota
|
|
||||||
// LogWinLogger indicates a WinLogger Logger type (Event Log).
|
// LogWinLogger indicates a WinLogger Logger type (Event Log).
|
||||||
LogWinLogger
|
LogWinLogger bitmask.MaskBit = 1 << iota
|
||||||
// LogFile flags a FileLogger Logger type.
|
// LogFile flags a FileLogger Logger type.
|
||||||
LogFile
|
LogFile
|
||||||
// LogStdout flags a StdLogger Logger type.
|
// LogStdout flags a StdLogger Logger type.
|
||||||
|
|||||||
+25
-8
@@ -3,18 +3,22 @@ Package logging implements and presents various loggers under a unified interfac
|
|||||||
|
|
||||||
These particular loggers (logging.Logger) available are:
|
These particular loggers (logging.Logger) available are:
|
||||||
|
|
||||||
StdLogger
|
NullLogger
|
||||||
FileLogger
|
StdLogger
|
||||||
SystemDLogger (Linux only)
|
FileLogger
|
||||||
SyslogLogger (Linux only)
|
SystemDLogger (Linux only)
|
||||||
WinLogger (Windows only)
|
SyslogLogger (Linux/macOS/other *NIX-like only)
|
||||||
|
WinLogger (Windows only)
|
||||||
|
|
||||||
There is a sixth type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call.
|
There is a seventh type of logging.Logger, MultiLogger, that allows for multiple loggers to be written to with a single call.
|
||||||
|
(This is similar to stdlib's io.MultiWriter()'s return value, but with priority awareness and fmt string support).
|
||||||
|
|
||||||
Note that for some Loggers, the prefix may be modified - "literal" loggers (StdLogger and FileLogger) will append a space to the end of the prefix.
|
As you may have guessed, NullLogger doesn't actually log anything but is fully "functional" as a logging.Logger (similar to io.discard/io.Discard()'s return).
|
||||||
|
|
||||||
|
Note that for some Loggers, the prefix may be modified after the Logger has already initialized.
|
||||||
|
"Literal" loggers (StdLogger and FileLogger) will append a space to the end of the prefix by default.
|
||||||
If this is undesired (unlikely), you will need to modify (Logger).Prefix and run (Logger).Logger.SetPrefix(yourPrefixHere) for the respective logger.
|
If this is undesired (unlikely), you will need to modify (Logger).Prefix and run (Logger).Logger.SetPrefix(yourPrefixHere) for the respective logger.
|
||||||
|
|
||||||
|
|
||||||
Every logging.Logger type has the following methods that correspond to certain "levels".
|
Every logging.Logger type has the following methods that correspond to certain "levels".
|
||||||
|
|
||||||
Alert(s string, v ...interface{}) (err error)
|
Alert(s string, v ...interface{}) (err error)
|
||||||
@@ -36,6 +40,7 @@ Note that in the case of a MultiLogger, err (if not nil) will be a (r00t2.io/gou
|
|||||||
logging.Logger types also have the following methods:
|
logging.Logger types also have the following methods:
|
||||||
|
|
||||||
DoDebug(d bool) (err error)
|
DoDebug(d bool) (err error)
|
||||||
|
GetDebug() (d bool)
|
||||||
SetPrefix(p string) (err error)
|
SetPrefix(p string) (err error)
|
||||||
GetPrefix() (p string, err error)
|
GetPrefix() (p string, err error)
|
||||||
Setup() (err error)
|
Setup() (err error)
|
||||||
@@ -43,5 +48,17 @@ logging.Logger types also have the following methods:
|
|||||||
|
|
||||||
In some cases, Logger.Setup and Logger.Shutdown are no-ops. In other cases, they perform necessary initialization/cleanup and closing of the logger.
|
In some cases, Logger.Setup and Logger.Shutdown are no-ops. In other cases, they perform necessary initialization/cleanup and closing of the logger.
|
||||||
It is recommended to *always* run Setup and Shutdown before and after using, respectively, regardless of the actual logging.Logger type.
|
It is recommended to *always* run Setup and Shutdown before and after using, respectively, regardless of the actual logging.Logger type.
|
||||||
|
|
||||||
|
Lastly, all logging.Loggers have a ToLogger() method. This returns a *log.Logger (from stdlib log), which also conforms to io.Writer inherently.
|
||||||
|
In addition. all have a ToRaw() method, which extends a Logger even further and returns an unexported type (*logging.logWriter) compatible with:
|
||||||
|
|
||||||
|
- io.ByteWriter
|
||||||
|
- io.Writer
|
||||||
|
- io.WriteCloser (Shutdown() on the Logger backend is called during Close(), rendering the underlying Logger unsafe to use afterwards)
|
||||||
|
- io.StringWriter
|
||||||
|
|
||||||
|
and, if stdlib io ever defines an e.g. RuneWriter (WriteRune(r rune) (n int, err error)), it will conform to that too (see (r00t2.io/goutils/iox).RuneWriter).
|
||||||
|
Obviously this and io.ByteWriter are fairly silly, as they're intended to be high-speed throughput-optimized methods, but if you wanted to e.g.
|
||||||
|
log every single byte on a wire as a separate log message, go ahead; I'm not your dad.
|
||||||
*/
|
*/
|
||||||
package logging
|
package logging
|
||||||
+3
-1
@@ -1,7 +1,7 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`errors`
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -12,6 +12,8 @@ var (
|
|||||||
exists with too restrictive perms to write/append to, and/or could not be created.
|
exists with too restrictive perms to write/append to, and/or could not be created.
|
||||||
*/
|
*/
|
||||||
ErrInvalidFile error = errors.New("a FileLogger was requested but the file does not exist and cannot be created")
|
ErrInvalidFile error = errors.New("a FileLogger was requested but the file does not exist and cannot be created")
|
||||||
|
// ErrInvalidRune is returned if a rune was expected but it is not a valid UTF-8 codepoint.
|
||||||
|
ErrInvalidRune error = errors.New("specified rune is not valid UTF-8 codepoint")
|
||||||
// ErrNoEntry indicates that the user attempted to MultiLogger.RemoveLogger a Logger but one by that identifier does not exist.
|
// ErrNoEntry indicates that the user attempted to MultiLogger.RemoveLogger a Logger but one by that identifier does not exist.
|
||||||
ErrNoEntry error = errors.New("the Logger specified to be removed does not exist")
|
ErrNoEntry error = errors.New("the Logger specified to be removed does not exist")
|
||||||
)
|
)
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`errors`
|
"errors"
|
||||||
`fmt`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNoSysD indicates that the user attempted to add a SystemDLogger to a MultiLogger but systemd is unavailable.
|
// ErrNoSysD indicates that the user attempted to add a SystemDLogger to a MultiLogger but systemd is unavailable.
|
||||||
ErrNoSysD error = errors.New("a systemd (journald) Logger was requested but systemd is unavailable on this system")
|
ErrNoSysD error = errors.New("a systemd (journald) Logger was requested but systemd is unavailable on this system")
|
||||||
// ErrNoSyslog indicates that the user attempted to add a SyslogLogger to a MultiLogger but syslog's logger device is unavailable.
|
|
||||||
ErrNoSyslog error = errors.New("a Syslog Logger was requested but Syslog is unavailable on this system")
|
|
||||||
/*
|
|
||||||
ErrInvalidDevLog indicates that the user attempted to add a SyslogLogger to a MultiLogger but
|
|
||||||
the Syslog char device file is... not actually a char device file.
|
|
||||||
*/
|
|
||||||
ErrInvalidDevLog error = errors.New(fmt.Sprintf("a Syslog Logger was requested but %v is not a valid logger handle", devlog))
|
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build !(windows || plan9 || wasip1 || js || ios)
|
||||||
|
// +build !windows,!plan9,!wasip1,!js,!ios
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoSyslog indicates that the user attempted to add a SyslogLogger to a MultiLogger but syslog's logger device is unavailable.
|
||||||
|
ErrNoSyslog error = errors.New("a Syslog Logger was requested but Syslog is unavailable on this system")
|
||||||
|
/*
|
||||||
|
ErrInvalidDevLog indicates that the user attempted to add a SyslogLogger to a MultiLogger but
|
||||||
|
the Syslog char device file is... not actually a char device file.
|
||||||
|
*/
|
||||||
|
ErrInvalidDevLog error = errors.New(fmt.Sprintf("a Syslog Logger was requested but %v is not a valid logger handle", devlog))
|
||||||
|
)
|
||||||
@@ -1,9 +1,33 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
ToLog returns a stdlib *log.Logger from a logging.Logger. It simply wraps the (logging.Logger).ToLogger() methods.
|
||||||
|
|
||||||
|
prio is an OR'd logPrio of the Priority* constants.
|
||||||
|
*/
|
||||||
|
func ToLog(l Logger, prio logPrio) (stdLibLog *log.Logger) {
|
||||||
|
|
||||||
|
stdLibLog = l.ToLogger(prio)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRaw returns a *logWriter from a logging.Logger. It is an alternative to the (logging.Logger).ToRaw() methods.
|
||||||
|
func ToRaw(l Logger, prio logPrio) (raw *logWriter) {
|
||||||
|
|
||||||
|
raw = &logWriter{
|
||||||
|
backend: l,
|
||||||
|
prio: prio,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// testOpen attempts to open a file for writing to test for suitability as a LogFile path.
|
// testOpen attempts to open a file for writing to test for suitability as a LogFile path.
|
||||||
func testOpen(path string) (success bool, err error) {
|
func testOpen(path string) (success bool, err error) {
|
||||||
|
|
||||||
|
|||||||
+34
-10
@@ -1,12 +1,12 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`errors`
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
`io/fs`
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
`strings`
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup sets up/configures a FileLogger and prepares it for use.
|
// Setup sets up/configures a FileLogger and prepares it for use.
|
||||||
@@ -43,8 +43,8 @@ func (l *FileLogger) Shutdown() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GetPrefix returns the prefix used by this FileLogger.
|
GetPrefix returns the prefix used by this FileLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *FileLogger) GetPrefix() (prefix string, err error) {
|
func (l *FileLogger) GetPrefix() (prefix string, err error) {
|
||||||
|
|
||||||
@@ -54,9 +54,9 @@ func (l *FileLogger) GetPrefix() (prefix string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
DoDebug sets the debug state of this FileLogger.
|
DoDebug sets the debug state of this FileLogger.
|
||||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *FileLogger) DoDebug(d bool) (err error) {
|
func (l *FileLogger) DoDebug(d bool) (err error) {
|
||||||
|
|
||||||
@@ -65,9 +65,17 @@ func (l *FileLogger) DoDebug(d bool) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDebug returns the debug status of this FileLogger.
|
||||||
|
func (l *FileLogger) GetDebug() (d bool) {
|
||||||
|
|
||||||
|
d = l.EnableDebug
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
SetPrefix sets the prefix for this FileLogger.
|
SetPrefix sets the prefix for this FileLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *FileLogger) SetPrefix(prefix string) (err error) {
|
func (l *FileLogger) SetPrefix(prefix string) (err error) {
|
||||||
|
|
||||||
@@ -212,6 +220,22 @@ func (l *FileLogger) Warning(s string, v ...interface{}) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToLogger returns a stdlib log.Logger.
|
||||||
|
func (l *FileLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
|
||||||
|
|
||||||
|
stdLibLog = log.New(l.ToRaw(prio), "", 0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRaw returns a *logWriter.
|
||||||
|
func (l *FileLogger) ToRaw(prio logPrio) (raw *logWriter) {
|
||||||
|
|
||||||
|
raw = &logWriter{backend: l, prio: prio}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// renderWrite prepares/formats a log message to be written to this FileLogger.
|
// renderWrite prepares/formats a log message to be written to this FileLogger.
|
||||||
func (l *FileLogger) renderWrite(msg, prio string) {
|
func (l *FileLogger) renderWrite(msg, prio string) {
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
`os`
|
`os`
|
||||||
`path`
|
`path`
|
||||||
|
|
||||||
sysd `github.com/coreos/go-systemd/journal`
|
sysd `github.com/coreos/go-systemd/v22/journal`
|
||||||
`r00t2.io/goutils/bitmask`
|
`r00t2.io/goutils/bitmask`
|
||||||
`r00t2.io/sysutils/paths`
|
`r00t2.io/sysutils/paths`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
`r00t2.io/goutils/bitmask`
|
||||||
|
)
|
||||||
|
|
||||||
|
// HasFlag provides a wrapper for functionality to the underlying bitmask.MaskBit.
|
||||||
|
func (l *logPrio) HasFlag(prio logPrio) (hasFlag bool) {
|
||||||
|
|
||||||
|
var m *bitmask.MaskBit
|
||||||
|
var p *bitmask.MaskBit
|
||||||
|
|
||||||
|
if l == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m = bitmask.NewMaskBitExplicit(uint(*l))
|
||||||
|
p = bitmask.NewMaskBitExplicit(uint(prio))
|
||||||
|
|
||||||
|
// Use IsOneOf instead in case PriorityAll is passed for prio.
|
||||||
|
// hasFlag = m.HasFlag(*p)
|
||||||
|
hasFlag = m.IsOneOf(*p)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"r00t2.io/goutils/multierr"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Close calls Logger.Shutdown() on the underlying Logger.
|
||||||
|
The Logger *must not be used* after this; it will need to be re-initialized with Logger.Setup()
|
||||||
|
or a new Logger (and thuse new logWriter) must be created to replace it.
|
||||||
|
|
||||||
|
It (along with logWriter.Write()) conforms to WriteCloser().
|
||||||
|
*/
|
||||||
|
func (l *logWriter) Close() (err error) {
|
||||||
|
|
||||||
|
if err = l.backend.Shutdown(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Write writes bytes b to the underlying Logger's priority level if the logWriter's priority level(s) match.
|
||||||
|
It conforms to io.Writer. n will *always* == len(b) on success, because otherwise n would technically be >= len(b)
|
||||||
|
(if multiple priorities are enabled), which is undefined behavior per io.Writer.
|
||||||
|
|
||||||
|
b is converted to a string to normalize to the underlying Logger.
|
||||||
|
*/
|
||||||
|
func (l *logWriter) Write(b []byte) (n int, err error) {
|
||||||
|
|
||||||
|
var s string
|
||||||
|
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
||||||
|
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s = string(b)
|
||||||
|
|
||||||
|
// Since this explicitly checks each priority level, there's no need for IsOneOf in case of PriorityAll.
|
||||||
|
|
||||||
|
if l.prio.HasFlag(PriorityEmergency) {
|
||||||
|
if err = l.backend.Emerg(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityAlert) {
|
||||||
|
if err = l.backend.Alert(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityCritical) {
|
||||||
|
if err = l.backend.Crit(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityError) {
|
||||||
|
if err = l.backend.Err(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityWarning) {
|
||||||
|
if err = l.backend.Warning(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityNotice) {
|
||||||
|
if err = l.backend.Notice(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityInformational) {
|
||||||
|
if err = l.backend.Info(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityDebug) {
|
||||||
|
if err = l.backend.Debug(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mErr.IsEmpty() {
|
||||||
|
err = mErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n = len(b)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
WriteByte conforms a logWriter to an io.ByteWriter. (It just wraps logWriter.Write().)
|
||||||
|
You should probably never use this; the logging overhead/prefix is going to be more data than the single byte itself.
|
||||||
|
|
||||||
|
c is converted to a string to normalize to the underlying Logger.
|
||||||
|
*/
|
||||||
|
func (l *logWriter) WriteByte(c byte) (err error) {
|
||||||
|
|
||||||
|
if _, err = l.Write([]byte{c}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
WriteRune follows the same signature of (bytes.Buffer).WriteRune() and (bufio.Writer).WriteRune(); thus if `io` ever defines an io.RuneWriter interface, here ya go.
|
||||||
|
|
||||||
|
n will *always* be equal to (unicode/utf8).RuneLen(r), unless r is an "invalid rune" -- in which case n will be 0 and err will be ErrInvalidRune..
|
||||||
|
*/
|
||||||
|
func (l *logWriter) WriteRune(r rune) (n int, err error) {
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
|
||||||
|
n = utf8.RuneLen(r)
|
||||||
|
if n < 0 {
|
||||||
|
err = ErrInvalidRune
|
||||||
|
n = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b = make([]byte, n)
|
||||||
|
utf8.EncodeRune(b, r)
|
||||||
|
|
||||||
|
if n, err = l.Write(b); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
WriteString writes string s to the underlying Logger's priority level if the logWriter's priority level(s) match.
|
||||||
|
It conforms to io.StringWriter. n will *always* == len(s) on success, because otherwise n would technically be >= len(s)
|
||||||
|
(if multiple priorities are enabled), which is undefined behavior per io.StringWriter.
|
||||||
|
*/
|
||||||
|
func (l *logWriter) WriteString(s string) (n int, err error) {
|
||||||
|
|
||||||
|
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
||||||
|
|
||||||
|
if l.prio.HasFlag(PriorityEmergency) {
|
||||||
|
if err = l.backend.Emerg(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityAlert) {
|
||||||
|
if err = l.backend.Alert(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityCritical) {
|
||||||
|
if err = l.backend.Crit(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityError) {
|
||||||
|
if err = l.backend.Err(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityWarning) {
|
||||||
|
if err = l.backend.Warning(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityNotice) {
|
||||||
|
if err = l.backend.Notice(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityInformational) {
|
||||||
|
if err = l.backend.Info(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.prio.HasFlag(PriorityDebug) {
|
||||||
|
if err = l.backend.Debug(s); err != nil {
|
||||||
|
mErr.AddError(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mErr.IsEmpty() {
|
||||||
|
err = mErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n = len(s)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`errors`
|
"errors"
|
||||||
`fmt`
|
"fmt"
|
||||||
`sync`
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
`r00t2.io/goutils/multierr`
|
"r00t2.io/goutils/multierr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup sets up/configures a MultiLogger (and all its MultiLogger.Loggers) and prepares it for use.
|
// Setup sets up/configures a MultiLogger (and all its MultiLogger.Loggers) and prepares it for use.
|
||||||
@@ -67,8 +68,8 @@ func (m *MultiLogger) Shutdown() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GetPrefix returns the prefix used by this MultiLogger (and all its MultiLogger.Loggers).
|
GetPrefix returns the prefix used by this MultiLogger (and all its MultiLogger.Loggers).
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (m *MultiLogger) GetPrefix() (prefix string, err error) {
|
func (m *MultiLogger) GetPrefix() (prefix string, err error) {
|
||||||
|
|
||||||
@@ -79,10 +80,10 @@ func (m *MultiLogger) GetPrefix() (prefix string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
DoDebug sets the debug state of this MultiLogger (and all its MultiLogger.Loggers).
|
DoDebug sets the debug state of this MultiLogger (and all its MultiLogger.Loggers).
|
||||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||||
|
|
||||||
If you had a logger-specific EnableDebug set, you will need to re-set it to your desired state after running this method.
|
If you had a logger-specific EnableDebug set, you will need to re-set it to your desired state after running this method.
|
||||||
*/
|
*/
|
||||||
func (m *MultiLogger) DoDebug(d bool) (err error) {
|
func (m *MultiLogger) DoDebug(d bool) (err error) {
|
||||||
|
|
||||||
@@ -114,10 +115,18 @@ func (m *MultiLogger) DoDebug(d bool) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// GetDebug returns the debug status of this MultiLogger.
|
||||||
SetPrefix sets the prefix for this MultiLogger (and all its MultiLogger.Loggers).
|
func (m *MultiLogger) GetDebug() (d bool) {
|
||||||
|
|
||||||
If you had a logger-specific Prefix set, you will need to re-set it to your desired prefix after running this method.
|
d = m.EnableDebug
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SetPrefix sets the prefix for this MultiLogger (and all its MultiLogger.Loggers).
|
||||||
|
|
||||||
|
If you had a logger-specific Prefix set, you will need to re-set it to your desired prefix after running this method.
|
||||||
*/
|
*/
|
||||||
func (m *MultiLogger) SetPrefix(prefix string) (err error) {
|
func (m *MultiLogger) SetPrefix(prefix string) (err error) {
|
||||||
|
|
||||||
@@ -362,3 +371,19 @@ func (m *MultiLogger) Warning(s string, v ...interface{}) (err error) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToLogger returns a stdlib log.Logger.
|
||||||
|
func (m *MultiLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
|
||||||
|
|
||||||
|
stdLibLog = log.New(m.ToRaw(prio), "", 0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRaw returns a *logWriter.
|
||||||
|
func (m *MultiLogger) ToRaw(prio logPrio) (raw *logWriter) {
|
||||||
|
|
||||||
|
raw = &logWriter{backend: m, prio: prio}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`path`
|
"path"
|
||||||
|
|
||||||
`github.com/google/uuid`
|
"github.com/google/uuid"
|
||||||
`r00t2.io/sysutils/paths`
|
"r00t2.io/sysutils/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -145,6 +145,40 @@ func (m *MultiLogger) AddFileLogger(identifier string, logFlags int, logfilePath
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
AddNullLogger adds a NullLogger to a MultiLogger.
|
||||||
|
|
||||||
|
identifier is a string to use to identify the added NullLogger in MultiLogger.Loggers.
|
||||||
|
If empty, one will be automatically generated.
|
||||||
|
*/
|
||||||
|
func (m *MultiLogger) AddNullLogger(identifier string) (err error) {
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
var prefix string
|
||||||
|
|
||||||
|
if identifier == "" {
|
||||||
|
identifier = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists = m.Loggers[identifier]; exists {
|
||||||
|
err = ErrExistingLogger
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Loggers[identifier] = &NullLogger{}
|
||||||
|
if err = m.Loggers[identifier].Setup(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveLogger will let you remove a Logger from MultiLogger.Loggers.
|
// RemoveLogger will let you remove a Logger from MultiLogger.Loggers.
|
||||||
func (m *MultiLogger) RemoveLogger(identifier string) (err error) {
|
func (m *MultiLogger) RemoveLogger(identifier string) (err error) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`os`
|
sysd "github.com/coreos/go-systemd/v22/journal"
|
||||||
|
"github.com/google/uuid"
|
||||||
sysd `github.com/coreos/go-systemd/journal`
|
|
||||||
`github.com/google/uuid`
|
|
||||||
`r00t2.io/sysutils/paths`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.
|
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.
|
||||||
|
|
||||||
identifier is a string to use to identify the added Logger in MultiLogger.Loggers.
|
identifier is a string to use to identify the added Logger in MultiLogger.Loggers.
|
||||||
If empty, one will be automatically generated.
|
If empty, one will be automatically generated.
|
||||||
|
|
||||||
See the documentation for GetLogger for details on other arguments.
|
See the documentation for GetLogger for details on other arguments.
|
||||||
*/
|
*/
|
||||||
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
|
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
|
||||||
|
|
||||||
@@ -40,10 +37,10 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
AddSysdLogger adds a SystemDLogger to a MultiLogger.
|
AddSysdLogger adds a SystemDLogger to a MultiLogger.
|
||||||
|
|
||||||
identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers.
|
identifier is a string to use to identify the added SystemDLogger in MultiLogger.Loggers.
|
||||||
If empty, one will be automatically generated.
|
If empty, one will be automatically generated.
|
||||||
*/
|
*/
|
||||||
func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
|
func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
|
||||||
|
|
||||||
@@ -80,55 +77,3 @@ func (m *MultiLogger) AddSysdLogger(identifier string) (err error) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
AddSyslogLogger adds a SyslogLogger to a MultiLogger.
|
|
||||||
|
|
||||||
identifier is a string to use to identify the added SyslogLogger in MultiLogger.Loggers.
|
|
||||||
If empty, one will be automatically generated.
|
|
||||||
*/
|
|
||||||
func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) {
|
|
||||||
|
|
||||||
var exists bool
|
|
||||||
var hasSyslog bool
|
|
||||||
var stat os.FileInfo
|
|
||||||
var devlogPath string = devlog
|
|
||||||
var prefix string
|
|
||||||
|
|
||||||
if identifier == "" {
|
|
||||||
identifier = uuid.New().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, exists = m.Loggers[identifier]; exists {
|
|
||||||
err = ErrExistingLogger
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
|
|
||||||
return
|
|
||||||
} else if !hasSyslog {
|
|
||||||
err = ErrNoSyslog
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if stat.Mode().IsRegular() {
|
|
||||||
err = ErrInvalidDevLog
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Loggers[identifier] = &SyslogLogger{
|
|
||||||
EnableDebug: m.EnableDebug,
|
|
||||||
Prefix: m.Prefix,
|
|
||||||
}
|
|
||||||
if err = m.Loggers[identifier].Setup(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
//go:build !(windows || plan9 || wasip1 || js || ios)
|
||||||
|
// +build !windows,!plan9,!wasip1,!js,!ios
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
AddSyslogLogger adds a SyslogLogger to a MultiLogger.
|
||||||
|
|
||||||
|
identifier is a string to use to identify the added SyslogLogger in MultiLogger.Loggers.
|
||||||
|
If empty, one will be automatically generated.
|
||||||
|
*/
|
||||||
|
func (m *MultiLogger) AddSyslogLogger(identifier string) (err error) {
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
var hasSyslog bool
|
||||||
|
var stat os.FileInfo
|
||||||
|
var devlogPath string = devlog
|
||||||
|
var prefix string
|
||||||
|
|
||||||
|
if identifier == "" {
|
||||||
|
identifier = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists = m.Loggers[identifier]; exists {
|
||||||
|
err = ErrExistingLogger
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
|
||||||
|
return
|
||||||
|
} else if !hasSyslog {
|
||||||
|
err = ErrNoSyslog
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Mode().IsRegular() {
|
||||||
|
err = ErrInvalidDevLog
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Loggers[identifier] = &SyslogLogger{
|
||||||
|
EnableDebug: m.EnableDebug,
|
||||||
|
Prefix: m.Prefix,
|
||||||
|
}
|
||||||
|
if err = m.Loggers[identifier].Setup(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix, err = m.Loggers[identifier].GetPrefix(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Loggers[identifier].Debug("logger initialized of type %T with prefix %v", m.Loggers[identifier], prefix)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
|
||||||
|
// +build !windows,!plan9,!wasip1,!js,!ios,!linux
|
||||||
|
|
||||||
|
// Linux is excluded because it has its own.
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
AddDefaultLogger adds a default Logger (as would be determined by GetLogger) to a MultiLogger.
|
||||||
|
|
||||||
|
identifier is a string to use to identify the added Logger in MultiLogger.Loggers.
|
||||||
|
If empty, one will be automatically generated.
|
||||||
|
|
||||||
|
See the documentation for GetLogger for details on other arguments.
|
||||||
|
*/
|
||||||
|
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
|
||||||
|
|
||||||
|
var l Logger
|
||||||
|
var exists bool
|
||||||
|
|
||||||
|
if identifier == "" {
|
||||||
|
identifier = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists = m.Loggers[identifier]; exists {
|
||||||
|
err = ErrExistingLogger
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Loggers[identifier] = l
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
||||||
Currently this will almost always return a WinLogger.
|
Currently this will almost always return a WinLogger.
|
||||||
*/
|
*/
|
||||||
func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID, logFlags int, logPaths ...string) (err error) {
|
func (m *MultiLogger) AddDefaultLogger(identifier string, logFlags int, logPaths ...string) (err error) {
|
||||||
|
|
||||||
var l Logger
|
var l Logger
|
||||||
var exists bool
|
var exists bool
|
||||||
@@ -36,9 +36,9 @@ func (m *MultiLogger) AddDefaultLogger(identifier string, eventIDs *WinEventID,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if logPaths != nil {
|
if logPaths != nil {
|
||||||
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags, logPaths...)
|
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags, logPaths...)
|
||||||
} else {
|
} else {
|
||||||
l, err = GetLogger(m.EnableDebug, m.Prefix, eventIDs, logFlags)
|
l, err = GetLogger(m.EnableDebug, m.Prefix, logFlags)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Setup() (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoDebug does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) DoDebug(d bool) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDebug returns the debug status of this NullLogger. It will always return true. 🙃
|
||||||
|
func (n *NullLogger) GetDebug() (d bool) {
|
||||||
|
|
||||||
|
d = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPrefix does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) SetPrefix(p string) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefix does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) GetPrefix() (p string, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Shutdown() (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Alert(s string, v ...interface{}) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crit does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Crit(s string, v ...interface{}) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Debug(s string, v ...interface{}) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emerg does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Emerg(s string, v ...interface{}) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Err(s string, v ...interface{}) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Info(s string, v ...interface{}) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notice does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Notice(s string, v ...interface{}) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning does nothing at all; it's here for interface compat. 🙃
|
||||||
|
func (l *NullLogger) Warning(s string, v ...interface{}) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLogger returns a stdlib log.Logger (that doesn't actually write to anything).
|
||||||
|
func (l *NullLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
|
||||||
|
|
||||||
|
stdLibLog = log.New(&nullWriter{}, "", 0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRaw returns a *logWriter. (This is a little less efficient than using ToLogger's log.Logger as an io.Writer if that's all you need.)
|
||||||
|
func (l *NullLogger) ToRaw(prio logPrio) (raw *logWriter) {
|
||||||
|
|
||||||
|
raw = &logWriter{backend: l, prio: prio}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Close conforms a nullWriter to an io.WriteCloser. It obviously does nothing, and will always return with err == nil.
|
||||||
|
func (nw *nullWriter) Close() (err error) {
|
||||||
|
|
||||||
|
// NO-OP
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write conforms a nullWriter to an io.Writer, but it writes... nothing. To avoid errors, however, in downstream code it pretends it does (n will *always* == len(b)).
|
||||||
|
func (nw *nullWriter) Write(b []byte) (n int, err error) {
|
||||||
|
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n = len(b)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteByte conforms to an io.ByteWriter but again... nothing is actually written anywhere.
|
||||||
|
func (nw *nullWriter) WriteByte(c byte) (err error) {
|
||||||
|
|
||||||
|
// NO-OP
|
||||||
|
|
||||||
|
_ = c
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
WriteRune conforms to the other Loggers. It WILL return the proper value for n (matching (bytes.Buffer).WriteRune() and (bufio.Writer).WriteRune() signatures,
|
||||||
|
and it WILL return an ErrInvalidRune if r is not a valid rune, but otherwise it will no-op.
|
||||||
|
*/
|
||||||
|
func (nw *nullWriter) WriteRune(r rune) (n int, err error) {
|
||||||
|
|
||||||
|
n = utf8.RuneLen(r)
|
||||||
|
if n < 0 {
|
||||||
|
err = ErrInvalidRune
|
||||||
|
n = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString conforms to an io.StringWriter but nothing is actually written. (n will *always* == len(s))
|
||||||
|
func (nw *nullWriter) WriteString(s string) (n int, err error) {
|
||||||
|
|
||||||
|
n = len(s)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
//go:build !(windows || plan9 || wasip1 || js || ios || linux)
|
||||||
|
|
||||||
|
// Linux is excluded because it has its own.
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
native "log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"r00t2.io/goutils/bitmask"
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ = native.Logger{}
|
||||||
|
_ = os.Interrupt
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetLogger returns an instance of Logger that best suits your system's capabilities.
|
||||||
|
|
||||||
|
If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written.
|
||||||
|
|
||||||
|
If prefix is "\x00" (a null byte), then the default logging prefix will be used. If anything else, even an empty string,
|
||||||
|
is specified then that will be used instead for the prefix.
|
||||||
|
|
||||||
|
logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and
|
||||||
|
https://pkg.go.dev/log#pkg-constants for details.
|
||||||
|
|
||||||
|
logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to,
|
||||||
|
it will be used (assuming you have no higher-level loggers available). Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
||||||
|
If you want to log to multiple files simultaneously, use a MultiLogger instead.
|
||||||
|
|
||||||
|
If you call GetLogger, you will only get a single ("best") logger your system supports.
|
||||||
|
If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type),
|
||||||
|
use GetMultiLogger.
|
||||||
|
*/
|
||||||
|
func GetLogger(enableDebug bool, prefix string, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
||||||
|
|
||||||
|
var logPath string
|
||||||
|
var logFlags bitmask.MaskBit
|
||||||
|
var currentPrefix string
|
||||||
|
|
||||||
|
// Configure system-supported logger(s).
|
||||||
|
|
||||||
|
// If we can detect syslog, use that. If not, try to use a file logger (+ stdout).
|
||||||
|
// Last ditch, stdout.
|
||||||
|
var hasSyslog bool
|
||||||
|
var stat os.FileInfo
|
||||||
|
var devlogPath string = devlog
|
||||||
|
|
||||||
|
if hasSyslog, stat, err = paths.RealPathExistsStat(&devlogPath); hasSyslog && err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasSyslog && !stat.Mode().IsRegular() {
|
||||||
|
logFlags.AddFlag(LogSyslog)
|
||||||
|
} else {
|
||||||
|
var exists bool
|
||||||
|
var success bool
|
||||||
|
var ckLogPaths []string
|
||||||
|
|
||||||
|
logFlags.AddFlag(LogStdout)
|
||||||
|
ckLogPaths = defLogPaths
|
||||||
|
if logPaths != nil {
|
||||||
|
ckLogPaths = logPaths
|
||||||
|
}
|
||||||
|
for _, p := range ckLogPaths {
|
||||||
|
if exists, _ = paths.RealPathExists(&p); exists {
|
||||||
|
if success, err = testOpen(p); err != nil {
|
||||||
|
continue
|
||||||
|
} else if !success {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logFlags.AddFlag(LogFile)
|
||||||
|
logPath = p
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
dirPath := path.Dir(p)
|
||||||
|
if err = paths.MakeDirIfNotExist(dirPath); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if success, err = testOpen(p); err != nil {
|
||||||
|
continue
|
||||||
|
} else if !success {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logFlags.AddFlag(LogFile)
|
||||||
|
logPath = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if logFlags.HasFlag(LogSyslog) {
|
||||||
|
logger = &SyslogLogger{
|
||||||
|
Prefix: logPrefix,
|
||||||
|
EnableDebug: enableDebug,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if logFlags.HasFlag(LogFile) {
|
||||||
|
logger = &FileLogger{
|
||||||
|
StdLogger: StdLogger{
|
||||||
|
Prefix: logPrefix,
|
||||||
|
EnableDebug: enableDebug,
|
||||||
|
LogFlags: logConfigFlags,
|
||||||
|
},
|
||||||
|
Path: logPath,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger = &StdLogger{
|
||||||
|
Prefix: logPrefix,
|
||||||
|
EnableDebug: enableDebug,
|
||||||
|
LogFlags: logConfigFlags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix != "\x00" {
|
||||||
|
if err = logger.SetPrefix(prefix); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = logger.Setup(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentPrefix, err = logger.GetPrefix(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("logger initialized of type %T with prefix %v", logger, currentPrefix)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
+39
-15
@@ -2,15 +2,15 @@ package logging
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
`io`
|
"io"
|
||||||
`log`
|
"log"
|
||||||
`os`
|
"os"
|
||||||
`strings`
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Setup sets up/configures a StdLogger and prepares it for use.
|
Setup sets up/configures a StdLogger and prepares it for use.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *StdLogger) Setup() (err error) {
|
func (l *StdLogger) Setup() (err error) {
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ func (l *StdLogger) Setup() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Shutdown cleanly shuts down a StdLogger.
|
Shutdown cleanly shuts down a StdLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *StdLogger) Shutdown() (err error) {
|
func (l *StdLogger) Shutdown() (err error) {
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ func (l *StdLogger) Shutdown() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GetPrefix returns the prefix used by this StdLogger.
|
GetPrefix returns the prefix used by this StdLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *StdLogger) GetPrefix() (prefix string, err error) {
|
func (l *StdLogger) GetPrefix() (prefix string, err error) {
|
||||||
|
|
||||||
@@ -69,9 +69,9 @@ func (l *StdLogger) GetPrefix() (prefix string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
DoDebug sets the debug state of this StdLogger.
|
DoDebug sets the debug state of this StdLogger.
|
||||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *StdLogger) DoDebug(d bool) (err error) {
|
func (l *StdLogger) DoDebug(d bool) (err error) {
|
||||||
|
|
||||||
@@ -80,9 +80,17 @@ func (l *StdLogger) DoDebug(d bool) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDebug returns the debug status of this StdLogger.
|
||||||
|
func (l *StdLogger) GetDebug() (d bool) {
|
||||||
|
|
||||||
|
d = l.EnableDebug
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
SetPrefix sets the prefix for this StdLogger.
|
SetPrefix sets the prefix for this StdLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *StdLogger) SetPrefix(prefix string) (err error) {
|
func (l *StdLogger) SetPrefix(prefix string) (err error) {
|
||||||
|
|
||||||
@@ -227,6 +235,22 @@ func (l *StdLogger) Warning(s string, v ...interface{}) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToLogger returns a stdlib log.Logger.
|
||||||
|
func (l *StdLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
|
||||||
|
|
||||||
|
stdLibLog = log.New(l.ToRaw(prio), "", 0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRaw returns a *logWriter.
|
||||||
|
func (l *StdLogger) ToRaw(prio logPrio) (raw *logWriter) {
|
||||||
|
|
||||||
|
raw = &logWriter{backend: l, prio: prio}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// renderWrite prepares/formats a log message to be written to this StdLogger.
|
// renderWrite prepares/formats a log message to be written to this StdLogger.
|
||||||
func (l *StdLogger) renderWrite(msg, prio string) {
|
func (l *StdLogger) renderWrite(msg, prio string) {
|
||||||
|
|
||||||
|
|||||||
+36
-12
@@ -4,12 +4,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/coreos/go-systemd/journal"
|
"github.com/coreos/go-systemd/v22/journal"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Setup sets up/configures a SystemDLogger and prepares it for use.
|
Setup sets up/configures a SystemDLogger and prepares it for use.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *SystemDLogger) Setup() (err error) {
|
func (l *SystemDLogger) Setup() (err error) {
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ func (l *SystemDLogger) Setup() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Shutdown cleanly shuts down a SystemDLogger.
|
Shutdown cleanly shuts down a SystemDLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *SystemDLogger) Shutdown() (err error) {
|
func (l *SystemDLogger) Shutdown() (err error) {
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ func (l *SystemDLogger) Shutdown() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GetPrefix returns the prefix used by this SystemDLogger.
|
GetPrefix returns the prefix used by this SystemDLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *SystemDLogger) GetPrefix() (prefix string, err error) {
|
func (l *SystemDLogger) GetPrefix() (prefix string, err error) {
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ func (l *SystemDLogger) GetPrefix() (prefix string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
DoDebug sets the debug state of this SystemDLogger.
|
DoDebug sets the debug state of this SystemDLogger.
|
||||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *SystemDLogger) DoDebug(d bool) (err error) {
|
func (l *SystemDLogger) DoDebug(d bool) (err error) {
|
||||||
|
|
||||||
@@ -52,9 +52,17 @@ func (l *SystemDLogger) DoDebug(d bool) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDebug returns the debug status of this SystemDLogger.
|
||||||
|
func (l *SystemDLogger) GetDebug() (d bool) {
|
||||||
|
|
||||||
|
d = l.EnableDebug
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
SetPrefix sets the prefix for this SystemDLogger.
|
SetPrefix sets the prefix for this SystemDLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *SystemDLogger) SetPrefix(prefix string) (err error) {
|
func (l *SystemDLogger) SetPrefix(prefix string) (err error) {
|
||||||
|
|
||||||
@@ -215,3 +223,19 @@ func (l *SystemDLogger) renderWrite(msg string, prio journal.Priority) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToLogger returns a stdlib log.Logger.
|
||||||
|
func (l *SystemDLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
|
||||||
|
|
||||||
|
stdLibLog = log.New(l.ToRaw(prio), "", 0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRaw returns a *logWriter.
|
||||||
|
func (l *SystemDLogger) ToRaw(prio logPrio) (raw *logWriter) {
|
||||||
|
|
||||||
|
raw = &logWriter{backend: l, prio: prio}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !(windows || plan9 || wasip1 || js || ios)
|
||||||
|
// +build !windows,!plan9,!wasip1,!js,!ios
|
||||||
|
|
||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -5,7 +8,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"log/syslog"
|
"log/syslog"
|
||||||
|
|
||||||
`r00t2.io/goutils/multierr`
|
"r00t2.io/goutils/multierr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup sets up/configures a SyslogLogger and prepares it for use.
|
// Setup sets up/configures a SyslogLogger and prepares it for use.
|
||||||
@@ -73,8 +76,8 @@ func (l *SyslogLogger) Shutdown() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GetPrefix returns the prefix used by this SyslogLogger.
|
GetPrefix returns the prefix used by this SyslogLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *SyslogLogger) GetPrefix() (prefix string, err error) {
|
func (l *SyslogLogger) GetPrefix() (prefix string, err error) {
|
||||||
|
|
||||||
@@ -84,9 +87,9 @@ func (l *SyslogLogger) GetPrefix() (prefix string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
DoDebug sets the debug state of this SyslogLogger.
|
DoDebug sets the debug state of this SyslogLogger.
|
||||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *SyslogLogger) DoDebug(d bool) (err error) {
|
func (l *SyslogLogger) DoDebug(d bool) (err error) {
|
||||||
|
|
||||||
@@ -95,6 +98,14 @@ func (l *SyslogLogger) DoDebug(d bool) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDebug returns the debug status of this SyslogLogger.
|
||||||
|
func (l *SyslogLogger) GetDebug() (d bool) {
|
||||||
|
|
||||||
|
d = l.EnableDebug
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// SetPrefix sets the prefix for this SyslogLogger.
|
// SetPrefix sets the prefix for this SyslogLogger.
|
||||||
func (l *SyslogLogger) SetPrefix(prefix string) (err error) {
|
func (l *SyslogLogger) SetPrefix(prefix string) (err error) {
|
||||||
|
|
||||||
@@ -258,3 +269,19 @@ func (l *SyslogLogger) Warning(s string, v ...interface{}) (err error) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToLogger returns a stdlib log.Logger.
|
||||||
|
func (l *SyslogLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
|
||||||
|
|
||||||
|
stdLibLog = log.New(l.ToRaw(prio), "", 0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRaw returns a *logWriter.
|
||||||
|
func (l *SyslogLogger) ToRaw(prio logPrio) (raw *logWriter) {
|
||||||
|
|
||||||
|
raw = &logWriter{backend: l, prio: prio}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
+46
-15
@@ -10,32 +10,63 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GetLogger returns an instance of Logger that best suits your system's capabilities. Note that this is a VERY generalized interface to the Windows Event Log.
|
GetLogger returns an instance of Logger that best suits your system's capabilities.
|
||||||
|
Note that this is a VERY generalized interface to the Windows Event Log to conform with multiplatform compat.
|
||||||
|
You'd have a little more flexibility with [GetLoggerWindows] (this function wraps that one).
|
||||||
|
If you need more custom behavior than that, I recommend using [golang.org/x/sys/windows/svc/eventlog] directly
|
||||||
|
(or using another logging module).
|
||||||
|
|
||||||
|
If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).
|
||||||
|
|
||||||
|
The `prefix` correlates to the `source` parameter in [GetLoggerWindows], and this function inherently uses [DefaultEventID],
|
||||||
|
but otherwise it remains the same as [GetLoggerWindows] - refer to it for documentation on the other parameters.
|
||||||
|
|
||||||
|
If you call [GetLogger], you will only get a single ("best") logger your system supports.
|
||||||
|
If you want to log to multiple [Logger] destinations at once (or want to log to an explicit [Logger] type),
|
||||||
|
use [GetMultiLogger].
|
||||||
|
*/
|
||||||
|
func GetLogger(enableDebug bool, prefix string, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
||||||
|
|
||||||
|
if logger, err = GetLoggerWindows(enableDebug, prefix, DefaultEventID, logConfigFlags, logPaths...); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetLoggerWindows returns an instance of Logger that best suits your system's capabilities.
|
||||||
|
This is a slightly less (but still quite) generalized interface to the Windows Event Log than [GetLogger].
|
||||||
|
|
||||||
If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
|
If you require more robust logging capabilities (e.g. custom event IDs per uniquely identifiable event),
|
||||||
you will want to set up your own logger (golang.org/x/sys/windows/svc/eventlog).
|
you will want to set up your own logger via [golang.org/x/sys/windows/svc/eventlog].
|
||||||
|
|
||||||
If enableDebug is true, debug messages (which according to your program may or may not contain sensitive data) are rendered and written (otherwise they are ignored).
|
If `enableDebug` is true, debug messages (which according to your program may or may not contain sensitive data)
|
||||||
|
are rendered and written (otherwise they are ignored).
|
||||||
|
|
||||||
A blank source will return an error as it's used as the source name. Other functions, struct fields, etc. will refer to this as the "prefix".
|
A blank `source` will return an error as it's used as the source name.
|
||||||
|
Throughout the rest of this documentation you will see this referred to as the `prefix` to remain platform-agnostic.
|
||||||
|
|
||||||
A pointer to a WinEventID struct may be specified for eventIDs to map extended logging levels (as Windows only supports three levels natively).
|
A pointer to a [WinEventID] struct may be specified for `eventIDs` to map extended logging levels
|
||||||
|
(as Windows only supports three levels natively).
|
||||||
If it is nil, a default one (DefaultEventID) will be used.
|
If it is nil, a default one (DefaultEventID) will be used.
|
||||||
|
|
||||||
logConfigFlags is the corresponding flag(s) OR'd for StdLogger.LogFlags / FileLogger.StdLogger.LogFlags if either is selected. See StdLogger.LogFlags and
|
`logConfigFlags` is the corresponding flag(s) OR'd for [StdLogger.LogFlags] (and/or the [StdLogger.LogFlags] for [FileLogger])
|
||||||
https://pkg.go.dev/log#pkg-constants for details.
|
if either is selected. See [StdLogger.LogFlags] and [stdlib log's constants] for details.
|
||||||
|
|
||||||
logPaths is an (optional) list of strings to use as paths to test for writing. If the file can be created/written to,
|
`logPaths` is an (optional) list of strings to use as paths to test for writing.
|
||||||
it will be used (assuming you have no higher-level loggers available).
|
If the file can be created/written to, it will be used (assuming you have no higher-level loggers available).
|
||||||
|
|
||||||
Only the first logPaths entry that "works" will be used, later entries will be ignored.
|
Only the first `logPaths` entry that "works" will be used, later entries will be ignored.
|
||||||
Currently this will almost always return a WinLogger.
|
Currently this will almost always return a [WinLogger].
|
||||||
|
|
||||||
If you call GetLogger, you will only get a single ("best") logger your system supports.
|
If you call [GetLoggerWindows], you will only get a single ("best") logger your system supports.
|
||||||
If you want to log to multiple Logger destinations at once (or want to log to an explicit Logger type),
|
If you want to log to multiple [Logger] destinations at once (or want to log to an explicit [Logger] type),
|
||||||
use GetMultiLogger.
|
use [GetMultiLogger].
|
||||||
|
|
||||||
|
[stdlib log's constants]: https://pkg.go.dev/log#pkg-constants
|
||||||
*/
|
*/
|
||||||
func GetLogger(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
func GetLoggerWindows(enableDebug bool, source string, eventIDs *WinEventID, logConfigFlags int, logPaths ...string) (logger Logger, err error) {
|
||||||
|
|
||||||
var logPath string
|
var logPath string
|
||||||
var logFlags bitmask.MaskBit
|
var logFlags bitmask.MaskBit
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func TestDefaultLogger(t *testing.T) {
|
|||||||
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
t.Fatalf("error when closing handler for temporary log file '%v': %v", tempfile.Name(), err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if l, err = GetLogger(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
|
if l, err = GetLoggerWindows(true, TestLogPrefix, DefaultEventID, logFlags, tempfilePath); err != nil {
|
||||||
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
|
t.Fatalf("error when spawning default Windows logger via GetLogger: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`errors`
|
"errors"
|
||||||
`fmt`
|
"fmt"
|
||||||
`os`
|
"log"
|
||||||
`os/exec`
|
"os"
|
||||||
`syscall`
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
`golang.org/x/sys/windows/registry`
|
"golang.org/x/sys/windows/registry"
|
||||||
`golang.org/x/sys/windows/svc/eventlog`
|
"golang.org/x/sys/windows/svc/eventlog"
|
||||||
`r00t2.io/sysutils/paths`
|
"r00t2.io/sysutils/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Setup sets up/configures a WinLogger and prepares it for use.
|
Setup sets up/configures a WinLogger and prepares it for use.
|
||||||
This will fail with an Access Denied (the first time, at least) unless running with elevated permissions unless WinLogger.Prefix is
|
This will fail with an Access Denied (the first time, at least) unless running with elevated permissions unless WinLogger.Prefix is
|
||||||
a registered Event Log source.
|
a registered Event Log source.
|
||||||
|
|
||||||
If a failure occurs while trying to open the log with the given WinLogger.Prefix ("source"), a new Event Log source will be registered.
|
If a failure occurs while trying to open the log with the given WinLogger.Prefix ("source"), a new Event Log source will be registered.
|
||||||
If WinLogger.Executable is not empty at the time of calling WinLogger.Setup (or WinLogger.ForceService is true),
|
If WinLogger.Executable is not empty at the time of calling WinLogger.Setup (or WinLogger.ForceService is true),
|
||||||
eventlog.Install will be used (with the WinLogger.ExpandKey field).
|
eventlog.Install will be used (with the WinLogger.ExpandKey field).
|
||||||
Otherwise eventlog.InstallAsEventCreate will be used.
|
Otherwise eventlog.InstallAsEventCreate will be used.
|
||||||
*/
|
*/
|
||||||
func (l *WinLogger) Setup() (err error) {
|
func (l *WinLogger) Setup() (err error) {
|
||||||
|
|
||||||
@@ -108,8 +109,8 @@ func (l *WinLogger) Remove() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Shutdown cleanly shuts down a WinLogger but keep the source registered. Use WinLogger.Remove
|
Shutdown cleanly shuts down a WinLogger but keep the source registered. Use WinLogger.Remove
|
||||||
(or set WinLogger.RemoveOnClose to true before calling WinLogger.Shutdown) to remove the registered source.
|
(or set WinLogger.RemoveOnClose to true before calling WinLogger.Shutdown) to remove the registered source.
|
||||||
*/
|
*/
|
||||||
func (l *WinLogger) Shutdown() (err error) {
|
func (l *WinLogger) Shutdown() (err error) {
|
||||||
|
|
||||||
@@ -128,8 +129,8 @@ func (l *WinLogger) Shutdown() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
GetPrefix returns the prefix used by this WinLogger.
|
GetPrefix returns the prefix used by this WinLogger.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *WinLogger) GetPrefix() (prefix string, err error) {
|
func (l *WinLogger) GetPrefix() (prefix string, err error) {
|
||||||
|
|
||||||
@@ -139,9 +140,9 @@ func (l *WinLogger) GetPrefix() (prefix string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
DoDebug sets the debug state of this WinLogger.
|
DoDebug sets the debug state of this WinLogger.
|
||||||
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
Note that this merely acts as a *safety filter* for debug messages to avoid sensitive information being written to the log.
|
||||||
err will always be nil; it's there for interface-compat.
|
err will always be nil; it's there for interface-compat.
|
||||||
*/
|
*/
|
||||||
func (l *WinLogger) DoDebug(d bool) (err error) {
|
func (l *WinLogger) DoDebug(d bool) (err error) {
|
||||||
|
|
||||||
@@ -150,6 +151,14 @@ func (l *WinLogger) DoDebug(d bool) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDebug returns the debug status of this WinLogger.
|
||||||
|
func (l *WinLogger) GetDebug() (d bool) {
|
||||||
|
|
||||||
|
d = l.EnableDebug
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// SetPrefix sets the prefix for this WinLogger.
|
// SetPrefix sets the prefix for this WinLogger.
|
||||||
func (l *WinLogger) SetPrefix(prefix string) (err error) {
|
func (l *WinLogger) SetPrefix(prefix string) (err error) {
|
||||||
|
|
||||||
@@ -334,3 +343,19 @@ func (l *WinLogger) Warning(s string, v ...interface{}) (err error) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToLogger returns a stdlib log.Logger.
|
||||||
|
func (l *WinLogger) ToLogger(prio logPrio) (stdLibLog *log.Logger) {
|
||||||
|
|
||||||
|
stdLibLog = log.New(l.ToRaw(prio), "", 0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRaw returns a *logWriter.
|
||||||
|
func (l *WinLogger) ToRaw(prio logPrio) (raw *logWriter) {
|
||||||
|
|
||||||
|
raw = &logWriter{backend: l, prio: prio}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ func TestMultiLogger(t *testing.T) {
|
|||||||
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
|
t.Fatalf("error when adding FileLogger to MultiLogger: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = l.AddDefaultLogger("DefaultLogger", DefaultEventID, logFlags, tempfilePath); err != nil {
|
if err = l.AddDefaultLogger("DefaultLogger", logFlags, tempfilePath); err != nil {
|
||||||
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
|
t.Fatalf("error when adding default logger to MultiLogger: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-8
@@ -2,11 +2,15 @@ package logging
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
`os`
|
"os"
|
||||||
|
|
||||||
|
"r00t2.io/goutils/bitmask"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type logPrio bitmask.MaskBit
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Logger is one of the various loggers offered by this module.
|
Logger is one of the various loggers offered by this module.
|
||||||
*/
|
*/
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
Alert(s string, v ...interface{}) (err error)
|
Alert(s string, v ...interface{}) (err error)
|
||||||
@@ -18,15 +22,18 @@ type Logger interface {
|
|||||||
Notice(s string, v ...interface{}) (err error)
|
Notice(s string, v ...interface{}) (err error)
|
||||||
Warning(s string, v ...interface{}) (err error)
|
Warning(s string, v ...interface{}) (err error)
|
||||||
DoDebug(d bool) (err error)
|
DoDebug(d bool) (err error)
|
||||||
|
GetDebug() (d bool)
|
||||||
SetPrefix(p string) (err error)
|
SetPrefix(p string) (err error)
|
||||||
GetPrefix() (p string, err error)
|
GetPrefix() (p string, err error)
|
||||||
Setup() (err error)
|
Setup() (err error)
|
||||||
Shutdown() (err error)
|
Shutdown() (err error)
|
||||||
|
ToLogger(prio logPrio) (stdLibLog *log.Logger)
|
||||||
|
ToRaw(prio logPrio) (raw *logWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
StdLogger uses the log package in stdlib to perform all logging. The default is to write to STDOUT.
|
StdLogger uses the log package in stdlib to perform all logging. The default is to write to STDOUT.
|
||||||
If you wish to modify the underling log.Logger object, you can access it directly via StdLogger.Logger.
|
If you wish to modify the underling log.Logger object, you can access it directly via StdLogger.Logger.
|
||||||
*/
|
*/
|
||||||
type StdLogger struct {
|
type StdLogger struct {
|
||||||
// All log.Logger fields/methods are exposed.
|
// All log.Logger fields/methods are exposed.
|
||||||
@@ -71,11 +78,11 @@ type StdLogger struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FileLogger uses a StdLogger with a file handle writer to write to the file given at Path.
|
FileLogger uses a StdLogger with a file handle writer to write to the file given at Path.
|
||||||
|
|
||||||
NOTE: If you wish to change the FileLogger.StdLogger.LogFlags, do *not* run FileLogger.StdLogger.Setup after doing so as this
|
NOTE: If you wish to change the FileLogger.StdLogger.LogFlags, do *not* run FileLogger.StdLogger.Setup after doing so as this
|
||||||
will instead create a logger detached from the file handler. Instead, be sure to call FileLogger.Setup.
|
will instead create a logger detached from the file handler. Instead, be sure to call FileLogger.Setup.
|
||||||
(Alternatively, run FileLogger.Shutdown and replace your logger with a new FileLogger.)
|
(Alternatively, run FileLogger.Shutdown and replace your logger with a new FileLogger.)
|
||||||
*/
|
*/
|
||||||
type FileLogger struct {
|
type FileLogger struct {
|
||||||
// StdLogger is used for the log formation and handling. See StdLogger for more details.
|
// StdLogger is used for the log formation and handling. See StdLogger for more details.
|
||||||
@@ -86,6 +93,9 @@ type FileLogger struct {
|
|||||||
writer *os.File
|
writer *os.File
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NullLogger is used mainly for test implementations, mockup code, etc. It does absolutely nothing with all messages sent to it.
|
||||||
|
type NullLogger struct{}
|
||||||
|
|
||||||
// MultiLogger is used to contain one or more Loggers and present them all as a single Logger.
|
// MultiLogger is used to contain one or more Loggers and present them all as a single Logger.
|
||||||
type MultiLogger struct {
|
type MultiLogger struct {
|
||||||
/*
|
/*
|
||||||
@@ -101,3 +111,12 @@ type MultiLogger struct {
|
|||||||
*/
|
*/
|
||||||
Loggers map[string]Logger
|
Loggers map[string]Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logWriter is used as a log.Logger and is returned by <Logger>.ToLogger.
|
||||||
|
type logWriter struct {
|
||||||
|
backend Logger
|
||||||
|
prio logPrio
|
||||||
|
}
|
||||||
|
|
||||||
|
// nullWriter is used as a shortcut by NullLogger.ToLogger.
|
||||||
|
type nullWriter struct{}
|
||||||
+1
-19
@@ -1,27 +1,9 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
|
||||||
`log/syslog`
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
SystemDLogger (yes, I'm aware it's actually written as "systemd") writes to journald on systemd-enabled systems.
|
SystemDLogger (yes, I'm aware it's actually written as "systemd") writes to journald on systemd-enabled systems.
|
||||||
*/
|
*/
|
||||||
type SystemDLogger struct {
|
type SystemDLogger struct {
|
||||||
EnableDebug bool
|
EnableDebug bool
|
||||||
Prefix string
|
Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyslogLogger writes to syslog on syslog-enabled systems.
|
|
||||||
type SyslogLogger struct {
|
|
||||||
EnableDebug bool
|
|
||||||
Prefix string
|
|
||||||
alert,
|
|
||||||
crit,
|
|
||||||
debug,
|
|
||||||
emerg,
|
|
||||||
err,
|
|
||||||
info,
|
|
||||||
notice,
|
|
||||||
warning *syslog.Writer
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//go:build !(windows || plan9 || wasip1 || js || ios)
|
||||||
|
// +build !windows,!plan9,!wasip1,!js,!ios
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/syslog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyslogLogger writes to syslog on syslog-enabled systems.
|
||||||
|
type SyslogLogger struct {
|
||||||
|
EnableDebug bool
|
||||||
|
Prefix string
|
||||||
|
alert,
|
||||||
|
crit,
|
||||||
|
debug,
|
||||||
|
emerg,
|
||||||
|
err,
|
||||||
|
info,
|
||||||
|
notice,
|
||||||
|
warning *syslog.Writer
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
Package mapsx includes functions that probably should have been in [maps] but aren't.
|
||||||
|
*/
|
||||||
|
package mapsx
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package mapsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`errors`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("key not found")
|
||||||
|
)
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
package mapsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"iter"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get mimics Python's [dict.get()] behavior, returning value `v` if key `k`
|
||||||
|
is not found in map `m`.
|
||||||
|
|
||||||
|
See also [GetOk], [Must].
|
||||||
|
|
||||||
|
[dict.get()]: https://docs.python.org/3/library/stdtypes.html#dict.get
|
||||||
|
*/
|
||||||
|
func Get[Map ~map[K]V, K comparable, V any](m Map, k K, v V) (val V) {
|
||||||
|
|
||||||
|
val, _ = GetOk(m, k, v)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOk is like [Get] but also explicitly indicates whether `k` was found or not. See also [Must].
|
||||||
|
func GetOk[Map ~map[K]V, K comparable, V any](m Map, k K, v V) (val V, found bool) {
|
||||||
|
|
||||||
|
if val, found = m[k]; !found {
|
||||||
|
val = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ListKeys returns map `m`'s keys as a slice (unlike [maps.Keys],
|
||||||
|
which returns an iterator via [iter.Seq]).
|
||||||
|
|
||||||
|
ListKeys will return nil if `m` is nil, and an empty slice
|
||||||
|
if `m` is non-nil but empty.
|
||||||
|
|
||||||
|
The ordering of `mapKeys` is unpredictable as maps are unsorted
|
||||||
|
and no explicit sorting is performed.
|
||||||
|
See [SortedKeys] and [ListSortedKeys] if sorting is desired.
|
||||||
|
*/
|
||||||
|
func ListKeys[Map ~map[K]V, K comparable, V any](m Map) (mapKeys []K) {
|
||||||
|
|
||||||
|
var k K
|
||||||
|
|
||||||
|
if m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mapKeys = make([]K, 0, len(m))
|
||||||
|
|
||||||
|
for k, _ = range m {
|
||||||
|
mapKeys = append(mapKeys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ListSortedKeys is like [ListKeys] but returns the keys in a sorted slice.
|
||||||
|
|
||||||
|
The keys in map `m` MUST conform to a [cmp.Ordered]!
|
||||||
|
*/
|
||||||
|
func ListSortedKeys[Map ~map[K]V, K cmp.Ordered, V any](m Map) (mapKeys []K) {
|
||||||
|
|
||||||
|
// All cmp.Ordered types are comparable, but not all comparable types are cmp.Ordered.
|
||||||
|
mapKeys = ListKeys(m)
|
||||||
|
|
||||||
|
slices.Sort(mapKeys)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ListSortedValues is like [ListValues] but returns the values in a sorted slice.
|
||||||
|
|
||||||
|
The values in map `m` MUST conform to a [cmp.Ordered]!
|
||||||
|
*/
|
||||||
|
func ListSortedValues[Map ~map[K]V, K comparable, V cmp.Ordered](m Map) (mapVals []V) {
|
||||||
|
|
||||||
|
// All cmp.Ordered types are comparable, but not all comparable types are cmp.Ordered.
|
||||||
|
mapVals = ListValues(m)
|
||||||
|
|
||||||
|
slices.Sort(mapVals)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ListValues returns map `m`'s values as a slice (unlike [maps.Values],
|
||||||
|
which returns an iterator via [iter.Seq]).
|
||||||
|
|
||||||
|
ListValues will return nil if `m` is nil, and an empty slice
|
||||||
|
if `m` is non-nil but empty.
|
||||||
|
|
||||||
|
The ordering of `mapVals` is unpredictable as maps are unsorted
|
||||||
|
and no explicit sorting is performed.
|
||||||
|
See [SortedValues] and [ListSortedValues] if sorting is desired.
|
||||||
|
*/
|
||||||
|
func ListValues[Map ~map[K]V, K comparable, V any](m Map) (mapVals []V) {
|
||||||
|
|
||||||
|
var v V
|
||||||
|
|
||||||
|
if m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mapVals = make([]V, 0, len(m))
|
||||||
|
|
||||||
|
for _, v = range m {
|
||||||
|
mapVals = append(mapVals, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SortedKeys is like [maps.Keys] but will sort the keys in the returned [iter.Seq].
|
||||||
|
(There is a small cost to this.)
|
||||||
|
|
||||||
|
The keys in map `m` MUST conform to a [cmp.Ordered]!
|
||||||
|
*/
|
||||||
|
func SortedKeys[Map ~map[K]V, K cmp.Ordered, V any](m Map) (mapKeys iter.Seq[K]) {
|
||||||
|
|
||||||
|
var k K
|
||||||
|
// All cmp.Ordered types are comparable, but not all comparable types are cmp.Ordered.
|
||||||
|
var keys []K = ListKeys(m)
|
||||||
|
|
||||||
|
slices.Sort(keys)
|
||||||
|
|
||||||
|
return func(yield func(K) bool) {
|
||||||
|
for _, k = range keys {
|
||||||
|
if !yield(k) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
SortedValues is like [maps.Values] but will sort the values in the returned [iter.Seq].
|
||||||
|
(There is a small cost to this.)
|
||||||
|
|
||||||
|
The values in map `m` MUST conform to a [cmp.Ordered]!
|
||||||
|
*/
|
||||||
|
func SortedValues[Map ~map[K]V, K comparable, V cmp.Ordered](m Map) (mapVals iter.Seq[V]) {
|
||||||
|
|
||||||
|
var v V
|
||||||
|
// All cmp.Ordered types are comparable, but not all comparable types are cmp.Ordered.
|
||||||
|
var vals []V = ListValues(m)
|
||||||
|
|
||||||
|
slices.Sort(vals)
|
||||||
|
|
||||||
|
return func(yield func(V) bool) {
|
||||||
|
for _, v = range vals {
|
||||||
|
if !yield(v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Must, unlike [Get] or [GetOk], requires that `k` be in map `m`.
|
||||||
|
|
||||||
|
A panic with error [ErrNotFound] will be raised if `k` is not present.
|
||||||
|
Otherwise the found value will be returned.
|
||||||
|
*/
|
||||||
|
func Must[Map ~map[K]V, K comparable, V any](m Map, k K) (val V) {
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if val, ok = m[k]; !ok {
|
||||||
|
panic(ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
- add unwrapping
|
||||||
|
https://go.dev/blog/go1.13-errors#the-unwrap-method
|
||||||
|
- add As method, takes a ptr to a slice of []error to return the first matching error type (errors.As) for each?
|
||||||
|
- add AsAll [][]error ptr param for multiple errors per type?
|
||||||
|
- add Map, returns map[string][]error, where key is k:
|
||||||
|
var sb strings.Builder
|
||||||
|
t = reflect.TypeOf(err)
|
||||||
|
if t.PkgPath() != "" {
|
||||||
|
sb.WriteString(t.PkgPath())
|
||||||
|
} else {
|
||||||
|
sb.WriteString("<UNKNOWN>")
|
||||||
|
}
|
||||||
|
sb.WriteString(".")
|
||||||
|
if t.Name() != "" {
|
||||||
|
sb.WriteString(t.Name())
|
||||||
|
} else {
|
||||||
|
sb.WriteString("<UNKNOWN>")
|
||||||
|
}
|
||||||
|
k = sb.String()
|
||||||
|
- support generics for similar to above?
|
||||||
|
- this might allow for "error filtering"
|
||||||
@@ -69,6 +69,9 @@ func (e *MultiError) Error() (errStr string) {
|
|||||||
numErrs = len(e.Errors)
|
numErrs = len(e.Errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
for idx, err := range e.Errors {
|
for idx, err := range e.Errors {
|
||||||
if (idx + 1) < numErrs {
|
if (idx + 1) < numErrs {
|
||||||
errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep)
|
errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep)
|
||||||
@@ -87,6 +90,9 @@ func (e *MultiError) AddError(err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
e.Errors = append(e.Errors, err)
|
e.Errors = append(e.Errors, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package multierr
|
package multierr
|
||||||
|
|
||||||
|
import (
|
||||||
|
`sync`
|
||||||
|
)
|
||||||
|
|
||||||
// MultiError is a type of error.Error that can contain multiple errors.
|
// MultiError is a type of error.Error that can contain multiple errors.
|
||||||
type MultiError struct {
|
type MultiError struct {
|
||||||
// Errors is a slice of errors to combine/concatenate when .Error() is called.
|
// Errors is a slice of errors to combine/concatenate when .Error() is called.
|
||||||
Errors []error `json:"errors"`
|
Errors []error `json:"errors"`
|
||||||
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
|
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
|
||||||
ErrorSep string `json:"separator"`
|
ErrorSep string `json:"separator"`
|
||||||
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
= `netx` Notes
|
||||||
|
Brent Saner <bts@square-r00t.net>
|
||||||
|
Last rendered {localdatetime}
|
||||||
|
:doctype: book
|
||||||
|
:docinfo: shared
|
||||||
|
:data-uri:
|
||||||
|
:imagesdir: images
|
||||||
|
:sectlinks:
|
||||||
|
:sectnums:
|
||||||
|
:sectnumlevels: 7
|
||||||
|
:toc: preamble
|
||||||
|
:toc2: left
|
||||||
|
:idprefix:
|
||||||
|
:toclevels: 7
|
||||||
|
:source-highlighter: rouge
|
||||||
|
:docinfo: shared
|
||||||
|
|
||||||
|
[id="4in6"]
|
||||||
|
== IPv4 Addresses as IPv6
|
||||||
|
There are, effectively, two RFC-specified ways of representing an IPv4 address in IPv6 notation/address space/context.
|
||||||
|
|
||||||
|
Per https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5[RFC 4291 § 2.5.5^]:
|
||||||
|
|
||||||
|
* _IPv4-Compatible IPv6 Address_ (https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1[RFC 4291 § 2.5.5.1^]) -- `::/96`
|
||||||
|
** e.g. `::192.0.2.10`
|
||||||
|
* _IPv4-Mapped IPv6 Address_ (https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2[RFC 4291 § 2.5.5.2^]) -- `::ffff:0:0/96`
|
||||||
|
** e.g. `::ffff:192.0.2.10`
|
||||||
|
|
||||||
|
The former is considered deprecated, whereas the latter is considered the modern proper representation.
|
||||||
|
Some naïve or legacy software, however, still use the former.
|
||||||
|
|
||||||
|
An IPv6 address is *128 bits* (hence a prefix length/mask of `/128` representing a complete address), or *16 bytes* (`128 / 8`).
|
||||||
|
|
||||||
|
An IPv4 address is *32 bits* (hence a prefix length/mask of `/32` representing a complete address), or *4 bytes* (`32 / 8`).
|
||||||
|
|
||||||
|
This is why both the "IPv4-compatible" and "IPv4-mapped" IPv6 addresses are within a reserved prefix length of 96 bits (`/96`) - it leaves a remaining
|
||||||
|
*32 bits* (`128 - 96`) for a complete IPv4 address.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
The IPv4-Compatible and IPv4-Mapped prefixes *are not* intended for routing!
|
||||||
|
|
||||||
|
They are used for *syntactic/contextual* use.
|
||||||
|
|
||||||
|
For routing/actual IPv4 <=> IPv6 translation, see https://datatracker.ietf.org/doc/html/rfc6052[RFC 6052^] and https://datatracker.ietf.org/doc/html/rfc8215[RFC 8215^].
|
||||||
|
|
||||||
|
This library may add supporting functions for these _translation_ prefixes at a later time.
|
||||||
|
====
|
||||||
|
|
||||||
|
[id="4in6_addr"]
|
||||||
|
=== Addresses
|
||||||
|
While https://pkg.go.dev/net/netip#ParseAddr[`net/netip.ParseAddr`^] (or https://pkg.go.dev/net/netip#MustParseAddr[`net/netip.MustParseAddr`^]) will handle
|
||||||
|
*parsing* the "IPv4-Compatible" format just fine, it will treat it *as* an IPv6 address -- not an "IPv6-wrapped/IPv6-mapped IPv4" address.
|
||||||
|
|
||||||
|
[%collapsible]
|
||||||
|
.For example...
|
||||||
|
====
|
||||||
|
[source,go]
|
||||||
|
----
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Printf(
|
||||||
|
"::192.0.2.10 -> %s\n",
|
||||||
|
netip.MustParseAddr("::192.0.2.10").String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
will print:
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
::192.0.2.10 -> ::c000:20a
|
||||||
|
----
|
||||||
|
====
|
||||||
|
|
||||||
|
For addresses, this is an easy enough fix:
|
||||||
|
|
||||||
|
. Take the IPv6 as a byte slice, `b` (`b = ip4in6.AsSlice()`)
|
||||||
|
. Slice out the last 4 bytes as `b2` (`b2 = b[12:16]`)
|
||||||
|
. Parse `b2` as an IPv4 address (`ip4 = netip.FromSlice(b2)`)
|
||||||
|
|
||||||
|
For the visual learners, the original address (`::192.0.2.10` or, more properly, `::c000:20a`) can be represented in multiple ways.
|
||||||
|
|
||||||
|
[%collapsible]
|
||||||
|
.For instance...
|
||||||
|
====
|
||||||
|
* A "canonical"/shorted IPv6 address: `::c000:20a`
|
||||||
|
* A fully "exploded" IPv6 address: `0000:0000:0000:0000:0000:0000:c000:020a`
|
||||||
|
* Hex bytes:
|
||||||
|
** `00:00:00:00:00:00:00:00:00:00:00:00:C0:00:02:0A`
|
||||||
|
** `0x000000000000000000000000C000020A`
|
||||||
|
** Via `xxd(1)`:
|
||||||
|
+
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
# xxd -c4 -g 1
|
||||||
|
00000000: 00 00 00 00 ....
|
||||||
|
00000004: 00 00 00 00 ....
|
||||||
|
00000008: 00 00 00 00 ....
|
||||||
|
0000000c: c0 00 02 0a ....
|
||||||
|
----
|
||||||
|
** Via `hexdump(1)`:
|
||||||
|
+
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
# hexdump -v -e '"%08_ax: " 4/1 "0x%02X ""\n"'
|
||||||
|
00000000: 0x00 0x00 0x00 0x00
|
||||||
|
00000004: 0x00 0x00 0x00 0x00
|
||||||
|
00000008: 0x00 0x00 0x00 0x00
|
||||||
|
0000000c: 0xC0 0x00 0x02 0x0A
|
||||||
|
----
|
||||||
|
* A Go byte slice:
|
||||||
|
** Either as explicit `byte`:
|
||||||
|
+
|
||||||
|
[source,go]
|
||||||
|
----
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xC0, 0x00, 0x02, 0x0A,
|
||||||
|
}
|
||||||
|
----
|
||||||
|
** Or as `uint8` (a `byte` is, quite literally, https://go.dev/ref/spec#Numeric_types[just a type alias for `uint8`^]):
|
||||||
|
+
|
||||||
|
[source,go]
|
||||||
|
----
|
||||||
|
[]byte{
|
||||||
|
uint8(0), uint8(0), uint8(0), uint8(0),
|
||||||
|
uint8(0), uint8(0), uint8(0), uint8(0),
|
||||||
|
uint8(0), uint8(0), uint8(0), uint8(0),
|
||||||
|
uint8(192), uint8(0), uint8(2), uint8(10),
|
||||||
|
}
|
||||||
|
----
|
||||||
|
====
|
||||||
|
|
||||||
|
When seeing it represented as a slice of `uint8`, some things should start to click mentally looking at the last
|
||||||
|
four bytes.
|
||||||
|
|
||||||
|
An IPv4 address can then be derived by taking the last four bytes (`[]byte{...}[12:16]`):
|
||||||
|
|
||||||
|
* `[]byte{0xC0, 0x00, 0x02, 0x0A}`
|
||||||
|
* `[]byte(uint8(192), uint8(0), uint8(2), uint8(10))`
|
||||||
|
|
||||||
|
As you may have guessed by now, this is the byte format of the IPv4 address `192.0.2.10`.
|
||||||
|
|
||||||
|
The `netx.UnmapAddr()` function handles this cleanly.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
If you instead prefer to think in _prefixing/masking_, the last four bytes are *32 bits* (`8 * 4`) -- the size of an IPv4 address.
|
||||||
|
|
||||||
|
The "compatible" mapping is '::/96', and an IPv6 address is *128 bits* (*16 bytes*, `128 / 8`) total -- `128 - 96 == 32 bits`, or *4 bytes*.
|
||||||
|
|
||||||
|
[id="4in6_pfx"]
|
||||||
|
=== Prefixes
|
||||||
|
Prefixes are a little more tricky.
|
||||||
|
|
||||||
|
Prefixing for IPv4-Compatible/IPv4-Mapped IPv6 addresses works a bit oddly because generally the "4-in-6" addresses are
|
||||||
|
meant to be purely representative - a way to represent IPv4 addresses in IPv6 implementations.
|
||||||
|
|
||||||
|
But there are cases where one needs a *prefix* represented.
|
||||||
|
This leads to the awkward case where an IPv4 prefix length (e.g. `/24`) needs to be represented as an IPv6 prefix length (e.g. `/120`).
|
||||||
|
|
||||||
|
While this module (`netx`) does offer helper functions to calculate this (`netx.IP4MapPfx6to4()`, `netx.IP4MapPfx4to6()`),
|
||||||
|
the `netx.UnmapPfx()` function is offered as a convenience function that handles the translation from an IPv6 representation
|
||||||
|
of an IPv4 prefix into a fully "native" IPv4 prefix.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
`netx.IP4MapPfx4to6(len4 uint8)` simply takes the prefix length `len4`, and (if `len4 <= 32`) adds `96`.
|
||||||
|
|
||||||
|
`netx.IP4MapPfx6to4(len6 uint8)` simply takes the prefix length `len6`, and (if `len6 >= 96`) subtracts `96`.
|
||||||
|
====
|
||||||
|
|
||||||
|
This is achieved by:
|
||||||
|
|
||||||
|
. Taking the address from the prefix (`ip4in6 = pfx4in6.Addr()`)
|
||||||
|
.. And <<4in6_addr, converting to native IPv4>> (`ip4 = netx.UnmapAddr(ip4in6)`)
|
||||||
|
. Taking the IPv6 prefix length (`len6 = pfx4in6.Bits()`)
|
||||||
|
.. And converting it to an IPv4 prefix length (`len4 = netx.IP4MapPfx6to4(uint8(len6))`)
|
||||||
|
. Then re-assembling (`pfx4 = netip.PrefixFrom(ip4, int(len4))`)
|
||||||
|
|
||||||
|
|
||||||
|
[id="misc"]
|
||||||
|
== Miscellaneous
|
||||||
|
|
||||||
|
[id="misc_unspec"]
|
||||||
|
=== Unspecified Address
|
||||||
|
There are two "unspecified" addresses for each family:
|
||||||
|
|
||||||
|
* IPv4
|
||||||
|
** `0.0.0.0/0`
|
||||||
|
** `0.0.0.0/32`
|
||||||
|
* IPv6
|
||||||
|
** `::/0`
|
||||||
|
** `::/128`
|
||||||
|
|
||||||
|
For each family, the `/0` prefix length
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"go4.org/netipx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ip4in6Legacy96 string = "::"
|
||||||
|
ip4in6Modern96 string = "::ffff:"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// IPv4 documentation prefixes (RFC 5737)
|
||||||
|
ipDoc4pfxs []netip.Prefix = []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("192.0.2.0/24"),
|
||||||
|
netip.MustParsePrefix("198.51.100.0/24"),
|
||||||
|
netip.MustParsePrefix("203.0.113.0/24"),
|
||||||
|
}
|
||||||
|
ipDoc4 *netipx.IPSet = MustIPSetFromNative(nil, ipDoc4pfxs, nil)
|
||||||
|
// IPv6 documentation prefixes (RFC 3849, RFC 9637)
|
||||||
|
ipDoc6pfxs []netip.Prefix = []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("2001:db8::/32"),
|
||||||
|
netip.MustParsePrefix("3fff::/20"),
|
||||||
|
}
|
||||||
|
ipDoc6 *netipx.IPSet = MustIPSetFromNative(nil, ipDoc6pfxs, nil)
|
||||||
|
// combined documentation prefixes
|
||||||
|
ipDoc *netipx.IPSet = MustIPSetCombined(
|
||||||
|
[]*netipx.IPSet{
|
||||||
|
ipDoc4,
|
||||||
|
ipDoc6,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// CGNAT/LSN (RFC 6598)
|
||||||
|
cgnat4 netip.Prefix = netip.MustParsePrefix("100.64.0.0/10")
|
||||||
|
|
||||||
|
// IPv4-Compatible IPv6 Address, RFC 4291 § 2.5.5.1
|
||||||
|
ip4in6Legacy netip.Prefix = netip.MustParsePrefix(ip4in6Legacy96 + "/96")
|
||||||
|
// IPv4-Mapped IPv6 Address, RFC 4291 § 2.5.5.2
|
||||||
|
ip4in6Modern netip.Prefix = netip.MustParsePrefix(ip4in6Modern96 + "0:0/96")
|
||||||
|
// both
|
||||||
|
ip4in6 *netipx.IPSet = MustIPSetFromNative(
|
||||||
|
nil,
|
||||||
|
[]netip.Prefix{
|
||||||
|
ip4in6Legacy,
|
||||||
|
ip4in6Modern,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AFUnspec uint16 = unix.AF_UNSPEC
|
||||||
|
AFInet uint16 = unix.AF_INET
|
||||||
|
AFInet6 uint16 = unix.AF_INET6
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AFUnspec uint16 = windows.AF_UNSPEC
|
||||||
|
AFInet uint16 = windows.AF_INET
|
||||||
|
AFInet6 uint16 = windows.AF_INET6
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package dnsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
`errors`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBadChars error = errors.New("netx/dnsx: invalid characters/encoding were encountered")
|
||||||
|
ErrBadLabelLen error = errors.New("netx/dnsx: a label with invalid length was encountered")
|
||||||
|
ErrBadPtrLen error = errors.New("netx/dnsx: a PTR record with invalid length was encountered")
|
||||||
|
ErrBadPtrRoot error = errors.New("netx/dnsx: a PTR record with invalid root encountered")
|
||||||
|
)
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPtrAddr is like IsPtr but only returns the `addr` value.
|
||||||
|
func IsPtrAddr(s string) (addr net.IP) {
|
||||||
|
|
||||||
|
_, addr = IsPtr(s)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPtrChk is like IsPtr but only returns the `isPtr` value.
|
||||||
|
func IsPtrChk(s string) (isPtr bool) {
|
||||||
|
|
||||||
|
isPtr, _ = IsPtr(s)
|
||||||
|
|
||||||
|
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,7 @@
|
|||||||
|
package ptr
|
||||||
|
|
||||||
|
const (
|
||||||
|
TLD string = "arpa"
|
||||||
|
Zone4 string = "in-addr." + TLD
|
||||||
|
Zone6 string = "ip6." + TLD
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
Package ptr includes some helper functions for working with PTR records/zones
|
||||||
|
and mapping/converting them to/from [net/netip.Addr] and [net/netip.Prefix] representations respectively.
|
||||||
|
*/
|
||||||
|
package ptr
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ptr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidBounds error = errors.New("invalid PTR boundary")
|
||||||
|
ErrInvalidPfx error = errors.New("invalid prefix")
|
||||||
|
ErrInvalidPtr error = errors.New("invalid PTR")
|
||||||
|
)
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
package ptr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"go4.org/netipx"
|
||||||
|
"r00t2.io/subnetter/netsplit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddrPtr returns the PTR lookup name of IP address ip (as a [net/netip.Addr]), normalized (lowercased and no trailing dot).
|
||||||
|
func AddrPtr(ip netip.Addr) (ptr string, err error) { return IpStrPtr(ip.String()) }
|
||||||
|
|
||||||
|
// IpPtr returns the PTR lookup name of IP address ip (as a [net.IP]), normalized (lowercased and no trailing dot).
|
||||||
|
func IpPtr(ip net.IP) (ptr string, err error) { return IpStrPtr(ip.String()) }
|
||||||
|
|
||||||
|
// IpStrPtr returns the PTR lookup name of IP address ip (as a string), normalized (lowercased and no trailing dot).
|
||||||
|
func IpStrPtr(ip string) (ptr string, err error) {
|
||||||
|
|
||||||
|
if ptr, err = dns.ReverseAddr(ip); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr = normalizePtr(ptr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PfxToPtr returns the PTR zone name(s) for a specified prefix.
|
||||||
|
Any host bits set in pfx will be masked off without error
|
||||||
|
(e.g. 192.0.2.20/24 -> 192.0.2.0/24).
|
||||||
|
|
||||||
|
err will be [ErrInvalidBounds] if pfx prefix length is longer than a /24 (e.g. a /32) for IPv4
|
||||||
|
or longer than the last/shorter than the first 4-bit/nibble/nybble boundary - /124 (e.g. a /128)
|
||||||
|
and /4 respectively - for IPv6.
|
||||||
|
*/
|
||||||
|
func PfxToPtr(pfx netip.Prefix) (ptrZones []string, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var tgtSz uint8
|
||||||
|
var n *net.IPNet
|
||||||
|
var maxBitLen int
|
||||||
|
var pfxs []*netip.Prefix
|
||||||
|
var c *netsplit.CIDRSplitter
|
||||||
|
|
||||||
|
if !pfx.IsValid() {
|
||||||
|
err = ErrInvalidPfx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pfx = pfx.Masked()
|
||||||
|
|
||||||
|
if pfx.Addr().Is4() {
|
||||||
|
maxBitLen = 24
|
||||||
|
if pfx.Bits() <= 24 {
|
||||||
|
tgtSz = 24
|
||||||
|
}
|
||||||
|
if pfx.Bits() <= 16 {
|
||||||
|
tgtSz = 16
|
||||||
|
}
|
||||||
|
if pfx.Bits() <= 8 {
|
||||||
|
tgtSz = 8
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
maxBitLen = 124
|
||||||
|
if pfx.Bits() < 4 {
|
||||||
|
err = ErrInvalidBounds
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tgtSz = uint8(pfx.Bits())
|
||||||
|
for tgtSz%4 != 0 {
|
||||||
|
tgtSz++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pfx.Bits() > maxBitLen {
|
||||||
|
err = ErrInvalidBounds
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c = &netsplit.CIDRSplitter{
|
||||||
|
PrefixLength: tgtSz,
|
||||||
|
BaseSplitter: new(netsplit.BaseSplitter),
|
||||||
|
}
|
||||||
|
n = netipx.PrefixIPNet(pfx)
|
||||||
|
c.BaseSplitter.SetParent(*n)
|
||||||
|
|
||||||
|
if pfxs, _, err = c.Split(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ptrZones = make([]string, len(pfxs))
|
||||||
|
for idx = range pfxs {
|
||||||
|
ptrZones[idx] = pfxZonePtr(*pfxs[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PtrToAddr returns a [net/netip.Addr] from a PTR record name.
|
||||||
|
func PtrToAddr(ptr string) (addr netip.Addr, err error) {
|
||||||
|
|
||||||
|
var pfx netip.Prefix
|
||||||
|
|
||||||
|
if pfx, err = PtrToPfx(ptr); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addr = pfx.Addr()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PtrToPfx returns a [net/netip.Prefix] from a PTR zone (or record).
|
||||||
|
|
||||||
|
The same boundary rules as [PfxToPtr] are in place; delegated subzones (RFC 2317) are not supported.
|
||||||
|
If an invalid PTR is received, err will be [ErrInvalidPtr].
|
||||||
|
*/
|
||||||
|
func PtrToPfx(ptr string) (pfx netip.Prefix, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var ok bool
|
||||||
|
var b []byte
|
||||||
|
var sfx string
|
||||||
|
var cidr uint8
|
||||||
|
var spl []string
|
||||||
|
var addr netip.Addr
|
||||||
|
|
||||||
|
ptr = normalizePtr(ptr)
|
||||||
|
spl = strings.Split(ptr, ".")
|
||||||
|
if len(spl) < 2 {
|
||||||
|
err = ErrInvalidPtr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sfx = strings.Join(spl[len(spl)-2:], ".")
|
||||||
|
spl = strings.Split(normalizePtr(strings.TrimSuffix(ptr, sfx)), ".")
|
||||||
|
|
||||||
|
slices.Reverse(spl)
|
||||||
|
|
||||||
|
switch sfx {
|
||||||
|
case Zone4:
|
||||||
|
switch len(spl) {
|
||||||
|
case 0:
|
||||||
|
cidr = 0
|
||||||
|
spl = []string{"0", "0", "0", "0"}
|
||||||
|
case 1, 2, 3, 4:
|
||||||
|
cidr = uint8(len(spl) * 8)
|
||||||
|
for len(spl) < 4 {
|
||||||
|
spl = append(spl, "0")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = ErrInvalidPtr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if addr, err = netip.ParseAddr(
|
||||||
|
strings.Join(spl, "."),
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case Zone6:
|
||||||
|
if len(spl) > 32 {
|
||||||
|
err = ErrInvalidPtr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(spl) == 0 {
|
||||||
|
cidr = 0
|
||||||
|
spl = make([]string, 32)
|
||||||
|
for idx = 0; idx < len(spl); idx++ {
|
||||||
|
spl[idx] = "0"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cidr = uint8(len(spl) * 4)
|
||||||
|
for len(spl) < 32 {
|
||||||
|
spl = append(spl, "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b, err = hex.DecodeString(
|
||||||
|
strings.ToLower(strings.Join(spl, "")),
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if addr, ok = netip.AddrFromSlice(b); !ok {
|
||||||
|
err = ErrInvalidPfx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = ErrInvalidPtr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pfx, err = addr.Prefix(int(cidr)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePtr(ptr string) (norm string) { return strings.ToLower(strings.TrimRight(ptr, ".")) }
|
||||||
|
|
||||||
|
func pfxZonePtr(pfx netip.Prefix) (ptr string) {
|
||||||
|
|
||||||
|
if pfx.Addr().Is4() {
|
||||||
|
ptr = pfxZonePtr4(pfx)
|
||||||
|
} else {
|
||||||
|
ptr = pfxZonePtr6(pfx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pfxZonePtr4(pfx netip.Prefix) (ptr string) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var b []byte
|
||||||
|
var s []string
|
||||||
|
|
||||||
|
idx = pfx.Bits() / 8
|
||||||
|
|
||||||
|
b = pfx.Addr().AsSlice()[0:idx]
|
||||||
|
s = make([]string, len(b))
|
||||||
|
|
||||||
|
for i := 0; i < idx; i++ {
|
||||||
|
s[idx-1-i] = strconv.Itoa(int(uint8(b[i])))
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr = strings.Join(s, ".") + "." + Zone4
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pfxZonePtr6(pfx netip.Prefix) (ptr string) {
|
||||||
|
|
||||||
|
var b byte
|
||||||
|
var nib byte
|
||||||
|
var numNibs int
|
||||||
|
var b16 [16]byte
|
||||||
|
var sb *strings.Builder = new(strings.Builder)
|
||||||
|
|
||||||
|
numNibs = pfx.Bits() / 4
|
||||||
|
b16 = pfx.Addr().As16()
|
||||||
|
|
||||||
|
for i := numNibs - 1; i >= 0; i-- {
|
||||||
|
b = b16[i/2]
|
||||||
|
if i%2 == 0 {
|
||||||
|
nib = b >> 4
|
||||||
|
} else {
|
||||||
|
nib = b & 0x0f
|
||||||
|
}
|
||||||
|
fmt.Fprintf(
|
||||||
|
sb,
|
||||||
|
"%x.",
|
||||||
|
nib,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sb.WriteString(Zone6)
|
||||||
|
|
||||||
|
ptr = sb.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
Package netx includes extensions to the stdlib [net] module.
|
||||||
|
|
||||||
|
It provides many functions that should be significantly more helpful for those working with networking a significant amount.
|
||||||
|
|
||||||
|
Some functions may be "missing". This is by design; the corresponding functionality should be available in stdlib.
|
||||||
|
|
||||||
|
For example, while there is an [IsAddr4Compat] function, there is no "IsAddr4Mapped". This is because this can be accomplished via the following:
|
||||||
|
|
||||||
|
var ip netip.Addr
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
ip.Is4In6()
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
If you are looking for specific functionality in this library but are unable to find it,
|
||||||
|
this is because the functionality already exists in (in the author's opinion) a reasonably convenient
|
||||||
|
methodology via one of the following:
|
||||||
|
|
||||||
|
- [net]
|
||||||
|
- [net/netip]
|
||||||
|
- [go4.org/netipx]
|
||||||
|
|
||||||
|
# IP Family/Versioning
|
||||||
|
|
||||||
|
There are different ways of expressing an IP address/network's version (e.g. IPv4, IPv6).
|
||||||
|
|
||||||
|
Some libraries expect an integer representation (e.g. 4, 6).
|
||||||
|
|
||||||
|
Some libraries expect a system/OS-constant instead (e.g. unix.AF_INET, unix.AF_INET6, windows.AF_INET, windows.AF_INET6).
|
||||||
|
|
||||||
|
The following functions are offered to assist with this translation:
|
||||||
|
|
||||||
|
- [FamilyToVer]
|
||||||
|
- [GetAddrFamily]
|
||||||
|
- [GetIpFamily]
|
||||||
|
- [IpVerStr]
|
||||||
|
- [VerToFamily]
|
||||||
|
|
||||||
|
The following constants are offered to make cross-platform development more fluid (these are set according to the build target):
|
||||||
|
|
||||||
|
- [AFUnspec]
|
||||||
|
- [AFInet]
|
||||||
|
- [AFInet6]
|
||||||
|
|
||||||
|
# IP Sets
|
||||||
|
|
||||||
|
This library provides convenience functions for getting a [netipx.IPSet] in single-entry functions:
|
||||||
|
|
||||||
|
- [IPSetCombined]
|
||||||
|
- [IPSetFrom]
|
||||||
|
- [IPSetFromNative]
|
||||||
|
- [MustIPSetCombined]
|
||||||
|
- [MustIPSetFrom]
|
||||||
|
- [MustIPSetFromNative]
|
||||||
|
|
||||||
|
# Canonical String Representation
|
||||||
|
|
||||||
|
Go's stdlib [net] and [net/netip] don't always properly display or handle the proper string formatting per RFC conventions.
|
||||||
|
|
||||||
|
The following functions assist with this:
|
||||||
|
|
||||||
|
- [AddrRfc]
|
||||||
|
- [IpRfc]
|
||||||
|
- [IpRfcStr]
|
||||||
|
- [IpStripRfcStr]
|
||||||
|
- [IsBracketedIp6]
|
||||||
|
- [IsPrefixNet]
|
||||||
|
|
||||||
|
# Netmask and CIDR Conversions
|
||||||
|
|
||||||
|
An IPv4 network size can be represented by both a netmask (e.g. "255.255.255.0") or a CIDR prefix length (e.g. "/24"),
|
||||||
|
as well as a numeric bitmask form (which is different from the prefix length).
|
||||||
|
|
||||||
|
This module offers functions that assist with easy translation between these formats:
|
||||||
|
|
||||||
|
- [Cidr4ToIPMask]
|
||||||
|
- [Cidr4ToMask]
|
||||||
|
- [Cidr4ToStr]
|
||||||
|
- [IPMask4ToCidr]
|
||||||
|
- [IPMask4ToMask]
|
||||||
|
- [IPMask4ToStr]
|
||||||
|
- [Mask4StrToCidr]
|
||||||
|
- [Mask4StrToIPMask]
|
||||||
|
- [Mask4StrToMask]
|
||||||
|
- [Mask4ToCidr]
|
||||||
|
- [Mask4ToIPMask]
|
||||||
|
- [Mask4ToStr]
|
||||||
|
|
||||||
|
# IPv4 Addresses as IPv6
|
||||||
|
|
||||||
|
There are two canonical ways of representing an IPv4 address as an IPv6 address, per RFC 4291 ([section 2.5.5]):
|
||||||
|
|
||||||
|
- "IPv4-Compatible IPv6 Addresses" (RFC [4291 § 2.5.5.1]) — ::/96
|
||||||
|
- "IPv4-Mapped IPv6 Addresses" (RFC [4291 § 2.5.5.2]) — ::ffff:0:0/96
|
||||||
|
|
||||||
|
The former (::/96) is considered deprecated, and there is only parsing support for it in Go stdlib.
|
||||||
|
|
||||||
|
There are a number of functions tailored for use with both of these formats in this module:
|
||||||
|
|
||||||
|
- [Addr4in6Compat]
|
||||||
|
- [Addr4in6Mapped]
|
||||||
|
- [IP4MapPfx4to6]
|
||||||
|
- [IP4MapPfx6to4]
|
||||||
|
- [IsAddr4Compat]
|
||||||
|
- [Pfx4in6Compat]
|
||||||
|
- [Pfx4in6Mapped]
|
||||||
|
- [UnmapAddr]
|
||||||
|
- [UnmapPfx]
|
||||||
|
|
||||||
|
For more technical/detailed information, refer to the ./NOTES.adoc file in this module's
|
||||||
|
git repository, specifically the "IPv4 Addresses as IPv6" section.
|
||||||
|
|
||||||
|
# Miscellaneous Functions
|
||||||
|
|
||||||
|
Other functions that may be of use:
|
||||||
|
|
||||||
|
- [IsPublic] — Note that this does not guarantee that it is a *valid* public IP address (especially in the case of IPv6),
|
||||||
|
simply that it isn't in a non-WAN-routable prefix reserved by current standards.
|
||||||
|
- [HasSubnet]
|
||||||
|
|
||||||
|
[section 2.5.5]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5
|
||||||
|
[4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1
|
||||||
|
[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
|
||||||
|
*/
|
||||||
|
package netx
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBadMask4Str error = errors.New("netx: unknown/bad IPv4 netmask dotted quad")
|
||||||
|
ErrBadNetFam error = errors.New("netx: unknown/bad IP network family")
|
||||||
|
)
|
||||||
+1231
File diff suppressed because it is too large.
Load diff
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user