Compare commits

...
4 Commits
Author SHA1 Message Date
brent saner 67a5c8a3fb v1.17.5
ADDED:
* netx/dnsx/ptr package
2026-09-11 22:12:00 -04:00
brent saner cd9b967306 v1.17.4
Fix typo
2026-09-04 04:15:33 -04:00
brent saner f002766c74 v1.17.3
ADDED:
* slicesx.First
* slicesx.FirstIndex
* slicesx.Remove
* slicesx.RemoveAll
* slicesx.RemoveReverse
2026-09-03 16:25:31 -04:00
brent saner 67bd30907e v1.17.2
ADDED:
* iox/fsx:
** FsPaths
** HasType
** IsType
2026-08-08 00:30:44 -04:00
9 changed files with 669 additions and 218 deletions
+16
View File
@@ -6,6 +6,8 @@ import (
"path/filepath"
)
// TODO: FilterFsPaths
/*
FsPaths returns all paths in [io/fs.FS] `fsys`.
@@ -82,6 +84,8 @@ func getEntries(fsys fs.FS, t fs.FileMode, inclFiles bool, maxDepth int, relPath
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)
@@ -89,12 +93,24 @@ func getEntries(fsys fs.FS, t fs.FileMode, inclFiles bool, maxDepth int, relPath
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]
}
}
+7
View File
@@ -0,0 +1,7 @@
package ptr
const (
TLD string = "arpa"
Zone4 string = "in-addr." + TLD
Zone6 string = "ip6." + TLD
)
+5
View File
@@ -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
+11
View File
@@ -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")
)
+266
View File
@@ -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
}
+3
View File
@@ -1,4 +1,7 @@
/*
Package slicesx aims to extend functionality of the stdlib [slices] module.
Most are convenience functions that aid in readability (sometimes at a VERY negligible performance hit),
but others can be quite useful (e.g. [Remove], [RemoveAll], [RemoveReverse]) that will save on significant boilerplate.
*/
package slicesx
+146 -1
View File
@@ -1,10 +1,51 @@
package slicesx
import (
"slices"
)
/*
First returns the first item/element in slice s.
e will be the nil/zero value of the slice element type
if the slice length is 0 (including if s == nil).
*/
func First[S ~[]E, E any](s S) (e E) {
if len(s) == 0 {
return
}
e = s[0]
return
}
/*
FirstIndex returns the index of the first element in slice s.
While at first glance this may seem useless, it may have some usecases as instead of causing panics:
- If s is empty, idx will be -1.
- If s is nil, idx will be -2.
- Otherwise idx will ALWAYS be 0.
*/
func FirstIndex[S ~[]E, E any](s S) (idx int) {
if s == nil {
idx = -2
} else if len(s) == 0 {
idx = -1
}
return
}
/*
Last returns the last item/element in slice s.
e will be the nil/zero value of the slice element type
if the slice length is 0.
if the slice length is 0 (including if s == nil).
*/
func Last[S ~[]E, E any](s S) (e E) {
@@ -41,3 +82,107 @@ func LastIndex[S ~[]E, E any](s S) (idx int) {
return
}
/*
Remove removes any values of value elem from slice s up to max number of elems.
If maxRemove is negative (< 0), start from the end instead.
This simply wraps [RemoveReverse]:
removed = slicesx.RemoveReverse(s, elem, uint(-maxRemove))
If maxRemove is exactly 0, remove ALL instances of elem.
If s is nil, removed will be nil as well.
Note that s will remain untouched, and removed will be a new slice.
*/
func Remove[S ~[]E, E comparable](s S, elem E, maxRemove int) (removed S) {
var idx int
if s == nil {
return
}
if len(s) == 0 {
removed = []E{}
return
}
removed = make(S, 0, len(s))
defer func() {
if removed != nil {
removed = slices.Clip(removed)
}
}()
if maxRemove < 0 {
removed = RemoveReverse(s, elem, uint(-maxRemove))
return
} else if maxRemove == 0 {
maxRemove = len(s)
}
for idx = range s {
if s[idx] == elem && maxRemove > 0 {
maxRemove--
continue
}
removed = append(removed, s[idx])
}
return
}
/*
RemoveAll is simply a convenience wrapper around [Remove], e.g.:
removed = slicesx.Remove(s, elem, 0)
*/
func RemoveAll[S ~[]E, E comparable](s S, elem E) (removed S) { return Remove(s, elem, 0) }
/*
RemoveReverse removes any values of value elem from slice s up to max number of elems in reverse order
(i.e. starting from the END of s).
If maxRemove is exactly 0, remove all instances of elem (via [RemoveAll]), i.e.:
removed = slicesx.RemoveAll(s, elem)
// or, more directly:
removed = slicesx.Remove(s, elem, 0)
If s is nil, removed will be nil as well.
Note that s will remain untouched, and removed will be a new slice.
*/
func RemoveReverse[S ~[]E, E comparable](s S, elem E, maxRemove uint) (removed S) {
var idx int
if s == nil {
return
}
if len(s) == 0 {
removed = []E{}
return
}
if maxRemove == 0 {
removed = RemoveAll(s, elem)
return
}
removed = make(S, 0, len(s))
for idx = len(s) - 1; idx >= 0; idx-- {
if s[idx] == elem && maxRemove > 0 {
maxRemove--
continue
}
removed = append(removed, s[idx])
}
removed = slices.Clip(removed)
slices.Reverse(removed)
return
}
+1 -1
View File
@@ -636,7 +636,7 @@ pre.rouge .gs {
<div class="details">
<span id="author" class="author">Brent Saner</span><br>
<span id="email" class="email"><a href="mailto:bts@square-r00t.net">bts@square-r00t.net</a></span><br>
<span id="revdate">Last rendered 2026-08-08 00:13:35 -0400</span>
<span id="revdate">Last rendered 2026-09-11 22:12:00 -0400</span>
</div>
<div id="toc" class="toc2">
<div id="toctitle">Table of Contents</div>
+214 -216
View File
File diff suppressed because it is too large Load Diff