Compare commits

..

4 Commits

Author SHA1 Message Date
brent saner cbfaaddf34 v1.17.0
ADDED:
* netx:
** docs updated
** added NOTES.adoc for more extensive info that doesn't need to be in
the library docs
** Addr4in6Compat()
** Addr4in6Mapped()
** Addr4in6Mapped()
** HasSubnet()
** IP4MapPfx4to6()
** IP4MapPfx6to4()
** IPSetCombined()
** IPSetFrom()
** IPSetFromNative()
** IsAddr4Compat()
** IsPublic()
** MustIPSetCombined()
** MustIPSetFrom()
** MustIPSetFromNative()
** Pfx4in6Compat() (untested)
** Pfx4in6Mapped() (untested)
** UnmapAddr()
** UnmapPfx()
** Better support in other functions for IPv4-Compatible/IPv4-Mapped
addressing
2026-07-29 15:05:36 -04:00
brent saner 5108b09df5 v1.16.13
ADDED:
* mapsx:
** ListKeys()
** ListValues()
2026-07-15 19:04:00 -04:00
brent saner 72af113204 v1.16.12
Added:
* slicesx:
** Last
** LastIndex
2026-07-14 03:02:22 -04:00
brent saner c9f3e7a639 v1.16.11
ADDED:
* stringsx:
** RunesInString
** RuneMapFromString
** SquashConsec
** SquashConsecRunes
** SquashConsecRunesAll
** SquashMap
** SquashWhitespace
2026-07-13 19:17:39 -04:00
23 changed files with 2409 additions and 152 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ for f in $(find . -type f -iname "README.adoc"); do
# * https://docbook.org/ # * https://docbook.org/
# * LaTex # * LaTex
# * Allows for *very* extensive domain-specific ligature/representation (very common in mathematic/scientific literature) # * 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 # * But nigh unreadable by human eyes unless you're rather familiar with it
# * Parsing/rendering support about on-par with DocBook # * Parsing/rendering support about on-par with DocBook
# * https://www.latex-project.org/ # * https://www.latex-project.org/
# </rant> # </rant>
+94
View File
@@ -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
}
+23
View File
@@ -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
}
)
+10 -8
View File
@@ -1,17 +1,18 @@
module r00t2.io/goutils module r00t2.io/goutils
go 1.25.0 go 1.26
require ( require (
github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0
github.com/coreos/go-systemd/v22 v22.7.0 github.com/coreos/go-systemd/v22 v22.7.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/deckarep/golang-set/v2 v2.9.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/olekukonko/tablewriter v1.1.4 github.com/olekukonko/tablewriter v1.1.4
github.com/shirou/gopsutil/v4 v4.26.5 github.com/shirou/gopsutil/v4 v4.26.6
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/sys v0.46.0 golang.org/x/sys v0.47.0
r00t2.io/sysutils v1.16.2 r00t2.io/sysutils v1.16.5
) )
require ( require (
@@ -22,12 +23,12 @@ require (
github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/djherbis/times v1.6.0 // indirect github.com/djherbis/times v1.6.0 // indirect
github.com/ebitengine/purego v0.10.1 // indirect github.com/ebitengine/purego v0.10.2 // indirect
github.com/fatih/color v1.19.0 // indirect github.com/fatih/color v1.19.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect
@@ -42,6 +43,7 @@ require (
github.com/tklauser/go-sysconf v0.4.0 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.12.0 // indirect github.com/tklauser/numcpus v0.12.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/crypto v0.53.0 // indirect go.mongodb.org/mongo-driver v1.17.9 // indirect
golang.org/x/sync v0.21.0 // indirect golang.org/x/crypto v0.54.0 // indirect
golang.org/x/sync v0.22.0 // indirect
) )
+18
View File
@@ -18,6 +18,8 @@ github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
@@ -26,6 +28,8 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 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 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 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/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= 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/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -51,6 +55,8 @@ github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= 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 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-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/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= 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-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 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
@@ -81,6 +87,8 @@ github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cI
github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= 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 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= 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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -97,6 +105,8 @@ github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqo
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/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 h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
@@ -105,10 +115,14 @@ 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.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= 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.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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -119,7 +133,11 @@ 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.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= 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.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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
r00t2.io/sysutils v1.16.2 h1:wI01UwZ/bXn/lzBiCpqDmzZCOWiK87kz04SB4xRw+W0= r00t2.io/sysutils v1.16.2 h1:wI01UwZ/bXn/lzBiCpqDmzZCOWiK87kz04SB4xRw+W0=
r00t2.io/sysutils v1.16.2/go.mod h1:iXK+ALOwIdRKjAJIE5USlkZ669SVDHBNNuYhunsznH8= 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=
+55 -3
View File
@@ -26,10 +26,62 @@ func GetOk[Map ~map[K]V, K comparable, V any](m Map, k K, v V) (val V, found boo
} }
/* /*
Must, unlike [Get] or [GetOk], requires that `k` be in map `m`. ListKeys returns map `m`'s keys as a slice (unlike [maps.Keys],
which returns an iterator via [iter.Seq]).
A panic with error [ErrNotFound] will be raised if `k` is not present. ListKeys will return nil if `m` is nil, and an empty slice
Otherwise the found value will be returned. if `m` is non-nil but empty.
The ordering of `mapKeys` is unpredictable as maps are unsorted
and no explicit sorting is performed.
*/
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
}
/*
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.
*/
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
}
/*
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) { func Must[Map ~map[K]V, K comparable, V any](m Map, k K) (val V) {
+206
View File
@@ -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
+45 -3
View File
@@ -1,10 +1,52 @@
package netx package netx
import ( import (
`net/netip` "net/netip"
"go4.org/netipx"
)
const (
ip4in6Legacy96 string = "::"
ip4in6Modern96 string = "::ffff:"
) )
var ( var (
ip4In6Legacy netip.Prefix = netip.MustParsePrefix("::/96") // IPv4 documentation prefixes (RFC 5737)
ip4In6Modern netip.Prefix = netip.MustParsePrefix("::ffff:0:0/96") 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,
)
) )
+1 -1
View File
@@ -3,7 +3,7 @@
package netx package netx
import ( import (
`golang.org/x/sys/unix` "golang.org/x/sys/unix"
) )
const ( const (
+1 -1
View File
@@ -3,7 +3,7 @@
package netx package netx
import ( import (
`golang.org/x/sys/windows` "golang.org/x/sys/windows"
) )
const ( const (
+124
View File
@@ -1,4 +1,128 @@
/* /*
Package netx includes extensions to the stdlib [net] module. 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 package netx
+1 -1
View File
@@ -1,7 +1,7 @@
package netx package netx
import ( import (
`errors` "errors"
) )
var ( var (
+654 -129
View File
@@ -1,15 +1,110 @@
package netx package netx
import ( import (
`math/bits` "fmt"
`net` "math/bits"
`net/netip` "net"
`strconv` "net/netip"
`strings` "strconv"
"strings"
`go4.org/netipx` "go4.org/netipx"
) )
/*
Addr4in6Compat takes IP address `addr` and, if an IPv4 address, returns it
as an "IPv4-Compatible IPv6 Address" (RFC [4291 § 2.5.5.1]).
If `addr` is already an IPv6 address and NOT in the '::/96' or '::0:0/96' prefix,
`addr` will be returned as `compatAddr`.
If `addr` is an IPv6 address and is an IPv4-Mapped prefix ('::0:0/96'),
then `compatAddr` will be an IPv4-Compatible address conversion of `addr`.
If `addr` is an IPv6 address and IS in the '::/96' prefix,
`addr` will be returned as `compatAddr`.
If `addr` is invalid, `compatAddr` will be the [netip.Addr] returned by [netip.IPv6Unspecified].
Note that this is the DEPRECATED format for IPv4-addresses-as-IPv6!
The modern, proper equivalent is "IPv4-Mapped IPv6 Address" (RFC [4291 § 2.5.5.2]),
which can be performed via [Addr4in6Mapped] instead.
- If you need an IPv6 address format for an IPv4 address and don't know
if you need this function or not, you want [Addr4in6Mapped] instead.
- If you THINK you want this function, you want [Addr4in6Mapped] instead.
- If you ABSOLUTELY KNOW you need this function, you're probably dealing with
some very old IPv6 implementations.
But you at least know you need this function.
[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
*/
func Addr4in6Compat(addr netip.Addr) (compatAddr netip.Addr) {
if !addr.IsValid() {
compatAddr = netip.IPv6Unspecified()
return
}
compatAddr = addr
if addr.Is6() {
if addr.Is4In6() {
compatAddr = Addr4in6Compat(UnmapAddr(addr))
}
return
}
if !addr.Is6() {
compatAddr = netip.MustParseAddr(fmt.Sprintf("%s%s", ip4in6Legacy96, addr.String()))
}
return
}
/*
Addr4in6Mapped takes IP address `addr` and, if an IPv4 address, returns it
as an "IPv4-Mapped IPv6 Address" (RFC [4291 § 2.5.5.2]).
You should almost assuredly be using this function instead of [Addr4in6Compat].
If `addr` is already an IPv6 address and NOT in the '::/96' or '::0:0/96' prefix,
`addr` will be returned as `mappedAddr`.
If `addr` is an IPv6 address and is an IPv4-Compatible prefix ('::/96'),
then `mappedAddr` will be an IPv4-Mapped address conversion of `addr`.
If `addr` is an IPv6 address and IS in the '::ffff:0:0/96' prefix,
`addr` will be returned as `mappedAddr`.
If `addr` is invalid, `mappedAddr` will be the [netip.Addr] returned by [netip.IPv6Unspecified].
[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
*/
func Addr4in6Mapped(addr netip.Addr) (mappedAddr netip.Addr) {
if !addr.IsValid() {
mappedAddr = netip.IPv6Unspecified()
return
}
mappedAddr = addr
if addr.Is6() {
if !IsAddr4Compat(addr) {
return
}
mappedAddr = UnmapAddr(addr)
}
if !mappedAddr.Is6() {
mappedAddr = netip.MustParseAddr(fmt.Sprintf("%s%s", ip4in6Modern96, mappedAddr.String()))
}
return
}
/* /*
AddrRfc returns an RFC-friendly string from an IP address ([net/netip.Addr]). AddrRfc returns an RFC-friendly string from an IP address ([net/netip.Addr]).
@@ -17,6 +112,15 @@ If addr is an IPv4 address, it will simply be the string representation (e.g. "2
If addr is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]"). If addr is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]").
IPv4-Compatible IPv6 addresses will be represented as e.g. "[::203.0.113.1]".
IPv4-Mapped IPv6 addresses will be represented as e.g. "[::ffff:203.0.113.1]".
Note that if addr is an IPv6 unspecifified address derived as an "IPv4-Compatible",
then `rfcStr` will always be the IPv6 unspecified address (::), NOT an IPv4-Compatible
unspecified (::0.0.0.0). Refer to the documentation for [IsAddr4Compat] as to why
this happens.
If the version can't be determined, rfcStr will be an empty string. If the version can't be determined, rfcStr will be an empty string.
*/ */
func AddrRfc(addr netip.Addr) (rfcStr string) { func AddrRfc(addr netip.Addr) (rfcStr string) {
@@ -24,7 +128,15 @@ func AddrRfc(addr netip.Addr) (rfcStr string) {
if addr.Is4() { if addr.Is4() {
rfcStr = addr.String() rfcStr = addr.String()
} else if addr.Is6() { } else if addr.Is6() {
rfcStr = "[" + addr.String() + "]" switch {
case IsAddr4Compat(addr):
rfcStr = fmt.Sprintf("%s%s", ip4in6Legacy96, UnmapAddr(addr).String())
// addr.Is4In6() addrs format properly
default:
rfcStr = addr.String()
}
rfcStr = "[" + rfcStr + "]"
} }
return return
@@ -34,11 +146,6 @@ func AddrRfc(addr netip.Addr) (rfcStr string) {
Cidr4ToIPMask takes an IPv4 CIDR/bit size/prefix length and returns the [net.IPMask]. Cidr4ToIPMask takes an IPv4 CIDR/bit size/prefix length and returns the [net.IPMask].
It's (essentially) the inverse of [net.IPMask.Size]. It's (essentially) the inverse of [net.IPMask.Size].
See also:
* [Cidr4ToMask]
* [Cidr4ToStr]
Inverse of [IPMask4ToCidr]. Inverse of [IPMask4ToCidr].
*/ */
func Cidr4ToIPMask(cidr uint8) (ipMask net.IPMask, err error) { func Cidr4ToIPMask(cidr uint8) (ipMask net.IPMask, err error) {
@@ -56,11 +163,6 @@ func Cidr4ToIPMask(cidr uint8) (ipMask net.IPMask, err error) {
/* /*
Cidr4ToMask takes an IPv4 CIDR/bit size/prefix length and returns the netmask *in bitmask form*. Cidr4ToMask takes an IPv4 CIDR/bit size/prefix length and returns the netmask *in bitmask form*.
See also:
* [Cidr4ToIPMask]
* [Cidr4ToStr]
Inverse of [Mask4ToCidr]. Inverse of [Mask4ToCidr].
*/ */
func Cidr4ToMask(cidr uint8) (mask uint32, err error) { func Cidr4ToMask(cidr uint8) (mask uint32, err error) {
@@ -80,11 +182,6 @@ func Cidr4ToMask(cidr uint8) (mask uint32, err error) {
/* /*
Cidr4ToStr is a convenience wrapper around [IPMask4ToStr]([Cidr4ToMask](cidr)). Cidr4ToStr is a convenience wrapper around [IPMask4ToStr]([Cidr4ToMask](cidr)).
See also:
* [Cidr4ToIPMask]
* [Cidr4ToMask]
Inverse of [Mask4StrToCidr]. Inverse of [Mask4StrToCidr].
*/ */
func Cidr4ToStr(cidr uint8) (maskStr string, err error) { func Cidr4ToStr(cidr uint8) (maskStr string, err error) {
@@ -128,8 +225,6 @@ func FamilyToVer(family uint16) (ipVer int) {
/* /*
GetAddrFamily returns the network family of a [net/netip.Addr]. 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). Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6).
(See [FamilyToVer] to get the associated higher-level value.) (See [FamilyToVer] to get the associated higher-level value.)
@@ -160,8 +255,6 @@ GetIpFamily returns the network family of a [net.IP].
Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6). Note that this returns [AFInet] or [AFInet6], NOT uint16(4) or uint16(6).
(See [FamilyToVer] to get the associated higher-level value.) (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, If ip is not a "valid" IP address or the version can't be determined,
family will be [golang.org/x/sys/unix.AF_UNSPEC] or [golang.org/x/sys/windows.AF_UNSPEC] depending on platform (usually 0x00/0). family will be [golang.org/x/sys/unix.AF_UNSPEC] or [golang.org/x/sys/windows.AF_UNSPEC] depending on platform (usually 0x00/0).
*/ */
@@ -180,36 +273,171 @@ func GetIpFamily(ip net.IP) (family uint16) {
} }
/* /*
IpRfc returns an RFC-friendly string from an IP address ([net.IP]). HasSubnet returns true if prefix `sub` is within prefix `parent`.
If ip is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1"). If `incl` is true, HasSubnet will return true if `sub` and `parent` are the same network.
If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]"). If `incl` is false, HasSubnet will return false if `sub` and `parent` are the same network.
This can be further determined by doing the following:
If the version can't be determined, rfcStr will be an empty string. var sub netip.Prefix
var parent netip.Prefix
See also [IpRfcStr] for providing an IP address as a string. // ...
if !netx.HasSubnet(parent, sub) && parent.Compare(sub) != 0 { // .Compare() returns 0 if the prefixes are the same.
// ...
}
// ...
It is expected that both prefixes are valid; HasSubnet will always return false if `parent` or `sub` are invalid.
HasSub will return false if `parent` and `sub` are different IP address families, but will
properly handle IPv4-Mapped/IPv4-Compatible IPv6 prefixes when compared with IPv6 prefixes.
Host bits may be set; it will not affect the comparison.
*/ */
func IpRfc(ip net.IP) (rfcStr string) { func HasSubnet(parent, sub netip.Prefix, incl bool) (hasSub bool) {
if ip.To4() != nil { // rule out unpredictable comparison
rfcStr = ip.To4().String() if !parent.IsValid() || !sub.IsValid() {
} else if ip.To16() != nil { return
rfcStr = "[" + ip.To16().String() + "]" }
// different families
if parent.Addr().Is4() != sub.Addr().Is4() {
return
}
// parent is smaller network size than sub
if parent.Bits() > sub.Bits() {
return
}
// parent does not contain network (or host) addr of sub
// (returns true even for parent's network addr, etc.)
if !parent.Contains(sub.Addr()) {
return
}
// parent and sub are same canon (masked) network
// (.Compare() compares host bits as well, hence the .Masked())
if parent.Masked().Compare(sub.Masked()) == 0 {
hasSub = incl
return
}
hasSub = true
return
}
/*
IP4MapPfx4to6 converts a native IPv4 prefix length (<= 32) to an
IPv6 prefix length for "IPv4-Compatible" (RFC [4291 § 2.5.5.1])
and "IPv4-Mapped" (RFC [4291 § 2.5.5.2]) addressing.
If `len4` is > 32, then `len6` will be equal to `len4`.
If this is not desired, ensure that you are gating calls to IP4MapPfx4to6
with [netip.Addr.Is4] == true on the prefix in question (it will return false
for IPv6 addresses, IPv4-Mapped IPv6 addresses, AND IPv4-Compatible IPv6 addresses):
var pfx netip.Prefix
// ...
ipv6BitLen = pfx.Bits()
if pfx.Addr().Is4() {
ipv6BitLen = netx.IP4MapPfx4to6(ipv6BitLen)
}
// ...
[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
*/
func IP4MapPfx4to6(len4 uint8) (len6 uint8) {
len6 = len4
if len4 <= 32 {
len6 = 96 + len4
} }
return return
} }
/* /*
IpRfcStr implements [IpRfc]/[AddrRfc] for string representations of an IP address s. IP4MapPfx6to4 converts a native IPv6 prefix length (>= 96) to an
IPv4 prefix length from "IPv4-Compatible" (RFC [4291 § 2.5.5.1])
and "IPv4-Mapped" (RFC [4291 § 2.5.5.2]) addressing.
If s is an IPv6 address already in the bracketed RFC format, If `len6` is < 96, then `len4` will be equal to `len6`.
then rfcStr will be equal to s. If this is not desired, ensure that you are gating calls to IP4MapPfx6to4
with [netip.Addr.Is4In6] == true on the prefix in question:
If s is not a string representation of an IP address, rfcStr will be empty. var pfx netip.Prefix
See [IpStripRfcStr] for the inverse (removing any brackets from s if present). // ...
ipv4BitLen = pfx.Bits()
if pfx.Addr().Is4In6() {
ipv4BitLen = IP4MapPfx6to4(ipv4BitLen)
}
if ipv4BitLen > 32 {
panic("lol oops")
}
// ...
[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
*/
func IP4MapPfx6to4(len6 uint8) (len4 uint8) {
len4 = len6
if len6 >= 96 {
len4 = len6 - 96
}
return
}
/*
IpRfc returns an RFC-friendly string from an IP address ([net.IP]).
If ip is an IPv4 address, it will simmply be the string representation (e.g. "203.0.113.1").
If ip is an IPv6 address, it will be enclosed in brackets (e.g. "[2001:db8::1]").
IPv4-Compatible IPv6 addresses will be represented as e.g. "[::203.0.113.1]".
IPv4-Mapped IPv6 addresses will be represented as e.g. "[::ffff:203.0.113.1]".
If the version can't be determined, `rfcStr` will be an empty string.
*/
func IpRfc(ip net.IP) (rfcStr string) {
var err error
var a netip.Addr
if a, err = netip.ParseAddr(ip.String()); err != nil {
return
}
rfcStr = AddrRfc(a)
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) { func IpRfcStr(s string) (rfcStr string) {
@@ -232,9 +460,9 @@ func IpRfcStr(s string) (rfcStr string) {
} }
/* /*
IpStripRfcStr returns IP address string s without any brackets. IpStripRfcStr returns IP address string `s` without any brackets.
If s is not a valid IP address, stripStr will be empty. If `s` is not a valid IP address, `stripStr` will be empty.
*/ */
func IpStripRfcStr(s string) (stripStr string) { func IpStripRfcStr(s string) (stripStr string) {
@@ -254,11 +482,6 @@ func IpStripRfcStr(s string) (stripStr string) {
/* /*
IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask]. IPMask4ToCidr returns a CIDR prefix size/bit size/bit length from a [net.IPMask].
See also:
* [IPMask4ToMask]
* [IPMask4ToStr]
Inverse of [Cidr4ToIPMask]. Inverse of [Cidr4ToIPMask].
*/ */
func IPMask4ToCidr(ipMask net.IPMask) (cidr uint8, err error) { func IPMask4ToCidr(ipMask net.IPMask) (cidr uint8, err error) {
@@ -285,11 +508,6 @@ func IPMask4ToCidr(ipMask net.IPMask) (cidr uint8, err error) {
/* /*
IPMask4ToMask returns the mask *in bitmask form* from a [net.IPMask]. IPMask4ToMask returns the mask *in bitmask form* from a [net.IPMask].
See also:
* [IPMask4ToCidr]
* [IPMask4ToStr]
Inverse of [Mask4ToIPMask]. Inverse of [Mask4ToIPMask].
*/ */
func IPMask4ToMask(ipMask net.IPMask) (mask uint32, err error) { func IPMask4ToMask(ipMask net.IPMask) (mask uint32, err error) {
@@ -310,11 +528,6 @@ func IPMask4ToMask(ipMask net.IPMask) (mask uint32, err error) {
/* /*
IPMask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a [net.IPMask]. IPMask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a [net.IPMask].
See also:
* [IPMask4ToCidr]
* [IPMask4ToMask]
Inverse of [Mask4StrToIPMask]. Inverse of [Mask4StrToIPMask].
*/ */
func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) { func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) {
@@ -339,24 +552,120 @@ func IPMask4ToStr(ipMask net.IPMask) (maskStr string, err error) {
} }
/* /*
IpVerStr provides the IP family of IP address/network string s. IPSetCombined combines multiple [go4.org/netipx.IPSet] sets into a single set.
s may be one of the following formats/syntaxes: The original `sets` will only be read and be untouched otherwise; `combined` is a new set.
* 203.0.113.0 Nil sets in `sets` will be silently skipped.
* 203.0.113.1 */
* 203.0.113.0/24 func IPSetCombined(sets []*netipx.IPSet) (combined *netipx.IPSet, err error) {
* 203.0.113.1/24
* 2001:db8::
* 2001:db8::1
* 2001:db8::/32
* 2001:db8::1/32
* [2001:db8::]
* [2001:db8::1]
var idx int
var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder)
for idx = range sets {
if sets[idx] == nil {
continue
}
ipsb.AddSet(sets[idx])
}
if combined, err = ipsb.IPSet(); err != nil {
return
}
return
}
/*
IPSetFrom skips some round-tripping and returns a [go4.org/netipx.IPSet] from a given
slice of strings.
`addrs` must be individual address strings but may be nil. See [net/netip.ParseAddr] for conventions.
`pfxs` must be prefix strings but may be nil. See [net/netip.ParsePrefix] for conventions.
`ranges` must be range strings but may be nil. See [go4.org/netipx.ParseIPRange] for conventions.
IPSetFrom will return immediately on first error, otherwise `ipset` will contain all provided addresses/prefixes/ranges.
*/
func IPSetFrom(addrs, pfxs, ranges []string) (ipset *netipx.IPSet, err error) {
var idx int
var a netip.Addr
var p netip.Prefix
var r netipx.IPRange
var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder)
for idx = range addrs {
if a, err = netip.ParseAddr(addrs[idx]); err != nil {
return
}
ipsb.Add(a)
}
for idx = range pfxs {
if p, err = netip.ParsePrefix(pfxs[idx]); err != nil {
return
}
ipsb.AddPrefix(p)
}
for idx = range ranges {
if r, err = netipx.ParseIPRange(ranges[idx]); err != nil {
return
}
ipsb.AddRange(r)
}
if ipset, err = ipsb.IPSet(); err != nil {
return
}
return
}
/*
IPSetFromNative is like [IPSetFrom] but takes native types instead of strings.
*/
func IPSetFromNative(addrs []netip.Addr, pfxs []netip.Prefix, ranges []netipx.IPRange) (ipset *netipx.IPSet, err error) {
var idx int
var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder)
for idx = range addrs {
ipsb.Add(addrs[idx])
}
for idx = range pfxs {
ipsb.AddPrefix(pfxs[idx])
}
for idx = range ranges {
ipsb.AddRange(ranges[idx])
}
if ipset, err = ipsb.IPSet(); err != nil {
return
}
return
}
/*
IpVerStr provides the IP family of IP address/network string `s`.
`s` may be one of the following formats/syntaxes:
- 203.0.113.0
- 203.0.113.1
- 203.0.113.0/24
- 203.0.113.1/24
- 2001:db8::
- 2001:db8::1
- 2001:db8::/32
- 2001:db8::1/32
- [2001:db8::]
- [2001:db8::1]
Unlike [GetAddrFamily]/[GetIpFamily], this returns a more "friendly" Unlike [GetAddrFamily]/[GetIpFamily], this returns a more "friendly"
version - if s is not valid syntax, ipVer will be int(0), 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. 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.) (See [VerToFamily] to get the associated system/lower-level value.)
*/ */
@@ -382,7 +691,47 @@ func IpVerStr(s string) (ipVer int) {
} }
/* /*
IsBracketedIp6 returns a boolean indicating if s is a valid bracket-enclosed IPv6 in string format IsAddr4Compat returns true if `addr` is in the "IPv4-Compatible IPv6 Address" prefix.
Note that if `addr` is from an IPv4-Compatible and was the unspecified address
(::0.0.0.0), isCompat will always return false by design.
This is because ::0.0.0.0 is, as a fully exploded IPv6 address:
0000:0000:0000:0000:0000:0000:0000:0000
Or, in other words,
[16]byte{
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
}
which is the *exact same* bytes as the native IPv6 "unspecified" address (::).
Because the IPv4-Compatible prefix itself is ::/96 (and no prefix information
is available from a netip.Addr), this function errs on the side of caution
and treats `addr` as the IPv6 unspecified address instead of the
IPv4-Compatible unspecified.
This "quirk" is not present in IPv4-Mapped addresses, as they use an explicit non-zero prefix.
(There are many reasons why IPv4-Compatible addressing was legacy/deprecated the moment it was
canonized in RFC; this is one of them.)
*/
func IsAddr4Compat(addr netip.Addr) (isCompat bool) {
if addr.Is6() && addr.IsUnspecified() {
return
}
isCompat = ip4in6Legacy.Contains(addr)
return
}
/*
IsBracketedIp6 returns a boolean indicating if `s` is a valid bracket-enclosed IPv6 in string format
(e.g. "[2001:db8::1]"). (e.g. "[2001:db8::1]").
It will return false for *non-bracketed* IPv6 addresses (e.g. "2001:db8::1"), IPv4 addresses, It will return false for *non-bracketed* IPv6 addresses (e.g. "2001:db8::1"), IPv4 addresses,
@@ -412,12 +761,12 @@ func IsBracketedIp6(s string) (isBrktdIp bool) {
} }
/* /*
IsIpAddr returns a boolean indicating if s is an IP address (either IPv4 or IPv6) in string format. 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: For IPv6, it will return true for both of these formats:
* 2001:db8::1 - 2001:db8::1
* [2001:db8::1] - [2001:db8::1]
[IsBracketedIp6] can be used to narrow down which form. [IsBracketedIp6] can be used to narrow down which form.
*/ */
@@ -436,7 +785,7 @@ func IsIpAddr(s string) (isIp bool) {
} }
/* /*
IsPrefixNet returns true if s is a (valid) IP address or network (either IPv4 or IPv6) in: IsPrefixNet returns true if `s` is a (valid) IP address or network (either IPv4 or IPv6) in:
<addr_or_net>/<prefix_len> <addr_or_net>/<prefix_len>
@@ -456,30 +805,36 @@ func IsPrefixNet(s string) (isNet bool) {
} }
/* /*
IsPublic returns true if ALL of the following conditions are *true* for ip: IsPublic returns true if ALL of the following conditions are *true* for `ip`:
* [net/netip.Addr.IsGlobalUnicast] - [net/netip.Addr.IsGlobalUnicast]
* [net/netip.Addr.IsValid] - [net/netip.Addr.IsValid]
* (IPv4) Is not in 192.0.2.0/24, 198.51.100.0/24, or 203.0.113.0/24 ([RFC 5737]) - (IPv4) Is not in 192.0.2.0/24, 198.51.100.0/24, or 203.0.113.0/24 ([RFC 5737])
* (IPv6) Is not in 2001:db8::/32 or 3fff::/20 ([RFC 3849], [RFC 9637]) - (IPv4) Is not in 100.64.0.0/10 ([RFC 6598])
- (IPv6) Is not in 2001:db8::/32 or 3fff::/20 ([RFC 3849], [RFC 9637])
AND ALL of the following conditions are *false* for ip: AND ALL of the following conditions are *false* for ip:
* [net/netip.Addr.IsLinkLocalMulticast] - [net/netip.Addr.IsLinkLocalMulticast]
* [net/netip.Addr.IsInterfaceLocalMulticast] - [net/netip.Addr.IsInterfaceLocalMulticast]
* [net/netip.Addr.IsLinkLocalUnicast] - [net/netip.Addr.IsLinkLocalUnicast]
* [net/netip.Addr.IsMulticast] - [net/netip.Addr.IsMulticast]
* [net/netip.Addr.IsLoopback] - [net/netip.Addr.IsLoopback]
* [net/netip.Addr.IsPrivate] - [net/netip.Addr.IsPrivate]
* [net/netip.Addr.IsUnspecified] - [net/netip.Addr.IsUnspecified]
4-in-6 addresses (::0:0/96, "IPv4-Compatible IPv6 Address" [RFC 4291 § 2.5.5.1] (Legacy/Obsolete); For 4-in-6 addresses:
::ffff:0:0/96, "IPv4-Mapped IPv6" [RFC 4291 § 2.5.5.2]) will be internally
"unwrapped" back to native IPv4 before performing conditional checks. - ::0:0/96 (RFC [4291 § 2.5.5.1], "IPv4-Compatible IPv6 Address") (Deprecated)
- ::ffff:0:0/96 (RFC [4291 § 2.5.5.2], "IPv4-Mapped IPv6")
they will be internally "unwrapped"/unmapped (via [UnmapAddr]) to native IPv4 first
before performing conditional checks.
[RFC 5737]: https://datatracker.ietf.org/doc/html/rfc5737 [RFC 5737]: https://datatracker.ietf.org/doc/html/rfc5737
[RFC 4291 § 2.5.5.1]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1 [RFC 6598]: https://datatracker.ietf.org/doc/html/rfc6598
[RFC 4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2 [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
[RFC 3849]: https://datatracker.ietf.org/doc/html/rfc3849 [RFC 3849]: https://datatracker.ietf.org/doc/html/rfc3849
[RFC 9637]: https://datatracker.ietf.org/doc/html/rfc9637 [RFC 9637]: https://datatracker.ietf.org/doc/html/rfc9637
*/ */
@@ -493,7 +848,8 @@ func IsPublic(ip netip.Addr) (isPub bool) {
if addr, err = netip.ParseAddr(ip.String()); err != nil { if addr, err = netip.ParseAddr(ip.String()); err != nil {
return return
} }
addr = addr.Unmap() // Unmap to properly determine routedness.
addr = UnmapAddr(addr)
// Short-circuit on invalid first to avoid any possible logic oddness. // Short-circuit on invalid first to avoid any possible logic oddness.
if !addr.IsValid() { if !addr.IsValid() {
@@ -509,8 +865,8 @@ func IsPublic(ip netip.Addr) (isPub bool) {
addr.IsPrivate(), addr.IsPrivate(),
addr.IsUnspecified(), addr.IsUnspecified(),
// these are in the FALSE to keep syntax pattern but it's cleaner to doc as if it's a !<cond> in TRUE. // these are in the FALSE to keep syntax pattern but it's cleaner to doc as if it's a !<cond> in TRUE.
ip4In6Legacy.Contains(ip), ipDoc.Contains(addr),
ip4In6Modern.Contains(ip), cgnat4.Contains(addr),
} { } {
if v { if v {
return return
@@ -534,11 +890,6 @@ func IsPublic(ip netip.Addr) (isPub bool) {
/* /*
Mask4ToCidr converts an IPv4 netmask *in bitmask form* to a CIDR prefix size/bit size/bit length. Mask4ToCidr converts an IPv4 netmask *in bitmask form* to a CIDR prefix size/bit size/bit length.
See also:
* [Mask4ToIPMask]
* [Mask4ToStr]
Inverse of [Cidr4ToMask]. Inverse of [Cidr4ToMask].
*/ */
func Mask4ToCidr(mask uint32) (cidr uint8, err error) { func Mask4ToCidr(mask uint32) (cidr uint8, err error) {
@@ -551,11 +902,6 @@ func Mask4ToCidr(mask uint32) (cidr uint8, err error) {
/* /*
Mask4ToIPMask returns mask *in bitmask form* as a [net.IPMask]. Mask4ToIPMask returns mask *in bitmask form* as a [net.IPMask].
See also:
* [Mask4ToCidr]
* [Mask4ToStr]
Inverse of [IPMask4ToMask]. Inverse of [IPMask4ToMask].
*/ */
func Mask4ToIPMask(mask uint32) (ipMask net.IPMask, err error) { func Mask4ToIPMask(mask uint32) (ipMask net.IPMask, err error) {
@@ -574,11 +920,6 @@ func Mask4ToIPMask(mask uint32) (ipMask net.IPMask, err error) {
/* /*
Mask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a netmask *in bitmask form*. Mask4ToStr returns a string representation of an IPv4 netmask (e.g. "255.255.255.0" for a /24) from a netmask *in bitmask form*.
See also:
* [Mask4ToCidr]
* [Mask4ToIPMask]
Inverse of [Mask4StrToMask]. Inverse of [Mask4StrToMask].
*/ */
func Mask4ToStr(mask uint32) (maskStr string, err error) { func Mask4ToStr(mask uint32) (maskStr string, err error) {
@@ -597,14 +938,9 @@ func Mask4ToStr(mask uint32) (maskStr string, err error) {
} }
/* /*
Mask4StrToCidr parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns am IPv4 CIDR/bit size/prefix length. Mask4StrToCidr parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns an IPv4 CIDR/bit size/prefix length.
See also: Inverse of [Cidr4ToStr].
* [Mask4StrToIPMask]
* [Mask4StrToMask]
Inverse of [Cidr4ToMaskStr].
*/ */
func Mask4StrToCidr(maskStr string) (cidr uint8, err error) { func Mask4StrToCidr(maskStr string) (cidr uint8, err error) {
@@ -624,11 +960,6 @@ func Mask4StrToCidr(maskStr string) (cidr uint8, err error) {
/* /*
Mask4StrToIPMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a [net.IPMask]. Mask4StrToIPMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a [net.IPMask].
See also:
* [Mask4StrToCidr]
* [Mask4StrToMask]
Inverse of [IPMask4ToStr]. Inverse of [IPMask4ToStr].
*/ */
func Mask4StrToIPMask(maskStr string) (mask net.IPMask, err error) { func Mask4StrToIPMask(maskStr string) (mask net.IPMask, err error) {
@@ -661,11 +992,6 @@ func Mask4StrToIPMask(maskStr string) (mask net.IPMask, err error) {
/* /*
Mask4StrToMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a netmask *in bitmask form*. Mask4StrToMask parses a "dotted-quad" IPv4 netmask (e.g. "255.255.255.0" for a /24) and returns a netmask *in bitmask form*.
See also:
* [Mask4StrToCidr]
* [Mask4StrToIPMask]
Inverse of [Mask4ToStr]. Inverse of [Mask4ToStr].
*/ */
func Mask4StrToMask(maskStr string) (mask uint32, err error) { func Mask4StrToMask(maskStr string) (mask uint32, err error) {
@@ -683,8 +1009,207 @@ func Mask4StrToMask(maskStr string) (mask uint32, err error) {
return return
} }
// MustIPSetCombined wraps [IPSetCombined] but will panic on error instead of returning it.
func MustIPSetCombined(sets []*netipx.IPSet) (combined *netipx.IPSet) {
var err error
if combined, err = IPSetCombined(sets); err != nil {
panic(err)
}
return
}
// MustIPSetFrom wraps [IPSetFrom] but will panic on error instead of returning it.
func MustIPSetFrom(addrs, pfxs, ranges []string) (ipset *netipx.IPSet) {
var err error
if ipset, err = IPSetFrom(addrs, pfxs, ranges); err != nil {
panic(err)
}
return
}
// MustIPSetFromNative wraps [IPSetFromNative] but will panic on error instead of returning it.
func MustIPSetFromNative(addrs []netip.Addr, pfxs []netip.Prefix, ranges []netipx.IPRange) (ipset *netipx.IPSet) {
var err error
if ipset, err = IPSetFromNative(addrs, pfxs, ranges); err != nil {
panic(err)
}
return
}
/* /*
VerToFamily takes a "human-readable" IP version ipVer (4 or 6) and returns Pfx4in6Compat takes network prefix `pfx` and, if an IPv4 network, returns it
as an "IPv4-Compatible IPv6 Address" (RFC [4291 § 2.5.5.1]) network.
If `pfx` is already an IPv6 network and NOT in the '::/96' or '::0:0/96' prefix (inclusive),
`pfx` will be returned as `compatPfx`.
If `pfx` is an IPv6 network and is an IPv4-Mapped prefix ('::0:0/96'),
then `compatPfx` will be an IPv4-Compatible network conversion of `pfx`.
If `pfx` is an IPv6 network and is in the '::/96' prefix (inclusive),
`pfx` will be returned as `compatPfx`.
If `pfx` is invalid or the zero network, `compatPfx` will be ::/0.
Note that this is the DEPRECATED format for IPv4-addresses-as-IPv6!
The modern, proper equivalent is "IPv4-Mapped IPv6 Address" (RFC [4291 § 2.5.5.2]),
which can be performed via [Pfx4in6Mapped] instead.
- If you need an IPv6 network format for an IPv4 network and don't know
if you need this function or not, you want [Pfx4in6Mapped] instead.
- If you THINK you want this function, you want [Pfx4in6Mapped] instead.
- If you ABSOLUTELY KNOW you need this function, you're probably dealing with
some very old IPv6 implementations.
But you at least know you need this function.
[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
*/
func Pfx4in6Compat(pfx netip.Prefix) (compatPfx netip.Prefix) {
var bitLen uint8
var a netip.Addr
if !a.IsValid() || !pfx.IsValid() {
compatPfx = netip.PrefixFrom(netip.IPv6Unspecified(), 0)
return
}
a = pfx.Addr()
bitLen = uint8(pfx.Bits())
compatPfx = pfx
if a.Is6() {
if a.Is4In6() {
a = Addr4in6Compat(UnmapAddr(a))
compatPfx = netip.PrefixFrom(a, int(bitLen))
}
return
}
a = netip.MustParseAddr(fmt.Sprintf("%s%s", ip4in6Legacy96, a.String()))
bitLen = IP4MapPfx4to6(bitLen)
compatPfx = netip.PrefixFrom(a, int(bitLen))
return
}
/*
Pfx4in6Mapped takes network prefix `pfx` and, if an IPv4 network, returns it
as an "IPv4-Mapped IPv6 Address" (RFC [4291 § 2.5.5.2]) network.
You should almost assuredly be using this function instead of [Pfx4in6Compat].
If `pfx` is already an IPv6 network and NOT in the '::/96' or '::0:0/96' prefix,
`pfx` will be returned as `mappedPfx`.
If `pfx` is an IPv6 network and is an IPv4-Compatible prefix ('::/96'),
then `mappedPfx` will be an IPv4-Mapped network conversion of `pfx`.
If `pfx` is an IPv6 address and is in the '::ffff:0:0/96' prefix (inclusive),
`pfx` will be returned as `mappedPfx`.
If `pfx` is invalid or the zero network, `mappedPfx` will be ::/0.
[4291 § 2.5.5.2]: https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2
*/
func Pfx4in6Mapped(pfx netip.Prefix) (mappedPfx netip.Prefix) {
var bitLen uint8
var a netip.Addr
if !a.IsValid() || !pfx.IsValid() {
mappedPfx = netip.PrefixFrom(netip.IPv6Unspecified(), 0)
return
}
a = pfx.Addr()
bitLen = uint8(pfx.Bits())
mappedPfx = pfx
if a.Is6() {
if !IsAddr4Compat(a) {
return
}
a = UnmapAddr(a)
}
a = netip.MustParseAddr(fmt.Sprintf("%s%s", ip4in6Modern96, a.String()))
bitLen = IP4MapPfx4to6(bitLen)
mappedPfx = netip.PrefixFrom(a, int(bitLen))
return
}
/*
UnmapAddr unmaps both a current-supported RFC [4291 § 2.5.5.2] format ("IPv4-Mapped")
*and* legacy RFC [4291 § 2.5.5.1] ("IPv4-Compatible") address.
The Unmap method of a [netip.Addr] only works for the former.
This function works for both.
If `ip` is not either of those formats (or a "true" IPv6 address, etc.),
then `unmapped` will be equal to `ip` (but a new/copied [net/netip.Addr]).
If `ip` is the "unspecified" address (0.0.0.0, ::) for either family, it is returned as-is.
[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
*/
func UnmapAddr(ip netip.Addr) (unmapped netip.Addr) {
if ip.IsUnspecified() {
unmapped = ip
return
}
// Do an initial .Unmap() - this should be safe regardless.
unmapped = ip.Unmap()
// If it's in the legacy prefix, it requires manual unmapping.
if ip4in6Legacy.Contains(unmapped) {
unmapped, _ = netip.AddrFromSlice(unmapped.AsSlice()[12:16])
}
return
}
/*
UnmapPfx unmaps both a current-supported RFC 4291 § 2.5.5.2 format ("IPv4-Mapped")
*and* legacy RFC 4291 § 2.5.5.1 ("IPv4-Compatible") prefix.
It's much like [UnmapAddr] in this sense, but requires some additional parsing/conversion.
[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
*/
func UnmapPfx(pfx netip.Prefix) (unmapped netip.Prefix) {
var a netip.Addr = pfx.Addr()
unmapped = pfx
if IsAddr4Compat(a) || a.Is4In6() {
a = UnmapAddr(a)
unmapped = netip.PrefixFrom(a, int(IP4MapPfx6to4(uint8(pfx.Bits()))))
}
return
}
/*
VerToFamily takes a "human-readable" IP version `ipVer` (4 or 6) and returns
a system-level constant (e.g. [AFUnspec], [AFInet], [AFInet6]). 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]. If not a known IP version (i.e. neither 4 nor 6), family will be [AFUnspec].
+5 -3
View File
@@ -1,12 +1,13 @@
package netx package netx
import ( import (
`math` "math"
`net` "net"
`net/netip` "net/netip"
"testing" "testing"
) )
/*
func TestFuncsDns(t *testing.T) { func TestFuncsDns(t *testing.T) {
var err error var err error
@@ -29,6 +30,7 @@ func TestFuncsDns(t *testing.T) {
return return
} }
*/
func TestFuncsIP(t *testing.T) { func TestFuncsIP(t *testing.T) {
+4
View File
@@ -0,0 +1,4 @@
/*
Package slicesx aims to extend functionality of the stdlib [slices] module.
*/
package slicesx
+43
View File
@@ -0,0 +1,43 @@
package slicesx
/*
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.
*/
func Last[S ~[]E, E any](s S) (e E) {
var idx int = LastIndex(s)
if idx < 0 {
return
}
e = s[idx]
return
}
/*
LastIndex returns the index of the last element in slice s.
If s is empty, idx will be -1.
If s is nil, idx will be -2.
*/
func LastIndex[S ~[]E, E any](s S) (idx int) {
var l int = len(s)
idx = -2
if s != nil {
idx++
}
if l == 0 {
return
}
idx = l - 1
return
}
+235
View File
@@ -0,0 +1,235 @@
package slicesx
import (
"testing"
)
func TestLast(t *testing.T) {
var v any
var e any
var intSl []int
var strSl []string
var boolSl []bool
var anySl []any
/*
int
*/
// nil
v = Last(intSl)
e = 0
if v != e {
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
// empty
intSl = make([]int, 0)
v = Last(intSl)
if v != e { // still expect 0
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
// populated
intSl = []int{0, 1, 2, 3}
v = Last(intSl)
e = 3
if v != e {
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
/*
string
*/
// nil
v = Last(strSl)
e = ""
if v != e {
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
// empty
strSl = make([]string, 0)
v = Last(strSl)
if v != e { // still expect ""
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
// populated
strSl = []string{"a", "b", "c"}
v = Last(strSl)
e = "c"
if v != e {
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
/*
bool
*/
// nil
v = Last(boolSl)
e = false
if v != e {
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
// empty
boolSl = make([]bool, 0)
v = Last(boolSl)
if v != e { // still expect false
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
// populated
boolSl = []bool{true, false, true, false, true}
v = Last(boolSl)
e = true
if v != e {
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
/*
any/interface{}
*/
// nil
v = Last(anySl)
e = nil
if v != e {
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
// empty
anySl = make([]any, 0)
v = Last(anySl)
if v != e { // still expect nil
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
// populated
anySl = []any{3, true, "foo", "bar"}
v = Last(anySl)
e = "bar"
if v != e {
t.Errorf("%T %#v, expected %T %#v", v, v, e, e)
}
}
/*
this test only runs in Go 1.26+;
which lets you new() on values instead of just types.
commented out until req bumps to 1.26+ in go.mod.
*/
/*
func TestLastPtr(t *testing.T) {
var v any
var i *int
var s *string
var b *bool
var ptrIntSl []*int
var ptrStrSl []*string
var ptrBoolSl []*bool
//
// int
//
// nil
v = Last(ptrIntSl)
i = nil
if v != i {
t.Errorf("%T %#v, expected %T %#v", v, v, i, i)
}
// empty
ptrIntSl = make([]*int, 0)
v = Last(ptrIntSl)
if v != i { // still expect nil
t.Errorf("%T %#v, expected %T %#v", v, v, i, i)
}
// populated
ptrIntSl = []*int{new(0), new(1), new(2), new(3)}
v = Last(ptrIntSl)
i = ptrIntSl[len(ptrIntSl)-1]
if v != i {
t.Errorf("%T %#v, expected %T %#v", v, v, i, i)
}
//
// string
//
// nil
v = Last(ptrStrSl)
if v != s {
t.Errorf("%T %#v, expected %T %#v", v, v, s, s)
}
// empty
ptrStrSl = make([]*string, 0)
v = Last(ptrStrSl)
if v != s { // still expect nil
t.Errorf("%T %#v, expected %T %#v", v, v, s, s)
}
// populated
ptrStrSl = []*string{new("a"), new("b"), new("c")}
v = Last(ptrStrSl)
s = ptrStrSl[len(ptrStrSl)-1]
if v != s {
t.Errorf("%T %#v, expected %T %#v", v, v, s, s)
}
//
// bool
//
// nil
v = Last(ptrBoolSl)
if v != b {
t.Errorf("%T %#v, expected %T %#v", v, v, b, b)
}
// empty
ptrBoolSl = make([]*bool, 0)
v = Last(ptrBoolSl)
if v != b { // still expect nil
t.Errorf("%T %#v, expected %T %#v", v, v, b, b)
}
// populated
ptrBoolSl = []*bool{new(true), new(false), new(true), new(false), new(true)}
v = Last(ptrBoolSl)
b = ptrBoolSl[len(ptrBoolSl)-1]
if v != b {
t.Errorf("%T %#v, expected %T %#v", v, v, b, b)
}
}
*/
func TestLastIndex(t *testing.T) {
var e int
var v int
var s []string
e = -2
v = LastIndex(s)
if e != v {
t.Errorf("Got %d, expected %d", v, e)
}
e = -1
s = make([]string, 0)
v = LastIndex(s)
if e != v {
t.Errorf("Got %d, expected %d", v, e)
}
e = 0
s = []string{"a"}
v = LastIndex(s)
if e != v {
t.Errorf("Got %d, expected %d", v, e)
}
e = 1
s = []string{"a", "b"}
v = LastIndex(s)
if e != v {
t.Errorf("Got %d, expected %d", v, e)
}
e = 2
s = []string{"a", "b", "c"}
v = LastIndex(s)
if e != v {
t.Errorf("Got %d, expected %d", v, e)
}
}
+457
View File
@@ -8,6 +8,7 @@ import (
"slices" "slices"
"strings" "strings"
"unicode" "unicode"
`unicode/utf8`
) )
/* /*
@@ -498,6 +499,462 @@ func Reverse(s string) (revS string) {
return return
} }
/*
RunesInString returns an ordered slice of all unique runes in
UTF-8 string `s`.
If `s` is an empty string, runes will be a non-nil empty slice.
*/
func RunesInString(s string) (runes []rune) {
var r rune
var idx int
var runeMap map[rune]uint64 = RuneMapFromString(s)
runes = make([]rune, len(runeMap))
idx = 0
for r, _ = range runeMap {
runes[idx] = r
idx++
}
slices.Sort(runes)
return
}
/*
RuneMapFromString returns a map of unique runes found in
UTF-8 string `s` along with a count of their occurrence.
If `s` is an empty string, runes will be a non-nil empty map.
Non-UTF-8 runes are skipped.
*/
func RuneMapFromString(s string) (runes map[rune]uint64) {
var r rune
var runeSz int
runes = make(map[rune]uint64)
for idx := 0; idx < len(s); {
r, runeSz = utf8.DecodeRuneInString(s[idx:])
if r == utf8.RuneError && runeSz == 1 {
idx += runeSz
continue
}
runes[r]++
idx += runeSz
}
return
}
/*
SquashConsec squashes/collapses consecutive instances of sequences
seq to a single instance.
It is expected that s is UTF-8 or a compatible subset (e.g. ASCII).
If seq is nil/empty, any n+1 consecutive instance of *any rune*
will be squashed to a single instance. This is *much* more performant
(as it simply wraps [SquashConsecRunesAll]) at the cost of lack of scoping.
If you are trying to squash/collapse whitespace instead, [SquashWhitespace]
may be more apropos.
If you want more fine-grained control over sequence replacement, see [SquashMap].
If s is an empty string, SquashConsec will return `s` as-is.
*/
func SquashConsec(s string, seq ...string) (squashed string) {
var idx int
var sIdx int
var kLen int
var start int
var seqIdx int
var nextNum int
var nextPos int
var sb *strings.Builder
squashed = s
if len(s) == 0 {
return
}
if len(seq) == 0 {
squashed = SquashConsecRunesAll(s)
return
}
for seqIdx = range seq {
if seq[seqIdx] == "" {
continue
}
sb = new(strings.Builder)
sb.Grow(len(squashed))
kLen = len(seq[seqIdx])
for idx = 0; idx < len(squashed); {
sIdx = strings.Index(squashed[idx:], seq[seqIdx])
if sIdx < 0 {
sb.WriteString(squashed[idx:])
break
}
start = idx + sIdx
sb.WriteString(squashed[idx:start])
nextNum = 0
nextPos = start
for strings.HasPrefix(squashed[nextPos:], seq[seqIdx]) {
nextNum++
nextPos += kLen
}
if nextNum >= 1 {
sb.WriteString(seq[seqIdx])
}
idx = nextPos
}
squashed = sb.String()
}
return
}
/*
SquashConsecRunes squashes/collapses consecutive instances of sequences
seq with a single instance.
It is expected that s is UTF-8 or a compatible subset (e.g. ASCII).
If seq is nil/empty, any n+1 consecutive instance of *any rune*
will be squashed to a single instance. This is *much* more performant
(as it simply wraps [SquashConsecRunesAll]) at the cost of lack of scoping.
If s is an empty string, SquashConsecRunes will return `s` as-is.
*/
func SquashConsecRunes(s string, seq ...rune) (squashed string) {
var r rune
var idx int
var prev rune
var seqIdx int
var runeSz int
var sb *strings.Builder
squashed = s
if len(s) == 0 {
return
}
if len(seq) == 0 {
squashed = SquashConsecRunesAll(s)
return
}
for seqIdx = range seq {
if seq[seqIdx] < 0 {
continue
}
sb = new(strings.Builder)
sb.Grow(len(squashed))
prev = -1
for idx = 0; idx < len(squashed); {
r, runeSz = utf8.DecodeRuneInString(squashed[idx:])
if r == utf8.RuneError && runeSz == 1 {
prev = -1
sb.WriteString(squashed[idx : idx+runeSz])
} else {
if r != seq[seqIdx] || prev != r {
prev = r
sb.WriteRune(r)
}
}
idx += runeSz
}
squashed = sb.String()
}
return
}
/*
SquashConsecRunesAll squashes/condenses/collapses all consecutive occurrences
of every rune in `s` to a single instance.
It is expected that s is UTF-8 or a compatible subset (e.g. ASCII);
non-UTF-8 runes will be written as-is to squashed (even if duplicate).
That is to say, for an `s` of:
* `fo`
* `foo`
* `fooo`
* `foooo`
SquashConsecRunesAll would return `fo` for all of them.
SquashConsecRunesAll will be a no-op and return an empty string
if `s` is an empty string.
*/
func SquashConsecRunesAll(s string) (squashed string) {
var r rune
var runeSz int
var prev rune = -1
var sb *strings.Builder
squashed = s
if len(s) == 0 {
return
}
sb = new(strings.Builder)
sb.Grow(len(s))
for idx := 0; idx < len(squashed); {
r, runeSz = utf8.DecodeRuneInString(squashed[idx:])
if r == utf8.RuneError && runeSz == 1 {
prev = -1
sb.WriteString(squashed[idx : idx+runeSz])
} else {
if prev != r {
prev = r
sb.WriteRune(r)
}
}
idx += runeSz
}
squashed = sb.String()
return
}
/*
SquashMap offers replacement of consecutive string series.
It is expected that s is UTF-8 or a compatible subset (e.g. ASCII).
If single is set to true, *single/standalone* instances of a sequence
in replaceMap keys will also be replaced with its corresponding value.
Be aware that this function is not ideal for very large replaceMap maps
or for very large strings, as s will be passed over for each key in replaceMap.
If dealing with very large strings or a very large explicit set of replacements,
you may want to directly use [strings.Map] or [strings.NewReplacer] (if multi-rune
replacement is needed) with your own logic.
Matching string replacements are done longest-to-shortest sequentially.
This means that given the string `fooo` (and assuming single==true),
and a replaceMap of:
map[string]string{
"o": "a",
"oo": "b",
}
the result will be "fba", not:
* "faaa"
* "fab"
Thus you may want to execute several calls if this is undesired.
Keys that are the same length are ordered alphabetically/alphanumerically
so this function is deterministic.
Empty keys in replaceMap (but NOT empty values) are skipped.
Keys in replaceMap that are not UTF-8 are skipped.
Note that this function offers high-granularity control over replacements
at the cost of complex setup.
If you want more simple squashing/collapsing of whitespace, [SquashWhitespace]
may be more apropos.
If you want more simple squashing/collapsing of non-whitespace, [SquashConsec]
may be more apropos.
If replaceMap is nil or empty, s will be returned as-is.
If s is an empty string, SquashMap will no-op.
*/
func SquashMap(s string, replaceMap map[string]string, single bool) (squashed string) {
var idx int
var sIdx int
var k string
var v string
var start int
var nextNum int
var nextPos int
var sb *strings.Builder
type kLen struct {
// k is the Key name.
k string
// r is the replacement string.
r string
// l is the length of k (in UTF-8 runes).
l int
// bl is the length of k (in bytes).
bl int
}
var kl kLen
var keysByLen []kLen
squashed = s
if len(replaceMap) == 0 || len(s) == 0 {
return
}
keysByLen = make([]kLen, 0, len(replaceMap))
for k, v = range replaceMap {
if k == "" {
continue
}
if !utf8.ValidString(k) {
continue
}
keysByLen = append(
keysByLen,
kLen{
k: k,
r: v,
l: utf8.RuneCountInString(k),
bl: len(k),
},
)
}
slices.SortFunc(
keysByLen,
func(a, b kLen) (cmp int) {
cmp = 0
// Sort so that the longer comes first.
if a.l > b.l {
cmp = -1
} else if a.l < b.l {
cmp = 1
} else if a.l == b.l {
if a.k < b.k {
cmp = -1
} else if a.k > b.k {
cmp = 1
}
}
return
},
)
for _, kl = range keysByLen {
switch strings.Count(squashed, kl.k) {
case 0:
continue
case 1:
if !single {
continue
}
squashed = strings.ReplaceAll(squashed, kl.k, kl.r)
default:
sb = new(strings.Builder)
sb.Grow(len(squashed))
for idx = 0; idx < len(squashed); {
if sIdx = strings.Index(squashed[idx:], kl.k); sIdx < 0 {
sb.WriteString(squashed[idx:])
break
}
start = idx + sIdx
sb.WriteString(squashed[idx:start])
nextNum = 0
nextPos = start
for strings.HasPrefix(squashed[nextPos:], kl.k) {
nextNum++
nextPos += kl.bl
}
if nextNum > 1 || (nextNum == 1 && single) {
sb.WriteString(kl.r)
} else {
sb.WriteString(kl.k)
}
idx = nextPos
}
squashed = sb.String()
}
}
return
}
/*
SquashWhitespace is used to collapse/squash consecutive whitespace.
It is expected that s is unicode or a compatible subset (e.g. ASCII).
If single is set to true, *single/standalone* instances of whitespace
will also be replaced with ws.
ws is a string that should replace *consecutive* whitespace.
ws does not necessarily have to be a whitespace string, and may be empty.
If you are trying to squash/collapse non-whitespace, [SquashConsec]
is more apropos.
If you want more fine-grained control over sequence replacement, see [SquashMap].
*/
func SquashWhitespace(s, ws string, single bool) (squashed string) {
var r rune
var runeSz int
var wsStart int
var invalid bool
var prevWs uint64
var sb *strings.Builder
squashed = s
if s == "" {
return
}
sb = new(strings.Builder)
sb.Grow(len(s))
for rIdx := 0; rIdx < len(s); {
r, runeSz = utf8.DecodeRuneInString(s[rIdx:])
invalid = r == utf8.RuneError && runeSz == 1
if invalid || !unicode.IsSpace(r) {
if prevWs > 1 || (prevWs == 1 && single) {
sb.WriteString(ws)
} else if prevWs == 1 {
sb.WriteString(s[wsStart:rIdx])
}
prevWs = 0
if invalid {
// Invalid UTF-8 rune; write byte as-is and advance index.
sb.WriteString(s[rIdx : rIdx+runeSz])
} else {
sb.WriteRune(r)
}
rIdx += runeSz
continue
}
if prevWs == 0 {
wsStart = rIdx
}
prevWs++
rIdx += runeSz
}
if prevWs > 1 || (prevWs == 1 && single) {
sb.WriteString(ws)
} else if prevWs == 1 {
sb.WriteString(s[wsStart:])
}
squashed = sb.String()
return
}
/* /*
StripWhitespace removes all leading, trailing, and INNER whitespace StripWhitespace removes all leading, trailing, and INNER whitespace
from unicode string `s`. from unicode string `s`.
+1 -1
View File
@@ -636,7 +636,7 @@ pre.rouge .gs {
<div class="details"> <div class="details">
<span id="author" class="author">Brent Saner</span><br> <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="email" class="email"><a href="mailto:bts@square-r00t.net">bts@square-r00t.net</a></span><br>
<span id="revdate">Last rendered 2026-07-06 16:21:08 -0400</span> <span id="revdate">Last rendered 2026-07-29 15:05:41 -0400</span>
</div> </div>
<div id="toc" class="toc2"> <div id="toc" class="toc2">
<div id="toctitle">Table of Contents</div> <div id="toctitle">Table of Contents</div>
+1 -1
View File
@@ -6,7 +6,7 @@
<span id="author" class="author">Brent Saner</span> <span id="author" class="author">Brent Saner</span>
<span id="email" class="email"><bts@square-r00t.net></span> <span id="email" class="email"><bts@square-r00t.net></span>
<span id="revdate">Last rendered 2026-07-06 16:21:13 -0400</span> <span id="revdate">Last rendered 2026-07-29 15:05:45 -0400</span>
</div> </div>