checking in some WIP

* added some netx funcs
* added netx/dnsx
* currently updating docs and adding *x funcs to sprigx
This commit is contained in:
brent saner
2026-02-24 17:41:57 -05:00
parent 4770052b52
commit c6fc692f5e
14 changed files with 2773 additions and 646 deletions

View File

@@ -15,23 +15,59 @@ for f in $(find . -type f -iname "README.adoc"); do
nosuffix="${filename%.*}"
pfx="${docsdir}/${nosuffix}"
# Render HTML, include in commit
newf="${pfx}.html"
asciidoctor -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
echo "Generated ${newf} from ${f}"
git add "${newf}"
# If asciidoctor-pdf is installed, render as PDF for local use
# (Does not get added to commit, and *.pdf is in .gitignore for a reason)
if command -v asciidoctor-pdf &> /dev/null;
then
newf="${pfx}.pdf"
asciidoctor-pdf -a ROOTDIR="${orig}/" -o "${newf}" "${f}"
fi
# If pandoc is installed, render to "GitHub-flavored Markdown" for better rendering on forks/mirrors
# and marginally better rendering on https://pkg.go.dev/ and add to commit.
#
# <rant>
# There is no such thing as "Markdown".
# The closest thing you have to any sort of standard is https://daringfireball.net/projects/markdown/
# but everybody and their mother adds their own "extensions"/"flavor", and sometimes even
# change how formatting works compared to the Daring Fireball/John Gruber spec (the original creator of the "syntax").
# Ergo "Markdown" inherently has no meaning.
# It's one of the worst formatting languages out there - just because it's popular doesn't mean it's good.
#
# If you're writing docs, you should stick to one of these which have defined, canonical, standardized
# syntax:
# * AsciiDoc/AsciiDoctor
# * Supports much more extensive formatting than any Markdown flavor I've seen
# * Source/raw/unrendered still *quite* readable by human eyes
# * Somewhat limited parsers/renderers
# * https://asciidoc.org/
# * https://asciidoctor.org/
# * DocBook
# * Supports even more extensive and flexible but exact formatting
# * Great for publishing, though - especially if you need control over formatting/layout
# * XML-based
# * Harder to read in plaintext, but fairly doable (XML lends to decent mental rendering)
# * Very wide support for parsing/rendering
# * https://docbook.org/
# * LaTex
# * Allows for *very* extensive domain-specific ligature/representation (very common in mathematic/scientific literature)
# * But nigh unreadable by human eyes unless you've rather familiar with it
# * Parsing/rendering support about on-par with DocBook
# * https://www.latex-project.org/
# </rant>
if command -v pandoc &> /dev/null;
then
newf="${pfx}.md"
set +e
#asciidoctor -a ROOTDIR="${orig}/" -b docbook -o - "${f}" | pandoc -f docbook -t markdown_strict -o "${newf}"
#asciidoctor -a ROOTDIR="${orig}/" -b html -o - "${f}" | pandoc -f html -t markdown_strict -o "${newf}"
asciidoctor -a ROOTDIR="${orig}/" -b html -o - "${f}" | pandoc -f html -t gfm -o "${newf}"
if [ $? -eq 0 ];
then

12
netx/dnsx/errors.go Normal file
View File

@@ -0,0 +1,12 @@
package dnsx
import (
`errors`
)
var (
ErrBadChars error = errors.New("netx/dnsx: invalid characters/encoding were encountered")
ErrBadLabelLen error = errors.New("netx/dnsx: a label with invalid length was encountered")
ErrBadPtrLen error = errors.New("netx/dnsx: a PTR record with invalid length was encountered")
ErrBadPtrRoot error = errors.New("netx/dnsx: a PTR record with invalid root encountered")
)

571
netx/dnsx/funcs.go Normal file
View File

@@ -0,0 +1,571 @@
package dnsx
import (
`bytes`
`encoding/base32`
`fmt`
`math`
`net`
`net/netip`
`strings`
`go4.org/netipx`
`r00t2.io/goutils/stringsx`
)
/*
AddrFromPtr returns a [net/netip.Addr] from a PTR record.
It is the inverse of [AddrToPtr].
See also [IpFromPtr].
*/
func AddrFromPtr(s string) (ip netip.Addr, err error) {
var idx int
var ipStr string
var tmpStr string
var spl []string = strings.Split(strings.TrimSuffix(s, "."), ".")
switch len(spl) {
case 6:
if strings.Join(spl[4:], ".") != "in-addr.arpa" {
err = ErrBadPtrRoot
return
}
ipStr = fmt.Sprintf("%s.%s.%s.%s", spl[3], spl[2], spl[1], spl[0])
case 34:
if strings.Join(spl[32:], ".") != "ip6.arpa" {
err = ErrBadPtrRoot
return
}
tmpStr = stringsx.Reverse(strings.ReplaceAll(strings.Join(spl[:32], ""), ".", ""))
for idx = 0; idx < len(tmpStr); idx++ {
if idx%4 == 0 && idx != 0 {
ipStr += ":"
}
ipStr += string(rune(tmpStr[idx]))
}
default:
err = ErrBadPtrLen
return
}
if ip, err = netip.ParseAddr(ipStr); err != nil {
return
}
return
}
/*
AddrToPtr returns a PTR record from ip.
It is the inverse of [AddrFromPtr].
It includes the root label at the end (the trailing period).
*/
func AddrToPtr(ip netip.Addr) (s string) {
var idx int
var b []byte
var ipStr string
if ip.Is6() {
ipStr = stringsx.Reverse(strings.ReplaceAll(ip.StringExpanded(), ":", ""))
ipStr = strings.Join(
strings.Split(ipStr, ""),
".",
)
s = fmt.Sprintf("%s.ip6.arpa.", ipStr)
} else {
b = make([]byte, 4)
copy(b, ip.AsSlice())
for idx = len(b) - 1; idx >= 0; idx-- {
ipStr += fmt.Sprintf("%d.", b[idx])
}
s = fmt.Sprintf("%s.in-addr.arpa.", ipStr)
}
return
}
/*
DnsStrToWire returns a wire-format of a DNS name.
No validation or conversion (other than to wire format) is performed,
and it is expected that any IDN(A)/Punycode translation has
*already been performed* such that recordNm is in the ASCII form.
(See [IsFqdn] for more information on IDN(A)/Punycode.)
For encoding reasons, if any given label/segment has a length of 0 or greater than 255 ([math.MaxUint8]),
[ErrBadLabelLen] will be returned.
See [DnsWireToStr] for the inverse.
*/
func DnsStrToWire(recordNm string) (recordNmBytes []byte, err error) {
var cLen int
var c []byte
var cStr string
var spl []string
var buf *bytes.Buffer = new(bytes.Buffer)
spl = strings.Split(strings.TrimSuffix(recordNm, "."), ".")
for _, cStr = range spl {
c = []byte(cStr)
cLen = len(c)
if !(cLen > 0 && cLen <= math.MaxUint8) {
err = ErrBadLabelLen
return
}
buf.Write(append([]byte{uint8(cLen)}, c...))
}
recordNmBytes = append(buf.Bytes(), 0x00)
return
}
/*
DnsWireToStr is the inverse of [DnsStrToWire]. A trailing . is not included.
For decoding reasons, it will exit with [ErrBadLabelLen] if recordNmBytes is nil/empty or
if no terminating nullbyte is found after 256 label characters have been encountered.
*/
func DnsWireToStr(recordNmBytes []byte) (recordNm string, err error) {
var c []byte
var cLen uint8
var arrLen int
var numChars int
var labels []string
var buf *bytes.Buffer
if recordNmBytes == nil || len(recordNmBytes) == 0 {
err = ErrBadLabelLen
return
}
buf = bytes.NewBuffer(recordNmBytes)
labels = make([]string, 0)
arrLen = len(recordNmBytes)
for {
if cLen, err = buf.ReadByte(); err != nil {
return
}
if cLen == 0x00 {
break
}
numChars += int(cLen)
if numChars > 255 {
err = ErrBadLabelLen
return
}
if numChars > arrLen {
err = ErrBadLabelLen
return
}
c = buf.Next(int(cLen))
labels = append(labels, string(c))
}
recordNm = strings.Join(labels, ".")
return
}
/*
IpFromPtr is like [AddrFromPtr] but with a [net.IP] instead.
It is the inverse of [IpToPtr].
*/
func IpFromPtr(s string) (ip net.IP, err error) {
var a netip.Addr
if a, err = AddrFromPtr(s); err != nil {
return
}
ip = net.IP(a.AsSlice())
return
}
/*
IpToPtr is like [AddrToPtr] but with a [net.IP] instead.
It is the inverse of [IpFromPtr].
*/
func IpToPtr(ip net.IP) (s string) {
var a netip.Addr
a, _ = netipx.FromStdIP(ip)
s = AddrToPtr(a)
return
}
/*
IsFqdn returns a boolean indicating if s is an FQDN that strictly adheres to RFC format requirements.
It performs no lookups/resolution attempts or network operations otherwise.
It will return true for the "apex record" (e.g. the "naked domain"), as this is a valid assignable FQDN.
It will return false for wildcard records (see [IsFqdnWildcard]).
s may or may not end in a period (the root zone; "absolute" FQDNs) (0x00 in wire format).
# TLDs
Because valid TLDs are fairly dynamic and can change frequently,
validation is *not* performed against the TLD itself.
This only ensures that s has a TLD label conforming to the character rules in the referenced RFCs.
See [golang.org/x/net/publicsuffix] if precise TLD validation is required (though true TLD validation generally
requires fetching the current TLD lists from IANA at runtime like [github.com/bombsimon/tld-validator]).
# Special RFC-Defined Accommodations
RFC 2181 [§ 11] specifies that site-local DNS software may accommodate non-RFC-conforming rules.
This function may and likely will return false for these site-local deviations.
The Lookup* functions/mthods in [net] should be used to validate in these casts
if that accommodation is necessary.
Note that underscores are not valid for "true" FQDNs as they are only valid for e.g. SRV record names,
TXT records, etc. - not A/AAAA/CNAME, etc. - see RFC 8553 for details.
See the following functions for allowing additional syntax/rule validation
that have record-type-specific accommodations made:
* [IsFqdnDefinedTxt]
* [IsFqdnNsec3]
* [IsFqdnSrv]
* [IsFqdnWildcard]
# RFC Coverage
This function should conform properly to:
* RFC 952
* RFC 1034 and RFC 1035
* RFC 1123
* RFC 2181 (selectively, see above)
preferring the most up-to-date rules where relevant (e.g. labels may start with digits, as per RFC 1123).
It enforces/checks label and overall length limits as defined by RFC.
# IDN(A) and Punycode
Note that it expects the ASCII-only/presentation form of a record name and
will not perform any IDN/IDNA nor Punycode translation.
If a caller anticipates FQDNs in their localized format,
the caller must perform translation first
(via e.g. [gitlab.com/golang-commonmark/puny], [golang.org/x/net/idna], etc.).
To reiterate, IDN/IDNA:
* RFC 3490
* RFC 5890
* RFC 5891
* RFC 5892
* RFC 5893
* RFC 5894
and Punycode (RFC 3492) *MUST* use their ASCII forms, NOT the localized/Unicode formats.
[§ 11]: https://datatracker.ietf.org/doc/html/rfc2181#section-11
*/
func IsFqdn(s string) (fqdn bool) {
var lbl string
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
if !commonFqdn(s) {
return
}
for _, lbl = range lbls {
if !IsLabel(lbl) {
return
}
}
fqdn = true
return
}
/*
IsFqdnDefinedTxt is like [IsFqdn] but explicitly *only* allows fully-qualified
RFC-defined TXT "subtypes":
* ACME DNS-01 (RFC 8555)
* BIMI (RFC draft [bimi])
* DKIM (RFC 6376, RFC 8301, RFC 8463)
* DKIM ATPS (RFC 6541)
* DMARC (RFC 7489, RFC 9091, RFC 9616)
* MTA-STS (RFC 8461)
* TLSRPT (RFC 8460)
Note that the following TXT "subtypes" do not have special formatting in labels/name,
and thus are not covered by this function:
* SPF (RFC 4408, RFC 7208)
[bimi]: https://datatracker.ietf.org/doc/html/draft-brand-indicators-for-message-identification
*/
func IsFqdnDefinedTxt(fqdn string) (isOk bool) {
var lbls []string = strings.Split(fqdn, ".")
if lbls == nil || len(lbls) < 2 {
return
}
switch lbls[0] {
case "_dmarc", "_mta-sts", "_acme-challenge":
if len(lbls) < 2 {
return
}
isOk = IsFqdn(strings.Join(lbls[1:], "."))
case "_smtp":
if len(lbls) <= 2 {
return
}
if lbls[1] != "_tls" {
return
}
isOk = IsFqdn(strings.Join(lbls[2:], "."))
default:
if !IsLabel(lbls[0]) {
return
}
switch lbls[1] {
case "_domainkey", "_atps", "_bimi":
isOk = IsFqdn(strings.Join(lbls[2:], "."))
}
}
// TODO
return
}
/*
IsFqdnNsec3 confirms (partially) that s is a valid NSEC3 record name.
Note that due to the record name being a base32 encoding of a *hash*, the validity
can't be 100% confirmed with certainty - only basic checks can be done.
NSEC3 can be found via:
* RFC 5155
* RFC 6840
* RFC 6944
* RFC 7129
* RFC 8198
* RFC 9077
* RFC 9157
* RFC 9276
* RFC 9905
At the time of writing, only one hashing algorithm (SHA-1) has been specified.
However, because this function does not check against the IANA registration at runtime,
it's possible that this changes but the library may not immediately reflect this.
*/
func IsFqdnNsec3(s string) (maybeNsec3 bool) {
var h []byte
var err error
var isAscii bool
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
if !commonFqdn(s) {
return
}
if len(lbls) <= 2 {
return
}
if len(lbls[0]) != 32 { // SHA1 is 160 bits/20 bytes digest, which is always 32 chars in base32(hex)
return
}
if h, err = base32.StdEncoding.DecodeString(strings.ToUpper(lbls[0])); err != nil {
return
}
if isAscii, err = stringsx.IsAsciiSpecial(
strings.ToLower(lbls[0]),
false, false, false, false,
[]byte{
// Normally, Base32 goes A-Z, 2-7
// but NSEC3 uses Base32Hex (RFC 4648 § 7),
// which is 0-9A-V
'0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h',
'j', 'k', 'm', 'n', 'p', 'q',
'r', 's', 't', 'u', 'v',
},
nil,
); err != nil {
return
}
if isAscii {
return
}
if len(h) != 20 {
return
}
maybeNsec3 = IsFqdn(strings.Join(lbls[1:], "."))
return
}
/*
IsFqdnSrv is like [IsFqdn], but explicitly *only* allows fully-qualified SRV records
(i.e. underscores must start the first two labels, and there must be at least two additional
labels after these labels).
Note that the protocol is not checked for validity, as that would require runtime
validation against a resource liable to change and would need to be fetched dynamically - see
the [IANA Protocol Numbers registry].
[IANA Protocol Numbers registry]: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
*/
func IsFqdnSrv(s string) (srv bool) {
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
if !commonFqdn(s) {
return
}
if len(lbls) <= 4 {
return
}
if !strings.HasPrefix(lbls[0], "_") {
return
}
if !strings.HasPrefix(lbls[1], "_") {
return
}
srv = IsFqdn(strings.Join(lbls[2:], "."))
return
}
// IsFqdnWildcard is like [IsFqdn] but explicitly *only* allows fully-qualified wildcard records.
func IsFqdnWildcard(s string) (wildcard bool) {
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
if len(lbls) < 2 {
return
}
if lbls[0] != "*" {
return
}
wildcard = IsFqdn(strings.Join(lbls[1:], "."))
return
}
// IsLabel returns true if s is a valid DNS label for standard records.
func IsLabel(s string) (isLbl bool) {
var err error
if strings.HasPrefix(s, "-") {
return
}
if strings.HasSuffix(s, "-") {
return
}
if isLbl, err = stringsx.IsAsciiSpecial(
s,
false, false, false, false,
[]byte{
'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9',
'-',
},
nil,
); err != nil {
return
}
return
}
/*
IsPtr returns true if s is a PTR (also called an "rDNS" or "reverse DNS" record) name.
If true, the IP is returned as well (otherwise it will be nil).
*/
func IsPtr(s string) (isPtr bool, addr net.IP) {
var err error
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
if len(lbls) < 6 {
return
}
if _, err = AddrFromPtr(s); err != nil {
return
}
isPtr = true
return
}
// commonFqdn is used to validate some rules common to all record names.
func commonFqdn(s string) (isOk bool) {
var err error
var lbl string
var isAscii bool
var labels []string
var domstr string = strings.ToLower(strings.TrimSuffix(s, "."))
if isAscii, err = stringsx.IsAsciiSpecial(
domstr, false, false, false, false,
[]byte{
'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9',
'-', '_', '.',
},
nil,
); err != nil {
return
}
if !isAscii {
return
}
if (len(domstr) + 1) > 255 { // +1 for root label
return
}
labels = strings.Split(domstr, ".")
for _, lbl = range labels {
if len(lbl) < 1 || len(lbl) > 63 {
return
}
}
// TODO?
isOk = true
return
}

29
netx/dnsx/funcs_test.go Normal file
View File

@@ -0,0 +1,29 @@
package dnsx
import (
`net/netip`
"testing"
)
func TestPtr(t *testing.T) {
var err error
var ptr string
var ip netip.Addr
var ipStr string = "::ffff:192.168.0.1"
var ptrStr string = "1.0.0.0.8.a.0.c.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa."
if ip, err = AddrFromPtr(ptrStr); err != nil {
t.Fatal(err)
}
t.Logf("PTR -> Addr: %s -> %s", ptrStr, ip.String())
if ip.String() != ipStr {
t.Fatalf("expect IP %v, got %v", ipStr, ip.String())
}
ptr = AddrToPtr(ip)
if ptr != ptrStr {
t.Fatalf("expect PTR %v, got %v", ptrStr, ptr)
}
t.Logf("Addr -> PTR: %s -> %s", ip.String(), ptr)
}

View File

@@ -102,11 +102,37 @@ func Cidr4ToStr(cidr uint8) (maskStr string, err error) {
return
}
/*
FamilyToVer returns a more "human-friendly" IP version from a system/lower-level IP family
([AFUnspec], [AFInet], [AFInet6]).
ipVer will be int(4) for [AFInet], int(6) for [AFInet6], int(0) for [AFUnspec], or
int(-1) for an unknown family.
*/
func FamilyToVer(family uint16) (ipVer int) {
switch family {
case AFInet:
ipVer = 4
case AFInet6:
ipVer = 6
case AFUnspec:
ipVer = 0
default:
ipVer = -1
}
return
}
/*
GetAddrFamily returns the network family of a [net/netip.Addr].
See also [GetIpFamily].
Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6).
(See [FamilyToVer] to get the associated higher-level value.)
If addr is not a "valid" IP address or the version can't be determined, family will be AFUnspec (usually 0x00/0).
*/
func GetAddrFamily(addr netip.Addr) (family uint16) {
@@ -131,6 +157,9 @@ func GetAddrFamily(addr netip.Addr) (family uint16) {
/*
GetIpFamily returns the network family of a [net.IP].
Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6).
(See [FamilyToVer] to get the associated higher-level value.)
See also [GetAddrFamily].
If ip is not a "valid" IP address or the version can't be determined,
@@ -158,6 +187,8 @@ If ip is an IPv4 address, it will simmply be the string representation (e.g. "20
If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]").
If the version can't be determined, rfcStr will be an empty string.
See also [IpRfcStr] for providing an IP address as a string.
*/
func IpRfc(ip net.IP) (rfcStr string) {
@@ -170,6 +201,56 @@ func IpRfc(ip net.IP) (rfcStr string) {
return
}
/*
IpRfcStr implements [IpRfc]/[AddrRfc] for string representations of an IP address s.
If s is an IPv6 address already in the bracketed RFC format,
then rfcStr will be equal to s.
If s is not a string representation of an IP address, rfcStr will be empty.
See [IpStripRfcStr] for the inverse (removing any brackets from s if present).
*/
func IpRfcStr(s string) (rfcStr string) {
var ip net.IP
if !IsIpAddr(s) {
return
}
if IsBracketedIp6(s) {
rfcStr = s
return
}
ip = net.ParseIP(s)
if ip == nil {
return
}
rfcStr = IpRfc(ip)
return
}
/*
IpStripRfcStr returns IP address string s without any brackets.
If s is not a valid IP address, stripStr will be empty.
*/
func IpStripRfcStr(s string) (stripStr string) {
if !IsIpAddr(s) {
return
}
if !IsBracketedIp6(s) {
stripStr = s
return
}
stripStr = strings.TrimPrefix(s, "[")
stripStr = strings.TrimSuffix(stripStr, "]")
return
}
/*
IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask].
@@ -257,6 +338,123 @@ func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) {
return
}
/*
IpVerStr provides the IP family of IP address/network string s.
s may be one of the following formats/syntaxes:
* 203.0.113.0
* 203.0.113.1
* 203.0.113.0/24
* 203.0.113.1/24
* 2001:db8::
* 2001:db8::1
* 2001:db8::/32
* 2001:db8::1/32
* [2001:db8::]
* [2001:db8::1]
Unlike [GetAddrFamily]/[GetIpFamily], this returns a more "friendly"
version - if s is not valid syntax, ipVer will be int(0),
otherwise ipVer will be int(4) for family IPv4 and int(6) for family IPv6.
(See [VerToFamily] to get the associated system/lower-level value.)
*/
func IpVerStr(s string) (ipVer int) {
var err error
var ipstr string
var p netip.Prefix
ipstr = strings.TrimPrefix(s, "[")
ipstr = strings.TrimSuffix(ipstr, "]")
if p, err = netip.ParsePrefix(ipstr); err != nil {
return
}
if p.Addr().Is6() {
ipVer = 6
} else {
ipVer = 4
}
return
}
/*
IsBracketedIp6 returns a boolean indicating if s is a valid bracket-enclosed IPv6 in string format
(e.g. "[2001:db8::1]").
It will return false for *non-bracketed* IPv6 addresses (e.g. "2001:db8::1"), IPv4 addresses,
or if s is not a valid IPv6 address string.
[IpRfcStr] or [IpStripRfcStr] can be used to coerce a string to a specific format.
*/
func IsBracketedIp6(s string) (isBrktdIp bool) {
var ip net.IP
var ipstr string
if IpVerStr(s) != 6 {
return
}
ipstr = strings.TrimPrefix(s, "[")
ipstr = strings.TrimSuffix(ipstr, "]")
if ip = net.ParseIP(ipstr); ip == nil {
return
}
isBrktdIp = ipstr == s
return
}
/*
IsIpAddr returns a boolean indicating if s is an IP address (either IPv4 or IPv6) in string format.
For IPv6, it will return true for both of these formats:
* 2001:db8::1
* [2001:db8::1]
[IsBracketedIp6] can be used to narrow down which form.
*/
func IsIpAddr(s string) (isIp bool) {
var err error
var a netip.Addr
if a, err = netip.ParseAddr(s); err != nil {
return
}
isIp = a.IsValid()
return
}
/*
IsPrefixNet returns true if s is a (valid) IP address or network (either IPv4 or IPv6) in:
<addr_or_net>/<prefix_len>
format.
*/
func IsPrefixNet(s string) (isNet bool) {
var err error
var p netip.Prefix
if p, err = netip.ParsePrefix(s); err != nil {
return
}
isNet = p.Masked().IsValid()
return
}
/*
Mask4ToCidr converts an IPv4 netmask *in bitmask form* to a CIDR prefix size/bit size/bit length.
@@ -408,3 +606,25 @@ func Mask4StrToMask(maskStr string) (mask uint32, err error) {
return
}
/*
VerToFamily takes a "human-readable" IP version ipVer (4 or 6) and returns
a system-level constant (e.g. [AFUnspec], [AFInet], [AFInet6]).
If not a known IP version (i.e. neither 4 nor 6), family will be [AFUnspec].
It is the inverse of [FamilyToVer].
*/
func VerToFamily(ipVer int) (family uint16) {
switch ipVer {
case 4:
family = AFInet
case 6:
family = AFInet6
default:
family = AFUnspec
}
return
}

View File

@@ -7,6 +7,29 @@ import (
"testing"
)
func TestFuncsDns(t *testing.T) {
var err error
var domBin []byte
var domStr string
var domEx string = "foo.r00t2.io"
if domBin, err = DnsStrToWire(domEx); err != nil {
t.Fatal(err)
}
t.Logf("Domain %s to wire: %#x\n", domEx, domBin)
if domStr, err = DnsWireToStr(domBin); err != nil {
t.Fatal(err)
}
t.Logf("Domain wire %#x to string: %s\n", domBin, domStr)
if domStr != domEx {
t.Fatalf("DNS str wrong (%s != %s)\n)", domStr, domEx)
}
t.Logf("Domain string %s matches %s", domStr, domEx)
return
}
func TestFuncsIP(t *testing.T) {
var err error

View File

@@ -0,0 +1,18 @@
package stringsx
import (
`fmt`
)
// Error conforms an [AsciiInvalidError] to an error interface.
func (a *AsciiInvalidError) Error() (errStr string) {
errStr = fmt.Sprintf(
"non-ASCII character '%c' at line:linepos %d:%d (byte %d), "+
"string position %d (byte %d): bytes %#x, UTF-8 codepoint U+%04X",
a.BadChar, a.Line, a.LineChar, a.LineByte,
a.Char, a.Byte, a.BadBytes, a.BadChar,
)
return
}

View File

@@ -1,11 +1,165 @@
package stringsx
import (
`bytes`
`errors`
`fmt`
`io`
`slices`
`strings`
`unicode`
)
/*
IsAscii returns true if all characters in string s are ASCII.
This simply wraps [IsAsciiSpecial]:
isAscii, err = IsAsciiSpecial(s, allowCtl, true, allowExt, true, nil, nil)
*/
func IsAscii(s string, allowCtl, allowExt bool) (isAscii bool, err error) {
if isAscii, err = IsAsciiSpecial(
s, allowCtl, true, allowExt, true, nil, nil,
); err != nil {
return
}
return
}
/*
IsAsciiBuf returns true if all of buffer buf is valid ASCII.
Note that the buffer will be consumed/read by this function.
This simply wraps [IsAsciiBufSpecial]:
isAscii, err = IsAsciiBufSpecial(r, allowCtl, true, allowExt, true, nil, nil)
*/
func IsAsciiBuf(r io.RuneReader, allowCtl, allowExt bool) (isAscii bool, err error) {
if isAscii, err = IsAsciiBufSpecial(
r, allowCtl, true, allowExt, true, nil, nil,
); err != nil {
return
}
return
}
/*
IsAsciiSpecial allows for specifying specific ASCII ranges.
allowCtl, if true, will allow control characters (0x00 to 0x1f inclusive).
allowPrint, if true, will allow printable characters (what most people think of
when they say "ASCII") (0x20 to 0x7f inclusive).
allowExt, if true, will allow for "extended ASCII" - some later dialects expand
to a full 8-bit ASCII range (0x80 to 0xff inclusive).
wsCtl, if true, "shifts" the "whitespace control characters" (\t, \n, \r) to the "printable" space
(such that allowPrint controls their validation). Thus:
IsAsciiSpecial(s, false, true, false, true, nil, nil)
has the same effect as specifying:
IsAsciiSpecial(s, false, true, false, (-), []byte("\t\n\r"), nil)
incl, if non-nil and non-empty, allows *additional* characters to be specified as included
that would normally *not* be allowed.
excl, if non-nil and non-empty, invalidates on additional characters that would normally be allowed.
excl, if specified, takes precedence over incl if specified.
An [AsciiInvalidError] will be returned on the first encountered invalid character.
*/
func IsAsciiSpecial(s string, allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (isAscii bool, err error) {
var buf *bytes.Buffer = bytes.NewBufferString(s)
if isAscii, err = IsAsciiBufSpecial(buf, allowCtl, allowPrint, allowExt, allowWs, incl, excl); err != nil {
return
}
return
}
/*
IsAsciiBufSpecial is the same as [IsAsciiSpecial] but operates on an [io.RuneReader].
Note that the buffer will be consumed/read by this function.
It will not return an [io.EOF] if encountered, but any other errors encountered will be returned.
It is expected that r will return an [io.EOF] when exhausted.
An [AsciiInvalidError] will be returned on the first encountered invalid character.
*/
func IsAsciiBufSpecial(r io.RuneReader, allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (isAscii bool, err error) {
var b rune
var bLen int
var nextNewline bool
var tmpErr *AsciiInvalidError = new(AsciiInvalidError)
// I know, I know. This is essentually a lookup table. Keeps it speedy.
var allowed [256]bool = getAsciiCharMap(allowCtl, allowPrint, allowExt, allowWs, incl, excl)
for {
if b, bLen, err = r.ReadRune(); err != nil {
if errors.Is(err, io.EOF) {
err = nil
isAscii = true
}
return
}
// Set these *before* OK
if nextNewline {
tmpErr.Line++
tmpErr.LineByte = 0
tmpErr.LineChar = 0
nextNewline = false
} else {
tmpErr.LineChar++
}
tmpErr.Char++
if b == '\n' {
nextNewline = true
}
if b == rune(0xfffd) {
// not even valid unicode
tmpErr.BadChar = b
tmpErr.BadBytes = []byte(string(b))
err = tmpErr
return
}
if bLen > 2 || b > 0xff {
// ASCII only occupies a single byte, ISO-8859-1 occupies 2
tmpErr.BadChar = b
tmpErr.BadBytes = []byte(string(b))
err = tmpErr
return
}
if !allowed[byte(b)] {
tmpErr.BadChar = b
tmpErr.BadBytes = []byte{byte(b)}
err = tmpErr
return
}
// Set these *after* OK
tmpErr.LineByte += uint64(bLen)
tmpErr.Byte += uint64(bLen)
}
isAscii = true
return
}
/*
LenSplit formats string `s` to break at, at most, every `width` characters.
@@ -252,6 +406,18 @@ func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted
return
}
// Reverse reverses string s. (It's absolutely insane that this isn't in stdlib.)
func Reverse(s string) (revS string) {
var rsl []rune = []rune(s)
slices.Reverse(rsl)
revS = string(rsl)
return
}
/*
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
@@ -313,6 +479,58 @@ func TrimSpaceRight(s string) (trimmed string) {
return
}
// getAsciiCharMap returns a lookup "table" for ASCII characters.
func getAsciiCharMap(allowCtl, allowPrint, allowExt, allowWs bool, incl, excl []byte) (charmap [256]bool) {
var idx uint8
if allowCtl {
for idx < 0x1f {
charmap[idx] = true
idx++
}
} else {
idx = 0x1f
}
if allowPrint {
for idx < 0x7f {
charmap[idx] = true
idx++
}
} else {
idx = 0x7f
}
if allowExt {
for {
charmap[idx] = true
if idx == 0xff {
break
}
idx++
}
} else {
idx = 0xff
}
if allowWs {
charmap['\t'] = true
charmap['\n'] = true
charmap['\r'] = true
}
if incl != nil && len(incl) > 0 {
for _, idx = range incl {
charmap[idx] = true
}
}
if excl != nil && len(excl) > 0 {
for _, idx = range excl {
charmap[idx] = false
}
}
return
}
// getNewLine is too unpredictable/nuanced to be used as part of a public API promise so it isn't exported.
func getNewLine(s string) (nl string) {

View File

@@ -37,6 +37,17 @@ type (
}
)
func TestFuncsAscii(t *testing.T) {
var err error
// var s string = "This is a §\nmulti-line\nstring 😀 with\nunicode text.\n"
var s string = "This is a §\nmulti-line\nstring with\nno unicode text.\n"
if _, err = IsAscii(s, false, true); err != nil {
t.Fatal(err)
}
}
func TestRedact(t *testing.T) {
var out string
@@ -171,6 +182,18 @@ func TestRedact(t *testing.T) {
}
}
func TestReverse(t *testing.T) {
var rev string
var s string = "012345679abcdef"
rev = Reverse(s)
if rev != "fedcba976543210" {
t.Errorf("reverse of s '%s'; expected 'fedcba976543210', got '%s'", s, rev)
}
t.Logf("s: %s\nReverse: %s", s, rev)
}
func TestTrimLines(t *testing.T) {
var out string

25
stringsx/types.go Normal file
View File

@@ -0,0 +1,25 @@
package stringsx
type (
/*
AsciiInvalidError is an error used to return an error for the IsAscii* validations.
It is returned on the first found instance of an invalid ASCII character.
*/
AsciiInvalidError struct {
// Line is a 0-indexed line number where the invalid character was found.
Line uint64
// LineByte is the 0-indexed byte position for the current Line.
LineByte uint64
// LineChar is a 0-indexed character (rune) position where the invalid character was found on line number Line.
LineChar uint64
// Byte is the 0-indexed byte position across the entire input.
Byte uint64
// Char is the 0-indexed character (rune) position across the entire input.
Char uint64
// BadChar is the invalid rune
BadChar rune
// BadBytes is BadChar as bytes.
BadBytes []byte
}
)

View File

@@ -17,6 +17,9 @@ Last rendered {localdatetime}
// BEGIN variable attributes
:sprig_ver: 3
:psutil_ver: 4
:git_owner: r00t2
:git_repo: go_goutils
:git_repo_full: {git_owner}/{git_repo}
:mod_me: r00t2.io/goutils
:pkg_me: tplx/sprigx
:src_root: https://git.r00t2.io
@@ -25,8 +28,12 @@ Last rendered {localdatetime}
:mod_sprig: github.com/Masterminds/sprig/v{sprig_ver}
:mod_psutil: github.com/shirou/gopsutil/v{psutil_ver}
:import_sprig: {mod_sprig}
:src_base: {src_root}/r00t2/go_goutils/src/branch/master
:src_dir: {src_base}/{pkg_me}
:src_base: {src_root}/{git_repo_full}
:src_git: {src_base}.git
:src_tree: {src_base}/src/branch/master
:src_raw: {src_base}/raw/branch/master
:src_dir: {src_tree}/{pkg_me}
:src_dir_raw: {src_raw}/{pkg_me}
:import_me: {mod_me}/{pkg_me}
:godoc_me: {godoc_root}/{import_me}
:godoc_sprig: {godoc_root}/{import_sprig}
@@ -42,11 +49,57 @@ They provide functions that offer more enriched use cases and domain-specific da
====
If you are reading this README on the Go Module Directory documentation ({godoc_me})
or the directory landing page ({src_dir}), it may not render correctly.
Anchor-links (links within this document to other sections of this document) will likely also not work.
Be sure to view it at properly via {src_dir}/README.adoc[the AsciiDoc rendering^]
or by downloading and viewing the {src_dir}/README.html[HTML version^] and/or {src_dir}/README.pdf[PDF version^].
Be sure to view it at properly via {src_dir}/README.adoc[the in-repo AsciiDoc rendering^]
or by downloading and viewing the {src_dir_raw}/README.html[HTML version^] in a browser locally
and/or <<rndr_pdf, rendering a PDF version>>.
====
[id="rndr"]
== How do I Render These Docs?
This documentation is written in https://asciidoc.org/[AsciiDoc^] (with https://asciidoctor.org/[AsciiDoctor^] extensions).
To re-render all docs (including <<rndr_pdf>>):
[source,bash, subs="attributes"]
----
git clone {src_git}
cd {git_repo}
.githooks/pre-commit/01-docgen
----
[id="rndr_html"]
=== HTML
HTML output is re-rendered and included in git {src_dir}/.githooks/pre-commit/01-docgen[on each commit^] automatically (via https://github.com/gabyx/Githooks[`github:gabyx/Githooks`^])
but can be re-rendered on-demand locally via:
[source,bash, subs="attributes"]
----
git clone {src_git}
cd {git_repo}
export orig_dir="$(pwd)"
cd {pkg_me}
asciidoctor -a ROOTDIR="${orig_dir}/" -o README.html README.adoc
----
[id="rndr_pdf"]
=== PDF
This documentation can be rendered to PDF via https://docs.asciidoctor.org/pdf-converter/latest/[`asciidoctor-pdf`^].
It is not included in git automatically because binary files that change on each commit is not a good idea for git,
especially for a repo that gets cloned as part of a library inclusion in a module/package dependency system (like `gomod`).
To render as PDF:
[source,bash,subs="attributes"]
----
git clone {src_git}
cd {git_repo}
export orig_dir="$(pwd)"
cd {pkg_me}
asciidoctor-pdf -a ROOTDIR="${orig_dir}/" -o README.pdf README.adoc
----
[id="use"]
== How do I Use SprigX?
@@ -81,10 +134,10 @@ var (
----
====
They can even be combined/used together.
They can even be combined/used together,
[%collapsible]
.Like this.
.like this.
====
[source,go,subs="attributes"]
----
@@ -125,10 +178,8 @@ If a `<template>.FuncMap` is added via `.Funcs()` *after* template parsing, it w
For example, if both `sprig` and `sprigx` provide a function `foo`:
this will use `foo` from `sprigx`
[%collapsible]
.(show)
.this will use `foo` from `sprigx`
====
[source,go,subs="attributes"]
----
@@ -157,10 +208,8 @@ var (
----
====
whereas this will use `foo` from `sprig`
[%collapsible]
.(show)
.whereas this will use `foo` from `sprig`
====
[source,go,subs="attributes"]
----
@@ -189,10 +238,10 @@ var (
----
====
and a function can even be explicitly [[override]]overridden.
and a function can even be explicitly [[override]]overridden,
[%collapsible]
.(show)
.like this.
====
This would override a function `foo` and `foo2` in `sprigx` from `foo` and `foo2` from `sprig`, but leave all other `sprig` functions untouched.
@@ -233,6 +282,7 @@ var (
[id="lib"]
== Library Functions
These are generally intended to be used *outside* the template in the actual Go code.
[id="lib_cmbfmap"]
@@ -261,7 +311,7 @@ func CombinedHtmlFuncMap(preferSprigX bool) (fmap template.FuncMap)
----
This function returns an {godoc_root}/html/template#FuncMap[`html/template.FuncMap`] function map (like <<lib_hfmap>>) combined with
{godoc_sprig}#HtmlFuncMap[`github.com/Masterminds/sprig/v3.HtmlFuncMap`^].
{godoc_sprig}#HtmlFuncMap[`{import_sprig}.HtmlFuncMap`^].
If `preferSprigx` is true, SprigX function names will override Sprig functions with the same name.
If false, Sprig functions will override conflicting SprigX functions with the same name.
@@ -275,7 +325,7 @@ func CombinedTxtFuncMap(preferSprigX bool) (fmap template.FuncMap)
----
This function returns a {godoc_root}/text/template#FuncMap[`text/template.FuncMap`] function map (like <<lib_tfmap>>) combined with
{godoc_sprig}#TxtFuncMap[`github.com/Masterminds/sprig/v3.TxtFuncMap`^].
{godoc_sprig}#TxtFuncMap[`{import_sprig}.TxtFuncMap`^].
If `preferSprigx` is true, SprigX function names will override Sprig functions with the same name.
If false, Sprig functions will override conflicting SprigX functions with the same name.
@@ -669,6 +719,132 @@ func netipxRange(from, to netip.Addr) (ipRange netipx.IPRange)
`netipxRange` directly calls {godoc_root}/go4.org/netipx#IPRangeFrom[`go4.org/netipx.IPRangeFrom`^].
[id="fn_netx"]
==== `netx`
These template functions contain capabilities from {godoc_root}/{mod_me}/netx[`{mod_me}/netx`^].
[id="fn_netx_addrrfc"]
===== `netxAddrRfc`
[source,go]
.Function Signature
----
func netxAddrRfc(addr netip.Addr) (rfcStr string)
----
`netxAddrRfc` directly calls {godoc_root}/{mod_me}/netx#AddrRfc[`{mod_me}/netx.AddrRfc`^].
[id="fn_netx_cidr4ipmask"]
===== `netxCidr4IpMask`
[source,go]
.Function Signature
----
func netxCidr4IpMask(cidr uint8) (ipMask net.IPMask, err error)
----
`netxCidr4IpMask` directly calls {godoc_root}/{mod_me}/netx#Cidr4ToIPMask[`{mod_me}/netx.Cidr4ToIPMask`^].
[id="fn_netx_cidr4mask"]
===== `netxCidr4Mask`
[source,go]
.Function Signature
----
func netxCidr4Mask(cidr uint8) (mask uint32, err error)
----
`netxCidr4IpMask` directly calls {godoc_root}/{mod_me}/netx#Cidr4ToMask[`{mod_me}/netx.Cidr4ToMask`^].
[id="fn_netx_cidr4str"]
===== `netxCidr4Str`
[source,go]
.Function Signature
----
func netxCidr4Str(cidr uint8) (maskStr string, err error)
----
`netxCidr4Str` directly calls {godoc_root}/{mod_me}/netx#Cidr4ToStr[`{mod_me}/netx.Cidr4ToStr`^].
[id="fn_netx_familyver"]
===== `netxFamilyVer`
[source,go]
.Function Signature
----
func netxFamilyVer(family uint16) (ipVer int)
----
`netxFamilyVer` directly calls {godoc_root}/{mod_me}/netx#FamilyToVer[`{mod_me}/netx.FamilyToVer`^].
[id="fn_netx_getaddrfam"]
===== `netxGetAddrFam`
[source,go]
.Function Signature
----
func netxGetAddrFam(addr netip.Addr) (family uint16)
----
`netxGetAddrFam` directly calls {godoc_root}/{mod_me}/netx#GetAddrFamily[`{mod_me}/netx.GetAddrFamily`^].
[id="fn_netx_getipfam"]
===== `netxGetIpFam`
[source,go]
.Function Signature
----
func netxGetIpFam(ip net.IP) (family uint16)
----
`netxGetIpFam` directly calls {godoc_root}/{mod_me}/netx#GetAddrFamily[`{mod_me}/netx.GetIpFamily`^].
[id="fn_netx_iprfc"]
===== `netxIpRfc`
[source,go]
.Function Signature
----
func netxIpRfc(ip net.IP) (rfcStr string)
----
`netxIpRfc` directly calls {godoc_root}/{mod_me}/netx#IpRfc[`{mod_me}/netx.IpRfc`^].
[id="fn_netx_iprfcstr"]
===== `netxIpRfcStr`
[source,go]
.Function Signature
----
func netxIpRfcStr(s string) (rfcStr string)
----
`netxIpRfcStr` directly calls {godoc_root}/{mod_me}/netx#IpRfcStr[`{mod_me}/netx.IpRfcStr`^].
[id="fn_netx_ipstriprfc"]
===== `netxIpStripRfc`
[source,go]
.Function Signature
----
func netxIpStripRfc(s string) (stripStr string)
----
`netxIpStripRfc` directly calls {godoc_root}/{mod_me}/netx#IpStripRfcStr[`{mod_me}/netx.IpStripRfcStr`^].
[id="fn_netx_ip4maskcidr"]
===== `netxIp4MaskCidr`
[source,go]
.Function Signature
----
func netxIp4MaskCidr(ipMask net.IPMask) (cidr uint8, err error)
----
`netxIp4MaskCidr` directly calls {godoc_root}/{mod_me}/netx#IPMask4ToCidr[`{mod_me}/netx.IPMask4ToCidr`^].
[id="fn_netx_ip4maskmask"]
===== `netxIp4MaskMask`
[source,go]
.Function Signature
----
func netxIp4MaskMask(ipMask net.IPMask) (mask uint32, err error)
----
`netxIp4MaskMask` directly calls {godoc_root}/{mod_me}/netx#IPMask4ToMask[`{mod_me}/netx.IPMask4ToMask`^].
// TODO
[id="fn_num"]
=== Numbers/Math

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@ import (
`github.com/shirou/gopsutil/v4/process`
`github.com/shirou/gopsutil/v4/sensors`
`go4.org/netipx`
`r00t2.io/goutils/netx`
`r00t2.io/goutils/timex`
`r00t2.io/sysutils`
)
@@ -67,6 +68,21 @@ var (
"netipxPfxLast": netipx.PrefixLastIP,
"netipxPfxRange": netipx.RangeOfPrefix,
"netipxRange": netipx.IPRangeFrom,
/*
Networking (r00t.io/goutils/netx)
*/
"netxAddrRfc": netx.AddrRfc,
"netxCidr4IpMask": netx.Cidr4ToIPMask,
"netxCidr4Mask": netx.Cidr4ToMask,
"netxCidr4Str": netx.Cidr4ToStr,
"netxFamilyVer": netx.FamilyToVer,
"netxGetAddrFam": netx.GetAddrFamily,
"netxGetIpFam": netx.GetIpFamily,
"netxIpRfc": netx.IpRfc,
"netxIpRfcStr": netx.IpRfcStr,
"netxIpStripRfc": netx.IpStripRfcStr,
"netxIp4MaskCidr": netx.IPMask4ToCidr,
"netxIp4MaskMask": netx.IPMask4ToMask,
/*
Numbers/Math
*/