Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
cbfaaddf34
|
|||
|
5108b09df5
|
|||
|
72af113204
|
|||
|
c9f3e7a639
|
|||
|
42c0e48081
|
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/deckarep/golang-set/v2"
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewEmbedFsMember returns an embedFsMember from embedFs.
|
||||||
|
func NewEmbedFsMember(embedFs *embed.FS) (m *embedFsMember) {
|
||||||
|
|
||||||
|
m = &embedFsMember{
|
||||||
|
FS: embedFs,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewMergedFS returns a MergedFS.
|
||||||
|
|
||||||
|
The last filesystem in a MergedFS is always embedTplFS.
|
||||||
|
If fsPaths is empty, mfs will only contain embedTplFS.
|
||||||
|
|
||||||
|
Nonexisting paths in fsPaths will be skipped (and added to the noExist slice).
|
||||||
|
|
||||||
|
Order will be retained but duplicates will be skipped and added to the dupes slice.
|
||||||
|
|
||||||
|
noExist and dupes will be nil if no non-existing or duplicate paths are encountereed.
|
||||||
|
*/
|
||||||
|
func NewMergedFS(fsPaths ...string) (mfs *MergedFS, noExist, dupes []string, err error) {
|
||||||
|
|
||||||
|
var m MergedFS
|
||||||
|
var exists bool
|
||||||
|
var fsPath string
|
||||||
|
var fsMem mergedFsMember
|
||||||
|
|
||||||
|
m = MergedFS{
|
||||||
|
fses: make([]mergedFsMember, 0, len(fsPaths)+1),
|
||||||
|
overridePaths: make([]string, 0, len(fsPaths)+1),
|
||||||
|
uniq: mapset.NewSet[string](),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fsPath = range fsPaths {
|
||||||
|
if exists, err = paths.RealPathExists(&fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
noExist = append(noExist, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !m.uniq.Add(fsPath) {
|
||||||
|
dupes = append(dupes, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fsMem, err = NewRealFsMember(fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.fses = append(m.fses, fsMem)
|
||||||
|
m.overridePaths = append(m.overridePaths, fsPath)
|
||||||
|
}
|
||||||
|
// embedded fs last
|
||||||
|
m.fses = append(m.fses, NewEmbedFsMember(&embedTplFS))
|
||||||
|
m.overridePaths = append(m.overridePaths, "")
|
||||||
|
|
||||||
|
m.fses = slices.Clip(m.fses)
|
||||||
|
m.overridePaths = slices.Clip(m.overridePaths)
|
||||||
|
|
||||||
|
mfs = &m
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRealFsMember returns a realFsMember (which properly conforms to mergedFsMember) from a real filesystem path.
|
||||||
|
func NewRealFsMember(fsPath string) (fsm *realFsMember, err error) {
|
||||||
|
|
||||||
|
var fsp string
|
||||||
|
var m realFsMember
|
||||||
|
|
||||||
|
if err = paths.RealPath(&fsp); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Root, err = os.OpenRoot(fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fsm = &m
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Close is a no-op, but used to conform to mergedFsMember.
|
||||||
|
func (e *embedFsMember) Close() (err error) { return }
|
||||||
|
|
||||||
|
// Stat wraps fs.Stat on the embeded filesystem.
|
||||||
|
func (e *embedFsMember) Stat(fnm string) (stat fs.FileInfo, err error) {
|
||||||
|
|
||||||
|
if stat, err = fs.Stat(e.FS, fnm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Append adds fsPaths as fs.FS to the MergedFS (before the embed.FS).
|
||||||
|
|
||||||
|
If fsPaths is empty, Append is a NO-OP.
|
||||||
|
|
||||||
|
noExist and dupes have the same meaning as in NewMergedFS.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) Append(fsPaths ...string) (noExist, dupes []string, err error) {
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
var fsPath string
|
||||||
|
var newPaths []string
|
||||||
|
var fsMem *realFsMember
|
||||||
|
var newFs []mergedFsMember
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
newPaths = make([]string, 0, len(fsPaths))
|
||||||
|
newFs = make([]mergedFsMember, 0, len(fsPaths))
|
||||||
|
|
||||||
|
for _, fsPath = range fsPaths {
|
||||||
|
if exists, err = paths.RealPathExists(&fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
noExist = append(noExist, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fsMem, err = NewRealFsMember(fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !m.uniq.Add(fsPath) {
|
||||||
|
dupes = append(dupes, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newFs = append(newFs, fsMem)
|
||||||
|
newPaths = append(newPaths, fsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newFs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.overridePaths = append(m.overridePaths, newPaths...)
|
||||||
|
m.fses = append(m.fses[:len(m.fses)-1], append(newFs, m.fses[len(m.fses)-1])...)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all realFsMember members. This MergedFS will not be able to be used after this is called.
|
||||||
|
func (m *MergedFS) Close() (err error) {
|
||||||
|
|
||||||
|
var fsMem mergedFsMember
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
for _, fsMem = range m.fses {
|
||||||
|
if err = fsMem.(io.Closer).Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Insert adds fsPaths as fs.FS to a MergedFS starting at index idx.
|
||||||
|
|
||||||
|
If fsPaths is empty, Insert is a NO-OP.
|
||||||
|
|
||||||
|
If idx is 0 or negative, they will be prepended.
|
||||||
|
If idx is larger than the last index of real directories (excluding the last embed.FS),
|
||||||
|
it will be reduced to the highest valid index.
|
||||||
|
|
||||||
|
noExist and dupes have the same meaning as in NewMergedFS.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) Insert(idx int, fsPaths ...string) (noExist, dupes []string, err error) {
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
var fsPath string
|
||||||
|
var newPaths []string
|
||||||
|
var fsMem *realFsMember
|
||||||
|
var newFs []mergedFsMember
|
||||||
|
|
||||||
|
if len(fsPaths) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
newPaths = make([]string, 0, len(fsPaths))
|
||||||
|
newFs = make([]mergedFsMember, 0, len(fsPaths))
|
||||||
|
|
||||||
|
for _, fsPath = range fsPaths {
|
||||||
|
if exists, err = paths.RealPathExists(&fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
noExist = append(noExist, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fsMem, err = NewRealFsMember(fsPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !m.uniq.Add(fsPath) {
|
||||||
|
dupes = append(dupes, fsPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newPaths = append(newPaths, fsPath)
|
||||||
|
newFs = append(newFs, fsMem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newFs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx < 0 {
|
||||||
|
m.fses = append(newFs, m.fses...)
|
||||||
|
m.overridePaths = append(newPaths, m.overridePaths...)
|
||||||
|
return
|
||||||
|
} else if idx >= len(m.fses)-1 {
|
||||||
|
idx = len(m.fses) - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
m.fses = append(m.fses[:idx], append(newFs, m.fses[idx:]...)...)
|
||||||
|
m.overridePaths = append(m.overridePaths[:idx], append(newPaths, m.overridePaths[idx:]...)...)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens filename fnm and returns it as file handler fh.
|
||||||
|
func (m *MergedFS) Open(fnm string) (fh fs.File, err error) {
|
||||||
|
|
||||||
|
var fsref fs.FS
|
||||||
|
|
||||||
|
m.mut.RLock()
|
||||||
|
defer m.mut.RUnlock()
|
||||||
|
|
||||||
|
// Iterate until a match is found.
|
||||||
|
for _, fsref = range m.fses {
|
||||||
|
if fh, err = fsref.Open(fnm); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match found.
|
||||||
|
err = &fs.PathError{
|
||||||
|
Op: "Open",
|
||||||
|
Path: fnm,
|
||||||
|
Err: fs.ErrNotExist,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Paths returns a slice of real filesystem paths that are used by this MergedFS.
|
||||||
|
|
||||||
|
If sorted is true, they will be returned as a sorted slice -
|
||||||
|
otherwise they will be returned in the order they are checked.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) Paths(sorted bool) (fsPaths []string) {
|
||||||
|
|
||||||
|
m.mut.RLock()
|
||||||
|
defer m.mut.RUnlock()
|
||||||
|
|
||||||
|
if sorted {
|
||||||
|
fsPaths = m.uniq.ToSlice()
|
||||||
|
slices.Sort(fsPaths)
|
||||||
|
} else {
|
||||||
|
fsPaths = make([]string, len(m.overridePaths)-1)
|
||||||
|
copy(fsPaths, m.overridePaths[:len(m.overridePaths)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ReadDir returns a listing of path matcher fpath.
|
||||||
|
|
||||||
|
The result is a combination of all filesystems; the first encountered matching
|
||||||
|
name is added.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) ReadDir(fpath string) (entries []fs.DirEntry, err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var e fs.DirEntry
|
||||||
|
var tmpEntries []fs.DirEntry
|
||||||
|
var seen map[string]bool = make(map[string]bool)
|
||||||
|
|
||||||
|
m.mut.RLock()
|
||||||
|
defer m.mut.RUnlock()
|
||||||
|
|
||||||
|
// merge real and embedded, preferring first-seen real paths
|
||||||
|
for idx, _ = range m.fses {
|
||||||
|
if m.overridePaths[idx] == "" {
|
||||||
|
if tmpEntries, err = m.fses[idx].ReadDir(filepath.FromSlash(fpath)); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if tmpEntries, err = os.ReadDir(filepath.Join(m.overridePaths[idx], filepath.FromSlash(fpath))); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e = range tmpEntries {
|
||||||
|
if seen[e.Name()] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[e.Name()] = true
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ReadFile returns the bytes for the first found fpath.
|
||||||
|
|
||||||
|
It wraps MergedFS.Open.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) ReadFile(fpath string) (b []byte, err error) {
|
||||||
|
|
||||||
|
var fh fs.File
|
||||||
|
|
||||||
|
if fh, err = m.Open(fpath); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if fh != nil {
|
||||||
|
if err = fh.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if b, err = io.ReadAll(fh); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Stat returns the fs.FileInfo for the first found fpath.
|
||||||
|
|
||||||
|
It wraps MergedFS.Open.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) Stat(fpath string) (stat fs.FileInfo, err error) {
|
||||||
|
|
||||||
|
var fh fs.File
|
||||||
|
|
||||||
|
if fh, err = m.Open(fpath); nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if fh != nil {
|
||||||
|
if err = fh.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if stat, err = fh.Stat(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
RemoveFsByIdx removes the filesystem found at index idx.
|
||||||
|
|
||||||
|
An attempt to remove the final embed.FS filesystem will result in an ErrFsIdxOOB.
|
||||||
|
|
||||||
|
If idx is larger than the last index of "real" filesystems, an ErrFsIdxOOB will be returned.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) RemoveFsByIdx(idx uint) (err error) {
|
||||||
|
|
||||||
|
var fsPath string
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
if idx+1 == uint(len(m.fses)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx > uint(len(m.overridePaths)-1) {
|
||||||
|
err = ErrFsIdxOOB
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fsPath = m.overridePaths[idx]
|
||||||
|
if m.uniq.Contains(fsPath) {
|
||||||
|
m.uniq.Remove(fsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.fses = append(m.fses[:idx], m.fses[idx+1:]...)
|
||||||
|
m.overridePaths = append(m.overridePaths[:idx], m.overridePaths[idx+1:]...)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
RemoveFsByPath removes the fileststem with path dirPath.
|
||||||
|
|
||||||
|
If dirpath is empty, this function NOOPs.
|
||||||
|
|
||||||
|
If dirpath does not exist in this MergedFS, an ErrFsPathNoExists will be returned.
|
||||||
|
*/
|
||||||
|
func (m *MergedFS) RemoveFsByPath(dirPath string) (err error) {
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
var fsp string
|
||||||
|
var p string = dirPath
|
||||||
|
|
||||||
|
if p == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = paths.RealPath(&p); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mut.Lock()
|
||||||
|
defer m.mut.Unlock()
|
||||||
|
|
||||||
|
if !m.uniq.Contains(p) {
|
||||||
|
err = ErrFsPathNoExists
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.uniq.Remove(p)
|
||||||
|
|
||||||
|
for idx, fsp = range m.overridePaths {
|
||||||
|
if fsp == p {
|
||||||
|
m.fses = append(m.fses[:idx], m.fses[idx+1:]...)
|
||||||
|
m.overridePaths = append(m.overridePaths[:idx], m.overridePaths[idx+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ErrFsIdxOOB
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open opens file fnm.
|
||||||
|
func (r *realFsMember) Open(fnm string) (fh fs.File, err error) {
|
||||||
|
|
||||||
|
if fh, err = r.Root.Open(fnm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir returns directory entries at fpath joined with the realFsMember's root.
|
||||||
|
func (r *realFsMember) ReadDir(fpath string) (entries []fs.DirEntry, err error) {
|
||||||
|
|
||||||
|
var rel string
|
||||||
|
var fsp string = fpath
|
||||||
|
|
||||||
|
if !filepath.IsAbs(fsp) {
|
||||||
|
fsp = filepath.Join(
|
||||||
|
r.Root.Name(),
|
||||||
|
filepath.FromSlash(fsp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if err = paths.RealPath(&fsp); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rel, err = filepath.Rel(r.Root.Name(), fsp); err != nil {
|
||||||
|
// Not relative to this root, so skip.
|
||||||
|
err = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(rel, "..") {
|
||||||
|
// Absolute but not relative to this root, so skip.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if entries, err = os.ReadDir(fsp); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type (
|
||||||
|
mergedFsMember interface {
|
||||||
|
fs.ReadDirFS
|
||||||
|
fs.ReadFileFS
|
||||||
|
fs.StatFS
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
realFsMember struct {
|
||||||
|
*os.Root
|
||||||
|
}
|
||||||
|
embedFsMember struct {
|
||||||
|
*embed.FS
|
||||||
|
}
|
||||||
|
MergedFS struct {
|
||||||
|
// always appended with embedTplFs
|
||||||
|
fses []mergedFsMember
|
||||||
|
overridePaths []string
|
||||||
|
uniq mapset.Set[string]
|
||||||
|
mut sync.RWMutex
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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,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=
|
||||||
|
|||||||
@@ -25,6 +25,58 @@ func GetOk[Map ~map[K]V, K comparable, V any](m Map, k K, v V) (val V, found boo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ListKeys returns map `m`'s keys as a slice (unlike [maps.Keys],
|
||||||
|
which returns an iterator via [iter.Seq]).
|
||||||
|
|
||||||
|
ListKeys will return nil if `m` is nil, and an empty slice
|
||||||
|
if `m` is non-nil but empty.
|
||||||
|
|
||||||
|
The ordering of `mapKeys` is unpredictable as maps are unsorted
|
||||||
|
and no explicit sorting is performed.
|
||||||
|
*/
|
||||||
|
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`.
|
Must, unlike [Get] or [GetOk], requires that `k` be in map `m`.
|
||||||
|
|
||||||
|
|||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
= `netx` Notes
|
||||||
|
Brent Saner <bts@square-r00t.net>
|
||||||
|
Last rendered {localdatetime}
|
||||||
|
:doctype: book
|
||||||
|
:docinfo: shared
|
||||||
|
:data-uri:
|
||||||
|
:imagesdir: images
|
||||||
|
:sectlinks:
|
||||||
|
:sectnums:
|
||||||
|
:sectnumlevels: 7
|
||||||
|
:toc: preamble
|
||||||
|
:toc2: left
|
||||||
|
:idprefix:
|
||||||
|
:toclevels: 7
|
||||||
|
:source-highlighter: rouge
|
||||||
|
:docinfo: shared
|
||||||
|
|
||||||
|
[id="4in6"]
|
||||||
|
== IPv4 Addresses as IPv6
|
||||||
|
There are, effectively, two RFC-specified ways of representing an IPv4 address in IPv6 notation/address space/context.
|
||||||
|
|
||||||
|
Per https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5[RFC 4291 § 2.5.5^]:
|
||||||
|
|
||||||
|
* _IPv4-Compatible IPv6 Address_ (https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.1[RFC 4291 § 2.5.5.1^]) -- `::/96`
|
||||||
|
** e.g. `::192.0.2.10`
|
||||||
|
* _IPv4-Mapped IPv6 Address_ (https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2[RFC 4291 § 2.5.5.2^]) -- `::ffff:0:0/96`
|
||||||
|
** e.g. `::ffff:192.0.2.10`
|
||||||
|
|
||||||
|
The former is considered deprecated, whereas the latter is considered the modern proper representation.
|
||||||
|
Some naïve or legacy software, however, still use the former.
|
||||||
|
|
||||||
|
An IPv6 address is *128 bits* (hence a prefix length/mask of `/128` representing a complete address), or *16 bytes* (`128 / 8`).
|
||||||
|
|
||||||
|
An IPv4 address is *32 bits* (hence a prefix length/mask of `/32` representing a complete address), or *4 bytes* (`32 / 8`).
|
||||||
|
|
||||||
|
This is why both the "IPv4-compatible" and "IPv4-mapped" IPv6 addresses are within a reserved prefix length of 96 bits (`/96`) - it leaves a remaining
|
||||||
|
*32 bits* (`128 - 96`) for a complete IPv4 address.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
The IPv4-Compatible and IPv4-Mapped prefixes *are not* intended for routing!
|
||||||
|
|
||||||
|
They are used for *syntactic/contextual* use.
|
||||||
|
|
||||||
|
For routing/actual IPv4 <=> IPv6 translation, see https://datatracker.ietf.org/doc/html/rfc6052[RFC 6052^] and https://datatracker.ietf.org/doc/html/rfc8215[RFC 8215^].
|
||||||
|
|
||||||
|
This library may add supporting functions for these _translation_ prefixes at a later time.
|
||||||
|
====
|
||||||
|
|
||||||
|
[id="4in6_addr"]
|
||||||
|
=== Addresses
|
||||||
|
While https://pkg.go.dev/net/netip#ParseAddr[`net/netip.ParseAddr`^] (or https://pkg.go.dev/net/netip#MustParseAddr[`net/netip.MustParseAddr`^]) will handle
|
||||||
|
*parsing* the "IPv4-Compatible" format just fine, it will treat it *as* an IPv6 address -- not an "IPv6-wrapped/IPv6-mapped IPv4" address.
|
||||||
|
|
||||||
|
[%collapsible]
|
||||||
|
.For example...
|
||||||
|
====
|
||||||
|
[source,go]
|
||||||
|
----
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Printf(
|
||||||
|
"::192.0.2.10 -> %s\n",
|
||||||
|
netip.MustParseAddr("::192.0.2.10").String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
will print:
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
::192.0.2.10 -> ::c000:20a
|
||||||
|
----
|
||||||
|
====
|
||||||
|
|
||||||
|
For addresses, this is an easy enough fix:
|
||||||
|
|
||||||
|
. Take the IPv6 as a byte slice, `b` (`b = ip4in6.AsSlice()`)
|
||||||
|
. Slice out the last 4 bytes as `b2` (`b2 = b[12:16]`)
|
||||||
|
. Parse `b2` as an IPv4 address (`ip4 = netip.FromSlice(b2)`)
|
||||||
|
|
||||||
|
For the visual learners, the original address (`::192.0.2.10` or, more properly, `::c000:20a`) can be represented in multiple ways.
|
||||||
|
|
||||||
|
[%collapsible]
|
||||||
|
.For instance...
|
||||||
|
====
|
||||||
|
* A "canonical"/shorted IPv6 address: `::c000:20a`
|
||||||
|
* A fully "exploded" IPv6 address: `0000:0000:0000:0000:0000:0000:c000:020a`
|
||||||
|
* Hex bytes:
|
||||||
|
** `00:00:00:00:00:00:00:00:00:00:00:00:C0:00:02:0A`
|
||||||
|
** `0x000000000000000000000000C000020A`
|
||||||
|
** Via `xxd(1)`:
|
||||||
|
+
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
# xxd -c4 -g 1
|
||||||
|
00000000: 00 00 00 00 ....
|
||||||
|
00000004: 00 00 00 00 ....
|
||||||
|
00000008: 00 00 00 00 ....
|
||||||
|
0000000c: c0 00 02 0a ....
|
||||||
|
----
|
||||||
|
** Via `hexdump(1)`:
|
||||||
|
+
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
# hexdump -v -e '"%08_ax: " 4/1 "0x%02X ""\n"'
|
||||||
|
00000000: 0x00 0x00 0x00 0x00
|
||||||
|
00000004: 0x00 0x00 0x00 0x00
|
||||||
|
00000008: 0x00 0x00 0x00 0x00
|
||||||
|
0000000c: 0xC0 0x00 0x02 0x0A
|
||||||
|
----
|
||||||
|
* A Go byte slice:
|
||||||
|
** Either as explicit `byte`:
|
||||||
|
+
|
||||||
|
[source,go]
|
||||||
|
----
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xC0, 0x00, 0x02, 0x0A,
|
||||||
|
}
|
||||||
|
----
|
||||||
|
** Or as `uint8` (a `byte` is, quite literally, https://go.dev/ref/spec#Numeric_types[just a type alias for `uint8`^]):
|
||||||
|
+
|
||||||
|
[source,go]
|
||||||
|
----
|
||||||
|
[]byte{
|
||||||
|
uint8(0), uint8(0), uint8(0), uint8(0),
|
||||||
|
uint8(0), uint8(0), uint8(0), uint8(0),
|
||||||
|
uint8(0), uint8(0), uint8(0), uint8(0),
|
||||||
|
uint8(192), uint8(0), uint8(2), uint8(10),
|
||||||
|
}
|
||||||
|
----
|
||||||
|
====
|
||||||
|
|
||||||
|
When seeing it represented as a slice of `uint8`, some things should start to click mentally looking at the last
|
||||||
|
four bytes.
|
||||||
|
|
||||||
|
An IPv4 address can then be derived by taking the last four bytes (`[]byte{...}[12:16]`):
|
||||||
|
|
||||||
|
* `[]byte{0xC0, 0x00, 0x02, 0x0A}`
|
||||||
|
* `[]byte(uint8(192), uint8(0), uint8(2), uint8(10))`
|
||||||
|
|
||||||
|
As you may have guessed by now, this is the byte format of the IPv4 address `192.0.2.10`.
|
||||||
|
|
||||||
|
The `netx.UnmapAddr()` function handles this cleanly.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
If you instead prefer to think in _prefixing/masking_, the last four bytes are *32 bits* (`8 * 4`) -- the size of an IPv4 address.
|
||||||
|
|
||||||
|
The "compatible" mapping is '::/96', and an IPv6 address is *128 bits* (*16 bytes*, `128 / 8`) total -- `128 - 96 == 32 bits`, or *4 bytes*.
|
||||||
|
|
||||||
|
[id="4in6_pfx"]
|
||||||
|
=== Prefixes
|
||||||
|
Prefixes are a little more tricky.
|
||||||
|
|
||||||
|
Prefixing for IPv4-Compatible/IPv4-Mapped IPv6 addresses works a bit oddly because generally the "4-in-6" addresses are
|
||||||
|
meant to be purely representative - a way to represent IPv4 addresses in IPv6 implementations.
|
||||||
|
|
||||||
|
But there are cases where one needs a *prefix* represented.
|
||||||
|
This leads to the awkward case where an IPv4 prefix length (e.g. `/24`) needs to be represented as an IPv6 prefix length (e.g. `/120`).
|
||||||
|
|
||||||
|
While this module (`netx`) does offer helper functions to calculate this (`netx.IP4MapPfx6to4()`, `netx.IP4MapPfx4to6()`),
|
||||||
|
the `netx.UnmapPfx()` function is offered as a convenience function that handles the translation from an IPv6 representation
|
||||||
|
of an IPv4 prefix into a fully "native" IPv4 prefix.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
`netx.IP4MapPfx4to6(len4 uint8)` simply takes the prefix length `len4`, and (if `len4 <= 32`) adds `96`.
|
||||||
|
|
||||||
|
`netx.IP4MapPfx6to4(len6 uint8)` simply takes the prefix length `len6`, and (if `len6 >= 96`) subtracts `96`.
|
||||||
|
====
|
||||||
|
|
||||||
|
This is achieved by:
|
||||||
|
|
||||||
|
. Taking the address from the prefix (`ip4in6 = pfx4in6.Addr()`)
|
||||||
|
.. And <<4in6_addr, converting to native IPv4>> (`ip4 = netx.UnmapAddr(ip4in6)`)
|
||||||
|
. Taking the IPv6 prefix length (`len6 = pfx4in6.Bits()`)
|
||||||
|
.. And converting it to an IPv4 prefix length (`len4 = netx.IP4MapPfx6to4(uint8(len6))`)
|
||||||
|
. Then re-assembling (`pfx4 = netip.PrefixFrom(ip4, int(len4))`)
|
||||||
|
|
||||||
|
|
||||||
|
[id="misc"]
|
||||||
|
== Miscellaneous
|
||||||
|
|
||||||
|
[id="misc_unspec"]
|
||||||
|
=== Unspecified Address
|
||||||
|
There are two "unspecified" addresses for each family:
|
||||||
|
|
||||||
|
* IPv4
|
||||||
|
** `0.0.0.0/0`
|
||||||
|
** `0.0.0.0/32`
|
||||||
|
* IPv6
|
||||||
|
** `::/0`
|
||||||
|
** `::/128`
|
||||||
|
|
||||||
|
For each family, the `/0` prefix length
|
||||||
+45
-3
@@ -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
@@ -3,7 +3,7 @@
|
|||||||
package netx
|
package netx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`golang.org/x/sys/unix`
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
package netx
|
package netx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`golang.org/x/sys/windows`
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+124
@@ -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
@@ -1,7 +1,7 @@
|
|||||||
package netx
|
package netx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`errors`
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
+654
-129
@@ -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
@@ -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) {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
Package slicesx aims to extend functionality of the stdlib [slices] module.
|
||||||
|
*/
|
||||||
|
package slicesx
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+528
-8
@@ -1,13 +1,14 @@
|
|||||||
package stringsx
|
package stringsx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`bytes`
|
"bytes"
|
||||||
`errors`
|
"errors"
|
||||||
`fmt`
|
"fmt"
|
||||||
`io`
|
"io"
|
||||||
`slices`
|
"slices"
|
||||||
`strings`
|
"strings"
|
||||||
`unicode`
|
"unicode"
|
||||||
|
`unicode/utf8`
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -17,6 +18,7 @@ It is more strict than [HasBoundary] (which only requires
|
|||||||
that s has sym at the beginning OR the end.)
|
that s has sym at the beginning OR the end.)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
HasBookend("|foo|", "|") → true
|
HasBookend("|foo|", "|") → true
|
||||||
HasBookend("|foo", "|") → false
|
HasBookend("|foo", "|") → false
|
||||||
HasBookend("foo|", "|") → false
|
HasBookend("foo|", "|") → false
|
||||||
@@ -38,6 +40,7 @@ func HasBookend(s, sym string) (bounded bool) {
|
|||||||
HasBoundary returns true if string s starts OR ends with symbol sym.
|
HasBoundary returns true if string s starts OR ends with symbol sym.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
HasBoundary("|foo|", "|") → true
|
HasBoundary("|foo|", "|") → true
|
||||||
HasBoundary("|foo", "|") → true
|
HasBoundary("|foo", "|") → true
|
||||||
HasBoundary("foo|", "|") → true
|
HasBoundary("foo|", "|") → true
|
||||||
@@ -453,6 +456,37 @@ func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
RemoveWhitespace removes all leading, trailing, and INNER whitespace
|
||||||
|
from unicode string `s`.
|
||||||
|
|
||||||
|
This is done by allocating a strings.Builder with len(s).
|
||||||
|
This may consume more memory than needed if s is mostly whitespace;
|
||||||
|
in that case, it is better to use [StripWhitespace].
|
||||||
|
*/
|
||||||
|
func RemoveWhitespace(s string) (removed string) {
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/32081808/strip-all-whitespace-from-a-string/32081891#32081891
|
||||||
|
|
||||||
|
var c rune
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
if s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Grow(len(s))
|
||||||
|
for _, c = range s {
|
||||||
|
if !unicode.IsSpace(c) {
|
||||||
|
sb.WriteRune(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removed = sb.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Reverse reverses string s. (It's absolutely insane that this isn't in stdlib.)
|
// Reverse reverses string s. (It's absolutely insane that this isn't in stdlib.)
|
||||||
func Reverse(s string) (revS string) {
|
func Reverse(s string) (revS string) {
|
||||||
|
|
||||||
@@ -465,6 +499,491 @@ 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
|
||||||
|
from unicode string `s`.
|
||||||
|
|
||||||
|
This is done by mapping each rune in `s`.
|
||||||
|
This may use far more allocations than necessary if `s` is mostly NON-whitespace;
|
||||||
|
in that case, it is better to use [RemoveWhitespace].
|
||||||
|
*/
|
||||||
|
func StripWhitespace(s string) (stripped string) {
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/32081808/strip-all-whitespace-from-a-string/32081891#32081891
|
||||||
|
|
||||||
|
if s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stripped = strings.Map(
|
||||||
|
func(c rune) rune {
|
||||||
|
if unicode.IsSpace(c) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
},
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
|
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
|
||||||
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
|
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
|
||||||
@@ -500,7 +1019,8 @@ func TrimLines(s string, left, right bool) (trimmed string) {
|
|||||||
} else if right {
|
} else if right {
|
||||||
sl = TrimSpaceRight(sl)
|
sl = TrimSpaceRight(sl)
|
||||||
}
|
}
|
||||||
sb.WriteString(sl + nl)
|
sb.WriteString(sl)
|
||||||
|
sb.WriteString(nl)
|
||||||
}
|
}
|
||||||
|
|
||||||
trimmed = sb.String()
|
trimmed = sb.String()
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
// "fmt"
|
||||||
|
"log"
|
||||||
|
// "os"
|
||||||
|
// "path/filepath"
|
||||||
|
"text/template"
|
||||||
|
// "github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed "tplfiles"
|
||||||
|
tplfiles embed.FS
|
||||||
|
|
||||||
|
tpl *template.Template = template.New("")
|
||||||
|
|
||||||
|
tw *TplWalker = new(TplWalker)
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
TplWalker struct {
|
||||||
|
tpl *template.Template
|
||||||
|
fs fs.FS
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *TplWalker) walkFunc(fpath string, d fs.DirEntry, inErr error) (err error) {
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
var f fs.File
|
||||||
|
|
||||||
|
if inErr != nil {
|
||||||
|
err = inErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.IsDir() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, err = t.fs.Open(fpath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
var ok bool
|
||||||
|
var c io.Closer
|
||||||
|
if f != nil {
|
||||||
|
if c, ok = f.(io.Closer); ok {
|
||||||
|
if err = c.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if b, err = io.ReadAll(f); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = t.tpl.New(fpath).Parse(string(b)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TplWalker) Walk() (err error) {
|
||||||
|
|
||||||
|
if err = fs.WalkDir(t.fs, ".", t.walkFunc); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
tw.tpl = tpl
|
||||||
|
tw.fs = tplfiles
|
||||||
|
|
||||||
|
if err = tw.Walk(); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var idx int
|
||||||
|
var sub *template.Template
|
||||||
|
|
||||||
|
log.Printf("Root template name is %q", tpl.Name())
|
||||||
|
for idx, sub = range tpl.Templates() {
|
||||||
|
log.Printf("Sub template %d: %q", idx, sub.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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-06-22 18:51:24 -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>
|
||||||
|
|||||||
@@ -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-06-22 18:51:29 -0400</span>
|
<span id="revdate">Last rendered 2026-07-29 15:05:45 -0400</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user