From 3a7ed5973b0d5c12dd8e02f844d086774af6f411 Mon Sep 17 00:00:00 2001 From: "brent s." Date: Sat, 1 Feb 2025 23:15:54 -0500 Subject: [PATCH] this is cool and all but the tables don't render properly --- .gitignore | 43 ++- build.sh | 127 ++++++++ cmd/subnetter/_tpl/table.tpl | 52 ++++ cmd/subnetter/args.go | 30 +- cmd/subnetter/consts.go | 99 +++++- cmd/subnetter/errs.go | 9 + cmd/subnetter/funcs.go | 40 +-- cmd/subnetter/funcs_tblfmt.go | 64 ++++ cmd/subnetter/funcs_tblrows.go | 54 ++++ cmd/subnetter/funcs_tblsizers.go | 113 +++++++ cmd/subnetter/funcs_tpl.go | 447 ++++++++++++++++++++++++++++ cmd/subnetter/main.go | 55 +++- cmd/subnetter/types.go | 126 ++++++++ go.mod | 10 +- go.sum | 20 +- netsplit/errs.go | 1 + netsplit/funcs.go | 19 +- netsplit/funcs_cidrsplitter.go | 50 +++- netsplit/funcs_hostsplitter.go | 60 +++- netsplit/funcs_structuredresults.go | 3 +- netsplit/funcs_subnetsplitter.go | 98 +++++- netsplit/funcs_vlsmsplitter.go | 12 +- netsplit/init.go | 3 + netsplit/types.go | 9 +- version/consts.go | 63 ++++ version/funcs.go | 180 +++++++++++ version/funcs_buildinfo.go | 133 +++++++++ version/types.go | 85 ++++++ 28 files changed, 1917 insertions(+), 88 deletions(-) create mode 100755 build.sh create mode 100644 cmd/subnetter/_tpl/table.tpl create mode 100644 cmd/subnetter/errs.go create mode 100644 cmd/subnetter/funcs_tblfmt.go create mode 100644 cmd/subnetter/funcs_tblrows.go create mode 100644 cmd/subnetter/funcs_tblsizers.go create mode 100644 cmd/subnetter/funcs_tpl.go create mode 100644 netsplit/init.go create mode 100644 version/consts.go create mode 100644 version/funcs.go create mode 100644 version/funcs_buildinfo.go create mode 100644 version/types.go diff --git a/.gitignore b/.gitignore index 8d4ab5f..69181dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,42 @@ -cmd/subnetter/subnetter +*.7z +*.bak +*.deb +*.jar +*.log +*.rar +*.run +*.sig +*.tar +*.tar.bz2 +*.tar.gz +*.tar.xz +*.tbz +*.tbz2 +*.tgz +*.txz +*.zip +.*.swp +.editix +.idea/ + +# https://github.com/github/gitignore/blob/master/Go.gitignore +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +/bin/* +/_examples + +# Test binary, built with `go test -c` +*.test +# But allow test suite. +!*test.go + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..218bcb5 --- /dev/null +++ b/build.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +set -e + +# This is not portable. It has bashisms. + +BUILD_TIME="$(date '+%s')" +BUILD_USER="$(whoami)" +BUILD_SUDO_USER="${SUDO_USER}" +BUILD_HOST="$(hostname)" + +# Check to make sure git is available. +if ! command -v git &> /dev/null; +then + echo "Git is not available; automatic version handling unsupported." + echo "You must build by calling 'go build' directly in the respective directories." + exit 0 +fi + +# Check git directory/repository. +if ! git rev-parse --is-inside-work-tree &>/dev/null; +then + echo "Not running inside a git work tree; automatic version handling unsupported/build script unsupported." + echo "You must build by calling 'go build' directly in the respective directories instead." + exit 0 +fi + +# The repo URI for origin. +REPO_URI="$(git remote get-url origin)" + +# If it has a tag in the path of the current HEAD that matches a version string... +# I wish git describe supported regex. It does not; only globs. Gross. +# If there's a bug anywhere, it's here. +if git describe --tags --abbrev=0 --match "v[0-9]*" HEAD &> /dev/null; +then + # It has a tag we can use. + CURRENT_VER="$(git describe --tags --abbrev=0 --match "v[0-9]*" HEAD)" + COMMITS_SINCE="$(git rev-list --count ${CURRENT_VER}..HEAD)" +else + # No tag available. + CURRENT_VER="" + COMMITS_SINCE="" +fi + +# If it's dirty (staged but not committed or unstaged files)... +if ! git diff-index --quiet HEAD; +then + # It's dirty. + IS_DIRTY="1" +else + # It's clean. + IS_DIRTY="0" +fi + +# Get the commit hash of the *most recent* commit in the path of current HEAD... +CURRENT_HASH="$(git rev-parse --verify HEAD)" +# The same as above, but abbreviated. +CURRENT_SHORT="$(git rev-parse --verify --short HEAD)" + +# Get the module name. +MODPATH="$(sed -n -re 's@^\s*module\s+(.*)(//.*)?$@\1@p' go.mod)" + +# Build the ldflags string. +LDFLAGS_STR="\ +-X '${MODPATH}/version.repoUri=${REPO_URI}' \ +-X '${MODPATH}/version.sourceControl=git' \ +-X '${MODPATH}/version.version=${CURRENT_VER}' \ +-X '${MODPATH}/version.commitHash=${CURRENT_HASH}' \ +-X '${MODPATH}/version.commitShort=${CURRENT_SHORT}' \ +-X '${MODPATH}/version.numCommitsAfterTag=${COMMITS_SINCE}' \ +-X '${MODPATH}/version.isDirty=${IS_DIRTY}' \ +-X '${MODPATH}/version.buildTime=${BUILD_TIME}' \ +-X '${MODPATH}/version.buildUser=${BUILD_USER}' \ +-X '${MODPATH}/version.buildSudoUser=${BUILD_SUDO_USER}' \ +-X '${MODPATH}/version.buildHost=${BUILD_HOST}'" + + +# And finally build. +export CGO_ENABLED=0 + +mkdir -p ./bin/ + +cmd="subnetter" +# Linux +bin="${cmd}" +echo "Building ./bin/${bin}..." +go build \ + -o "./bin/${bin}" \ + -ldflags \ + "${LDFLAGS_STR}" \ + cmd/${cmd}/*.go +echo " Done." + +# Windows +GOOS="windows" +bin="subnetter.exe" +echo "Building ./bin/${bin}..." +go build \ + -o "./bin/${bin}" \ + -ldflags \ + "${LDFLAGS_STR}" \ + cmd/${cmd}/*.go +echo " Done." + +# macOS +GOOS="darwin" +## x86_64 +bin="subnetter.x86_64.app" +echo "Building ./bin/${bin}..." +go build \ + -o "./bin/${bin}" \ + -ldflags \ + "${LDFLAGS_STR}" \ + cmd/${cmd}/*.go +echo " Done." +## ARM64 (m1/m2/... etc.; "Apple Silicon") +GOARCH="arm64" +bin="subnetter.m1.app" +echo "Building ./bin/${bin}..." +go build \ + -o "./bin/${bin}" \ + -ldflags \ + "${LDFLAGS_STR}" \ + cmd/${cmd}/*.go +echo " Done." + +echo "Build complete." diff --git a/cmd/subnetter/_tpl/table.tpl b/cmd/subnetter/_tpl/table.tpl new file mode 100644 index 0000000..eaed57d --- /dev/null +++ b/cmd/subnetter/_tpl/table.tpl @@ -0,0 +1,52 @@ +{{- /*gotype: subnetter/cmd/subnetter.tableOpts*/ -}} +{{- $opts := . -}} +{{- $numRows := 0 -}} +{{- if not $opts.NoIpv4 }} +IPv4: + {{- if $opts.Legacy -}} + {{- $legacyspec := legacy4 }} + {{- $numRows = len $legacyspec.Rows }} + + LEGACY: + {{ $legacyspec.Sizer.Hdr "" $opts.Plain }} + {{- range $idx, $row := $legacyspec.Rows }} + {{- $row.Row $legacyspec.Sizer "\t" $opts.Plain -}} + {{- $legacyspec.Sizer.Line "\t" $opts.Plain $idx $numRows }} + {{- end }} + {{- end }} + + {{- if not $opts.NoV4Mask }} + {{- $masks := mask4 }} + NETMASKS: + {{ $masks.Sizer.Hdr "\t" $opts.Plain }} + {{- range $idx, $row := $masks.Rows }} + {{- $row.Row $masks.Sizer "\t" $opts.Plain }} + {{- $masks.Sizer.Line "\t" $opts.Plain $idx $numRows }} + {{- end }} + {{- end }} + + CIDR: + {{- $pfxs := addrs 4 }} + {{- $numRows = len $pfxs.Rows }} + {{ $pfxs.Sizer.Hdr "" $opts.Plain }} + {{- range $idx, $row := $pfxs.Rows }} + {{- $row.Row $pfxs.Sizer "\t" $opts.Plain }} + {{- $pfxs.Sizer.Line "\t" $opts.Plain $idx $numRows }} + {{- end }} + +{{- end }} + +{{- if not $opts.NoIpv6 }} + +IPv6: + + CIDR: + {{- $pfxs := addrs 6 }} + {{- $numRows = len $pfxs.Rows }} + {{- $pfxs.Sizer.Hdr "\t" $opts.Plain }} + {{- range $idx, $row := $pfxs.Rows }} + {{- $row.Row $pfxs.Sizer "\t" $opts.Plain }} + {{- $pfxs.Sizer.Line "\t" $opts.Plain $idx $numRows }} + {{- end }} + +{{- end }} diff --git a/cmd/subnetter/args.go b/cmd/subnetter/args.go index 57edc8d..07946c2 100644 --- a/cmd/subnetter/args.go +++ b/cmd/subnetter/args.go @@ -1,19 +1,21 @@ package main type Args struct { - SplitCIDR SplitCIDRArgs `command:"split-cidr" alias:"se" description:"Split a network into as many equal parts of a given prefix as possible." validate:"omitempty"` - SplitHost SplitHostArgs `command:"split-hosts" alias:"sh" description:"Split a network into n total number of hosts into subnet as cleanly/evenly as possible." validate:"omitempty"` - SplitSubnets SplitSubnetArgs `command:"split-nets" alias:"sn" description:"Split a network into n number of subnets as cleanly as possible." validate:"omitempty"` - VLSM VLSMArgs `command:"vlsm" alias:"v" description:"Use VLSM (Variable-Length Subnet Masks) to split a network into differently sized subnets." validate:"omitempty"` - Parse ParseArgs `command:"parse" alias:"p" alias:"read" alias:"convert" description:"Parse/convert output from a previous subnetter run." validate:"omitempty"` - Table TableArgs `command:"table" alias:"t" alias:"tab" alias:"tbl" description:"Show prefix summaries (by default both IPv4 and IPv6)." validate:"omitempty"` + Version bool `short:"v" long:"version" description:"Print the version and exit."` + DetailVersion bool `short:"V" long:"detail" description:"Print detailed version info and exit."` + SplitCIDR SplitCIDRArgs `command:"split-cidr" alias:"se" description:"Split a network into as many equal subnets of prefix size N as possible." validate:"omitempty"` + SplitHost SplitHostArgs `command:"split-hosts" alias:"sh" description:"Split a network into N total number of hosts *per subnet* as cleanly/evenly as possible. (VERY easy to run out of memory for IPv6 prefixes; be sure to specify very small network!)" validate:"omitempty"` + SplitSubnets SplitSubnetArgs `command:"split-nets" alias:"sn" description:"Split a network into N number of subnets as cleanly as possible." validate:"omitempty"` + VLSM VLSMArgs `command:"vlsm" alias:"v" description:"Use VLSM (Variable-Length Subnet Masks) to split a network into differently sized subnets." validate:"omitempty"` + Parse ParseArgs `command:"parse" alias:"p" alias:"read" alias:"convert" description:"Parse/convert output from a previous subnetter run." validate:"omitempty"` + Table TableArgs `command:"table" alias:"t" alias:"tab" alias:"tbl" description:"Show prefix summaries (by default both IPv4 and IPv6)." validate:"omitempty"` } type outputOpts struct { SuppressRemaining bool `short:"r" long:"no-remaining" description:"Don't show leftover/unallocated/remaining space.'"` - Verbose []bool `short:"v" long:"verbose" description:"Show verbose information. May be specified multiple times to increase verbosity (up to 3 levels)."` + Verbose []bool `short:"v" long:"verbose" description:"Show verbose information if -f/--format=pretty. May be specified multiple times to increase verbosity (up to 3 levels)."` Seperator string `short:"S" long:"seperator" default:"\n" description:"Separator between addresses; only used for -f/--format=pretty with no verbosity."` - Fmt string `short:"f" long:"format" choice:"json" choice:"pretty" choice:"yml" choice:"xml" default:"pretty" description:"Output format. DO NOT parse 'pretty' as its output is not guaranteed between versions."` + Fmt string `short:"f" long:"format" choice:"json" choice:"pretty" choice:"yml" choice:"xml" default:"pretty" description:"Output format. 'pretty' is not intended to be parseable, either by subnetter or by external tooling."` } type common struct { @@ -36,21 +38,21 @@ type SplitCIDRArgs struct { } type SplitHostArgs struct { - Hosts uint `short:"n" long:"num-hosts" required:"true" description:"Number of hosts (usable addresses) per subnet." validate:"required"` + Strict bool `short:"t" long:"strict" description:"If specified, an error will occur if the number of hosts/assignable addresses in a subnet is not exactly -n/--num-hosts."` + Hosts uint `short:"n" long:"num-hosts" required:"true" description:"Number of hosts (usable addresses) per subnet." validate:"required"` common } type SplitSubnetArgs struct { + Strict bool `short:"t" long:"strict" description:"If specified, an error will occur if the number of possible equally-sized subnets is not exactly -n/--num-nets."` NumNets uint `short:"n" long:"num-nets" required:"true" description:"Number of networks." validate:"required"` common } type TableArgs struct { - NoIpv6 bool `short:"4" long:"ipv4" description:"Show IPv4 table."` - NoIpv4 bool `short:"6" long:"ipv6" description:"Show IPv6 table."` - Verbose []bool `short:"v" long:"verbose" description:"Show verbose information. May be specified multiple times to increase verbosity (up to 3 levels)."` - Fmt string `short:"f" long:"format" choice:"csv" choice:"json" choice:"pretty" choice:"tsv" choice:"yml" choice:"xml" default:"pretty" description:"Output format."` - Net *string `short:"n" long:"network" description:"If specified, provide information explicitly about this network. Ignores -4/--ipv4 and -6/--ipv6." validate:"omitempty,cidr"` + tableOpts + Verbose []bool `short:"v" long:"verbose" description:"Show verbose information (if -n/--network is specified). May be specified multiple times to increase verbosity (up to 2 levels)."` + Net *string `short:"n" long:"network" description:"If specified, print detailed information explicitly about this network instead of reference. Ignores all other options except -v/--verbose." validate:"omitempty,cidr"` } type VLSMArgs struct { diff --git a/cmd/subnetter/consts.go b/cmd/subnetter/consts.go index a8fc530..aae000c 100644 --- a/cmd/subnetter/consts.go +++ b/cmd/subnetter/consts.go @@ -1,18 +1,103 @@ package main import ( - "github.com/go-playground/validator/v10" + `embed` "strings" + `text/template` + + "github.com/go-playground/validator/v10" ) var ( - args *Args = new(Args) - validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled()) + args = new(Args) + validate = validator.New(validator.WithRequiredStructEnabled()) +) + +const ( + /* + fixedPad is a fixed "surrounding" pad always present (as minimum), even for values max len on columns. + *Must* be positive even, as and == fixedPad/2. + */ + fixedPad int = 2 + /* + padChars is what fills the pads in table cells. + At the *LEAST*, a cell will be "" + */ + padChars string = " " ) var ( - sectSepCnt int = 48 - sectSep1 string = strings.Repeat("=", sectSepCnt) - sectSep2 string = strings.Repeat("-", sectSepCnt) - sectSep3 string = strings.Repeat(".", sectSepCnt) + //go:embed "_tpl" + tplDir embed.FS + tblTpl *template.Template = template.Must(template.New("").Funcs( + template.FuncMap{ + "legacy4": tplClass4Iter, + "addrs": tplAddrIter, + "mask4": tplMaskIter4, + }, + ).ParseFS(tplDir, "_tpl/*.tpl")) +) + +var ( + // Primarily output formatting stuff in this block. + sectSepCnt = 48 + sectSep1 = strings.Repeat("=", sectSepCnt) + sectSep2 = strings.Repeat("-", sectSepCnt) + sectSep3 = strings.Repeat(".", sectSepCnt) + // tblFmts contains a lookup of map[]*tableFormatter. + tblFmts map[bool]*tableFormatter = map[bool]*tableFormatter{ + // Plaintext/ASCII-only + true: &tableFormatter{ + TopLeftHdr: "*", // Or _ + TopFillHdr: "*", // "" + TopColSepHdr: "*", // "" + TopRightHdr: "*", // "" + ColSepHdr: "|", + BottomLeftHdr: "*", // Or + + BottomFillHdr: "*", // Or - + BottomColSepHdr: "*", // Or + + BottomRightHdr: "*", // "" + Left: "|", + Fill: "-", + LineColSep: "|", + LineLeft: "|", + LineRight: "|", + ColSep: "|", + Right: "|", + LastLeft: "+", + LastFill: "-", + LastSep: "-", + LastRight: "+", + SuppressLineSep: true, + NoUpperTitle: false, + NoBoldTitle: true, + }, + // Unicode/UTF-8 + // https://en.wikipedia.org/wiki/Box-drawing_characters + false: &tableFormatter{ + TopLeftHdr: "┏", + TopFillHdr: "━", + TopColSepHdr: "┳", + TopRightHdr: "┓", + ColSepHdr: "┃", + BottomLeftHdr: "┣", + BottomFillHdr: "━", + BottomColSepHdr: "╇", + BottomRightHdr: "┫", + Left: "┃", + Fill: "─", + LineColSep: "┼", + LineLeft: "┠", + LineRight: "┨", + ColSep: "│", + Right: "┃", + LastLeft: "┗", + LastFill: "━", + LastSep: "┷", + LastRight: "┛", + SuppressLineSep: false, + NoUpperTitle: true, + NoBoldTitle: false, + }, + } ) diff --git a/cmd/subnetter/errs.go b/cmd/subnetter/errs.go new file mode 100644 index 0000000..dd067b9 --- /dev/null +++ b/cmd/subnetter/errs.go @@ -0,0 +1,9 @@ +package main + +import ( + `errors` +) + +var ( + errBadNet error = errors.New("bad inet/addr family/version") +) diff --git a/cmd/subnetter/funcs.go b/cmd/subnetter/funcs.go index d335999..24895b2 100644 --- a/cmd/subnetter/funcs.go +++ b/cmd/subnetter/funcs.go @@ -6,24 +6,26 @@ import ( "encoding/json" "encoding/xml" "fmt" - "github.com/goccy/go-yaml" - "github.com/projectdiscovery/mapcidr" - "go4.org/netipx" "io" "net" "net/netip" "os" "strings" - "subnetter/netsplit" "time" + + "github.com/goccy/go-yaml" + "github.com/projectdiscovery/mapcidr" + "go4.org/netipx" + "subnetter/netsplit" + `subnetter/version` ) func printHostPrefix(label string, pfx *netip.Prefix, verb, indent int, indentStr string) (out string) { var maskEvery uint - var sb *strings.Builder = new(strings.Builder) - var pre string = strings.Repeat(indentStr, indent) - var pre2 string = strings.Repeat(indentStr, indent+1) + var sb = new(strings.Builder) + var pre = strings.Repeat(indentStr, indent) + var pre2 = strings.Repeat(indentStr, indent+1) if pfx == nil { fmt.Fprintf(sb, "%s%s:\n%sAddress:\t(N/A)\n", pre, label, pre2) @@ -102,10 +104,10 @@ func printMask(label string, pfx netip.Prefix, verb, indent int, indentStr strin var mask net.IPMask var first netip.Addr var last netip.Addr - var sb *strings.Builder = new(strings.Builder) - var pre string = strings.Repeat(indentStr, indent) - var pre2 string = strings.Repeat(indentStr, indent+1) - var pre3 string = strings.Repeat(indentStr, indent+2) + var sb = new(strings.Builder) + var pre = strings.Repeat(indentStr, indent) + var pre2 = strings.Repeat(indentStr, indent+1) + var pre3 = strings.Repeat(indentStr, indent+2) if !pfx.IsValid() { return @@ -156,7 +158,8 @@ func printMask(label string, pfx netip.Prefix, verb, indent int, indentStr strin fmt.Fprintf(sb, "%sBits:\t\t%d\n", pre2, pfx.Bits()) fmt.Fprintf(sb, "%sFirst:\t\t%s\n", pre2, first.String()) fmt.Fprintf(sb, "%sLast:\t\t%s\n", pre2, last.String()) - fmt.Fprintf(sb, "%sAddresses:\t%d\n", pre2, mapcidr.CountIPsInCIDR()) + fmt.Fprintf(sb, "%sAddresses:\t%d\n", pre2, mapcidr.CountIPsInCIDR(true, true, netipx.PrefixIPNet(pfx.Masked()))) + fmt.Fprintf(sb, "%sHosts:\t\t%d\n", pre2, mapcidr.CountIPsInCIDR(false, false, netipx.PrefixIPNet(pfx.Masked()))) if verb >= 2 { fmt.Fprintf(sb, "%sExpanded:\t%s\n", pre2, netsplit.MaskExpand(mask, pfx.Addr().Is6())) fmt.Fprintf(sb, "%sHex:\t\t0x%s\n", pre2, mask.String()) @@ -215,7 +218,7 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem var remPfxs []*netip.Prefix var invertedMask net.IPMask var res *netsplit.StructuredResults - var verb int = -1 + var verb = -1 if orig == nil { return @@ -341,7 +344,6 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem } } else { buf = new(bytes.Buffer) - // TODO: data-formatted/structured output if res, err = netsplit.Contain(orig, nets, remaining, splitter); err != nil { return } @@ -355,11 +357,11 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem fmt.Fprintf( buf, ``+ - "\n", - time.Now().String(), + version.Ver.Short(), time.Now().String(), ) if b, err = xml.MarshalIndent(res, "", " "); err != nil { return @@ -368,9 +370,9 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem case "yml": fmt.Fprintf( buf, - "# Generated by subnetter.\n"+ + "# Generated by subnetter %s\n"+ "# %s\n\n", - time.Now().String(), + version.Ver.Short(), time.Now().String(), ) if b, err = yaml.Marshal(res); err != nil { return diff --git a/cmd/subnetter/funcs_tblfmt.go b/cmd/subnetter/funcs_tblfmt.go new file mode 100644 index 0000000..daf86d6 --- /dev/null +++ b/cmd/subnetter/funcs_tblfmt.go @@ -0,0 +1,64 @@ +package main + +import ( + `fmt` + `strings` +) + +func padStr(str string, colSize uint8) (out string) { + + var fill int + var lFill int + var rFill int + var strLen int = len(str) + + // EZPZ. Exact match. + if strLen+fixedPad == int(colSize) { + out = fmt.Sprintf( + "%s%s%s", + strings.Repeat(padChars, fixedPad/2), str, strings.Repeat(padChars, fixedPad/2), + ) + return + } + + // This is where it gets... annoying. + fill = int(colSize) - (strLen + fixedPad) + if fill%2 == 0 { + /* + Split evenly left/right. + This condition will be met if BOTH strLen and colSize are even, + or if BOTH strLen and colSize are odd. + Math! + */ + lFill = fill / 2 + rFill = fill / 2 + } else { + // Either the value or the width is odd, and the other is even. + // (Note: Goland automatically floors an int/int calculation's result.) + // As such, asymmetrical padding is needed. + if strLen%2 == 0 { + /* + String is even, width is odd. + Favor (smaller fill) the left. + */ + lFill = fill / 2 + rFill = (fill + 1) / 2 // This works instead of math.Ceil because dividing by 2. + } else { + /* + String is odd, width is even. + Favor right pad. + */ + lFill = (fill + 1) / 2 + rFill = fill / 2 + } + } + + out = fmt.Sprintf( + "%s%s%s", + strings.Repeat(padChars, lFill+fixedPad/2), + str, + strings.Repeat(padChars, rFill+fixedPad/2), + ) + + return +} diff --git a/cmd/subnetter/funcs_tblrows.go b/cmd/subnetter/funcs_tblrows.go new file mode 100644 index 0000000..1a57d74 --- /dev/null +++ b/cmd/subnetter/funcs_tblrows.go @@ -0,0 +1,54 @@ +package main + +import ( + `reflect` +) + +// Row prints the formatted row for a tableAddr. +func (t *tableAddr) Row(sizer *tableAddrSizer, indent string, plain bool) (out string) { + + var val reflect.Value + var sizerVal reflect.Value + + if t == nil || sizer == nil { + return + } + val = reflect.ValueOf(*t) + sizerVal = reflect.ValueOf(*sizer) + + out = rowRender(val, sizerVal, indent, plain) + return +} + +// Row prints the formatted row for a tableLegacy4. +func (t *tableLegacy4) Row(sizer *tableLegacy4Sizer, indent string, plain bool) (out string) { + + var val reflect.Value + var sizerVal reflect.Value + + if t == nil || sizer == nil { + return + } + val = reflect.ValueOf(*t) + sizerVal = reflect.ValueOf(*sizer) + + out = rowRender(val, sizerVal, indent, plain) + return +} + +// Row prints the formatted row for a tableMask4. +func (t *tableMask4) Row(sizer *tableMask4Sizer, indent string, plain bool) (out string) { + + var val reflect.Value + var sizerVal reflect.Value + + if t == nil || sizer == nil { + return + } + val = reflect.ValueOf(*t) + sizerVal = reflect.ValueOf(*sizer) + + out = rowRender(val, sizerVal, indent, plain) + + return +} diff --git a/cmd/subnetter/funcs_tblsizers.go b/cmd/subnetter/funcs_tblsizers.go new file mode 100644 index 0000000..b606abd --- /dev/null +++ b/cmd/subnetter/funcs_tblsizers.go @@ -0,0 +1,113 @@ +package main + +import ( + `reflect` +) + +/* + Hdr prints the header for a tableAddrSizer corresponding to a slice of tableAddr. + + indent will be printed before the string. + + If plain is true, only ASCII chars will be used; otherwise fancy-schmancy Unicode. +*/ +func (t *tableAddrSizer) Hdr(indent string, plain bool) (out string) { + + var val reflect.Value + + if t == nil { + return + } + val = reflect.ValueOf(*t) + + out = hdrRender(val, indent, plain) + + return +} + +// Line either prints the *last line* (the border) or a *row separator* (if allowed in the format). +func (t *tableAddrSizer) Line(indent string, plain bool, rowIdx, numRows int) (out string) { + + var val reflect.Value + + if t == nil { + return + } + val = reflect.ValueOf(*t) + + out = hdrLineRender(val, indent, plain, rowIdx, numRows) + + return +} + +/* + Hdr prints the header for a tableLegacy4Sizer corresponding to a slice of tableLegacy4. + + indent will be printed before the string. + + If plain is true, only ASCII chars will be used; otherwise fancy-schmancy Unicode. +*/ +func (t *tableLegacy4Sizer) Hdr(indent string, plain bool) (out string) { + + var val reflect.Value + + if t == nil { + return + } + val = reflect.ValueOf(*t) + + out = hdrRender(val, indent, plain) + + return +} + +// Line either prints the *last line* (the border) or a *row separator* (if allowed in the format). +func (t *tableLegacy4Sizer) Line(indent string, plain bool, rowIdx, numRows int) (out string) { + + var val reflect.Value + + if t == nil { + return + } + val = reflect.ValueOf(*t) + + out = hdrLineRender(val, indent, plain, rowIdx, numRows) + + return +} + +/* + Hdr prints the header for a tableMask4Sizer corresponding to a slice of tableMask4. + + indent will be printed before the string. + + If plain is true, only ASCII chars will be used; otherwise fancy-schmancy Unicode. +*/ +func (t *tableMask4Sizer) Hdr(indent string, plain bool) (out string) { + + var val reflect.Value + + if t == nil { + return + } + val = reflect.ValueOf(*t) + + out = hdrRender(val, indent, plain) + + return +} + +// Line either prints the *last line* (the border) or a *row separator* (if allowed in the format). +func (t *tableMask4Sizer) Line(indent string, plain bool, rowIdx, numRows int) (out string) { + + var val reflect.Value + + if t == nil { + return + } + val = reflect.ValueOf(*t) + + out = hdrLineRender(val, indent, plain, rowIdx, numRows) + + return +} diff --git a/cmd/subnetter/funcs_tpl.go b/cmd/subnetter/funcs_tpl.go new file mode 100644 index 0000000..b79e7d0 --- /dev/null +++ b/cmd/subnetter/funcs_tpl.go @@ -0,0 +1,447 @@ +package main + +import ( + `encoding/binary` + `fmt` + `net` + `net/netip` + `reflect` + `strconv` + `strings` + + `github.com/TwiN/go-color` + `github.com/projectdiscovery/mapcidr` + `go4.org/netipx` + `subnetter/netsplit` +) + +/* + tplClass4Iter should only be called if legacy info is enabled. + It returns a tableLegacy4Sizer and a slice of tableLegacy4. + + It takes no input. +*/ +func tplClass4Iter() (legacySpec *tableLegacy4Ret, err error) { + + // This whole thing feels dirty. + // It's like adding a microcontroller to a rock. + // But it works. + var pfx *net.IPNet + var classNets []*netip.Prefix + var netRange netipx.IPRange + var v *netsplit.VLSMSplitter = &netsplit.VLSMSplitter{ + Ascending: false, + PrefixLengths: []uint8{ + 1, // A + 2, // B + 3, // C + 4, // D + 4, // E + }, + BaseSplitter: new(netsplit.BaseSplitter), + } + + if _, pfx, err = net.ParseCIDR("0.0.0.0/0"); err != nil { + return + } + v.SetParent(*pfx) + if classNets, _, err = v.Split(); err != nil { + return + } + + legacySpec = &tableLegacy4Ret{ + Sizer: &tableLegacy4Sizer{ + Class: 5, // "CLASS" + CIDR: 4, // "BITS" + Start: 5, // "START" + End: 3, // "END" + }, + Rows: make([]tableLegacy4, 5), + } + for idx, cls := range []string{ + "A", "B", "C", "D", "E", + } { + legacySpec.Rows[idx] = tableLegacy4{ + Class: cls, + CIDR: classNets[idx].String(), + NetCIDR: *classNets[idx], + } + netRange = netipx.RangeOfPrefix(legacySpec.Rows[idx].NetCIDR) + legacySpec.Rows[idx].NetStart = netRange.From() + legacySpec.Rows[idx].NetEnd = netRange.To() + legacySpec.Rows[idx].Start = legacySpec.Rows[idx].NetStart.String() + legacySpec.Rows[idx].End = legacySpec.Rows[idx].NetEnd.String() + } + + return +} + +/* + tplAddrIter takes a 4 or 6 for inet family/version and returns a tableAddrSizer and + slice of tableAddr. + tableAddr is sorted from smallest prefix/largest network to largest prefix/smallest network. +*/ +func tplAddrIter(ipVer uint8) (addrs *tableAddrRet, err error) { + + var dummyAddr netip.Addr + var dummyNet *net.IPNet + var l int + + addrs = &tableAddrRet{ + Sizer: &tableAddrSizer{ + Prefix: 6, // "PREFIX" + Bits: 4, // "BITS" + Addresses: 9, // "ADDRESSES" + Hosts: 5, // "HOSTS" + }, + } + + switch ipVer { + case 4: + if dummyAddr, err = netip.ParseAddr("0.0.0.0"); err != nil { + return + } + case 6: + if dummyAddr, err = netip.ParseAddr("::"); err != nil { + return + } + default: + err = errBadNet + return + } + + // Before we size, we generate the tableAddrs. + addrs.Rows = make([]tableAddr, dummyAddr.BitLen()+1) + for i := 0; i <= dummyAddr.BitLen(); i++ { + addrs.Rows[i] = tableAddr{ + Prefix: uint8(i), + Bits: uint8(dummyAddr.BitLen() - i), + } + if addrs.Rows[i].NetPrefix, err = dummyAddr.Prefix(i); err != nil { + return + } + dummyNet = netipx.PrefixIPNet(addrs.Rows[i].NetPrefix.Masked()) + addrs.Rows[i].Addresses = mapcidr.CountIPsInCIDR(true, true, dummyNet) + addrs.Rows[i].Hosts = mapcidr.CountIPsInCIDR(false, false, dummyNet) + } + + // Now the sizer. The padding itself is handled in different logic, just need the length of the longest value as a string. + for _, addr := range addrs.Rows { + // I *abhor* walrus operators in anything but loops. + l = len(strconv.Itoa(int(addr.Prefix))) + if int(addrs.Sizer.Prefix) < l { + addrs.Sizer.Prefix = uint8(l) + } + l = len(strconv.Itoa(int(addr.Bits))) + if int(addrs.Sizer.Bits) < l { + addrs.Sizer.Bits = uint8(l) + } + // Use the full numeric length. + l = len(addr.Addresses.String()) + if int(addrs.Sizer.Addresses) < l { + addrs.Sizer.Addresses = uint8(l) + } + l = len(addr.Hosts.String()) + if int(addrs.Sizer.Hosts) < l { + addrs.Sizer.Hosts = uint8(l) + } + } + + return +} + +/* + tplMaskIter4 returns a slice of IPv4 netmasks and returns a slice of tableMask4. + Sorted from smallest prefix/largest network to largest prefix/smallest network. +*/ +func tplMaskIter4() (masks *tableMask4Ret, err error) { + + var dummyAddr netip.Addr + var pfx netip.Prefix + var dummyNet *net.IPNet + var l int + + masks = &tableMask4Ret{ + Sizer: &tableMask4Sizer{ + Prefix: 6, // "PREFIX" + Netmask: 7, // "NETMASK" + Hex: 3, // "HEX" + Dec: 3, // "DEC" + Bin: 3, // "BIN" + }, + } + + if dummyAddr, err = netip.ParseAddr("0.0.0.0"); err != nil { + return + } + + masks.Rows = make([]tableMask4, dummyAddr.BitLen()+1) + for i := 0; i <= dummyAddr.BitLen(); i++ { + if pfx, err = dummyAddr.Prefix(i); err != nil { + return + } + dummyNet = netipx.PrefixIPNet(pfx.Masked()) + masks.Rows[i] = tableMask4{ + Prefix: uint8(i), + Netmask: netsplit.MaskFmt( + dummyNet.Mask, + "d", ".", "", + 1, 0, + ), + Hex: dummyNet.Mask.String(), + Dec: binary.BigEndian.Uint32(dummyNet.Mask), + Bin: netsplit.MaskFmt( + dummyNet.Mask, + "08b", ".", "", + 1, 0, + ), + Mask: dummyNet.Mask, + } + } + + // Now the sizer. + for _, mask := range masks.Rows { + l = len(strconv.Itoa(int(mask.Prefix))) + if int(masks.Sizer.Prefix) < l { + masks.Sizer.Prefix = uint8(l) + } + l = len(mask.Netmask) + if int(masks.Sizer.Netmask) < l { + masks.Sizer.Netmask = uint8(l) + } + l = len(mask.Hex) + if int(masks.Sizer.Hex) < l { + masks.Sizer.Hex = uint8(l) + } + l = len(strconv.FormatUint(uint64(mask.Dec), 10)) + if int(masks.Sizer.Dec) < l { + masks.Sizer.Dec = uint8(l) + } + l = len(mask.Bin) + if int(masks.Sizer.Bin) < l { + masks.Sizer.Bin = uint8(l) + } + } + + return +} + +// do not include in template funcs; used externally +func hdrRender(hdrVal reflect.Value, indent string, plain bool) (out string) { + + var val reflect.Value + var field reflect.StructField + var fieldVal reflect.Value + var colLen uint8 + var colTitle string + var lastField int + var valType reflect.Type + var tfmt *tableFormatter = tblFmts[plain] + var sb *strings.Builder = new(strings.Builder) + + val = hdrVal + valType = val.Type() + + // Avoid the edge case where a struct's last field is skipped rendering + for i := val.NumField(); i > 0; i-- { + field = valType.Field(i - 1) + if field.Tag.Get("render") == "-" { + continue + } + lastField = i + break + } + + // Top-most line. + sb.WriteString(indent) + sb.WriteString(tfmt.TopLeftHdr) + for i := 0; i < val.NumField(); i++ { + field = valType.Field(i) + if field.Tag.Get("render") == "-" { + continue + } + fieldVal = val.Field(i) + colLen = uint8(fieldVal.Uint()) + sb.WriteString(strings.Repeat(tfmt.TopFillHdr, int(colLen)+fixedPad)) + if i == lastField { + sb.WriteString(tfmt.TopRightHdr) + } else { + sb.WriteString(tfmt.TopColSepHdr) + } + } + sb.WriteString("\n") + + // Column titles + sb.WriteString(indent) + sb.WriteString(tfmt.Left) + for i := 0; i < val.NumField(); i++ { + field = valType.Field(i) + if field.Tag.Get("render") == "-" { + continue + } + fieldVal = val.Field(i) + colLen = uint8(fieldVal.Uint()) + uint8(fixedPad) + colTitle = field.Name + if !tfmt.NoUpperTitle { + colTitle = strings.ToUpper(colTitle) + } + if !tfmt.NoBoldTitle { + sb.WriteString(color.InBold(padStr(colTitle, colLen))) + } else { + sb.WriteString(padStr(colTitle, colLen)) + } + if i == lastField { + sb.WriteString(tfmt.Right) + } else { + sb.WriteString(tfmt.ColSepHdr) + } + } + sb.WriteString("\n") + + // Header bottom line; headers always include bottom separators. + sb.WriteString(indent) + sb.WriteString(tfmt.BottomLeftHdr) + for i := 0; i < val.NumField(); i++ { + field = valType.Field(i) + if field.Tag.Get("render") == "-" { + continue + } + fieldVal = val.Field(i) + colLen = uint8(fieldVal.Uint()) + sb.WriteString(strings.Repeat(tfmt.BottomFillHdr, int(colLen)+fixedPad)) + if i == lastField { + sb.WriteString(tfmt.BottomRightHdr) + } else { + sb.WriteString(tfmt.BottomColSepHdr) + } + } + sb.WriteString("\n") + + out = sb.String() + + return +} + +// do not include in template funcs; used externally +func hdrLineRender(hdrVal reflect.Value, indent string, plain bool, rowIdx int, numRows int) (out string) { + + var val reflect.Value + var field reflect.StructField + var fieldVal reflect.Value + var colLen uint8 + var lastField int + var isLastLine bool + var valType reflect.Type + var tfmt *tableFormatter = tblFmts[plain] + var sb *strings.Builder = new(strings.Builder) + + isLastLine = rowIdx == (numRows - 1) + if !isLastLine && tfmt.SuppressLineSep { + return + } + + val = hdrVal + valType = val.Type() + lastField = valType.NumField() - 1 + + for i := val.NumField(); i >= 0; i-- { + field = valType.Field(i - 1) + if field.Tag.Get("render") == "-" { + continue + } + lastField = i + break + } + + sb.WriteString(indent) + if isLastLine { + sb.WriteString(tfmt.LastLeft) + } else { + sb.WriteString(tfmt.LineLeft) + } + for i := 0; i < val.NumField(); i++ { + field = valType.Field(i) + if field.Tag.Get("render") == "-" { + continue + } + fieldVal = val.Field(i) + colLen = uint8(fieldVal.Uint()) + if isLastLine { + sb.WriteString(strings.Repeat(tfmt.LastFill, int(colLen)+fixedPad)) + } else { + sb.WriteString(strings.Repeat(tfmt.Fill, int(colLen)+fixedPad)) + } + if i == lastField { + if isLastLine { + sb.WriteString(tfmt.LastRight) + } else { + sb.WriteString(tfmt.LineRight) + } + } else { + if isLastLine { + sb.WriteString(tfmt.LastSep) + } else { + sb.WriteString(tfmt.LineColSep) + } + } + } + sb.WriteString("\n") + + out = sb.String() + + return +} + +// do not include in template funcs; used externally +func rowRender(val reflect.Value, sizerVal reflect.Value, indent string, plain bool) (out string) { + + var field reflect.StructField + var fieldVal reflect.Value + var colLen uint8 + var sizerName string + var sizerField reflect.Value + var callVal string + var valType reflect.Type = val.Type() + var tfmt *tableFormatter = tblFmts[plain] + var sb *strings.Builder = new(strings.Builder) + + sb.WriteString(indent) + for i := 0; i < val.NumField(); i++ { + field = valType.Field(i) + if field.Tag.Get("render") == "-" { + continue + } + sb.WriteString(tfmt.Left) + fieldVal = val.Field(i) + sizerName = field.Tag.Get("renderSizeName") + if sizerName == "" { + sizerName = field.Name + } + sizerField = sizerVal.FieldByName(sizerName) + colLen = uint8(sizerField.Uint()) + uint8(fixedPad) + switch fieldVal.Kind() { + // This is tailored specifically to this implementation. + case reflect.String: + sb.WriteString(fieldVal.String()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + sb.WriteString(padStr(fmt.Sprintf("%d", fieldVal.Int()), colLen)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + sb.WriteString(padStr(fmt.Sprintf("%d", fieldVal.Uint()), colLen)) + case reflect.Ptr: + // It's a *big.Int. + if fieldVal.IsNil() { + sb.WriteString(padStr(strings.Repeat(padChars, int(colLen)), colLen)) + } else { + // TIL you can even *do* this in reflection. + fieldVal = fieldVal.MethodByName("String").Call(nil)[0] + callVal = fieldVal.String() + sb.WriteString(padStr(callVal, colLen)) + } + } + } + sb.WriteString("\n") + + out = sb.String() + + return +} diff --git a/cmd/subnetter/main.go b/cmd/subnetter/main.go index 951941a..b8a29c1 100644 --- a/cmd/subnetter/main.go +++ b/cmd/subnetter/main.go @@ -3,14 +3,17 @@ package main import ( "bytes" "errors" - "go4.org/netipx" + `fmt` "io" "log" "net" "net/netip" "os" "strings" + + "go4.org/netipx" "subnetter/netsplit" + `subnetter/version` "github.com/jessevdk/go-flags" "r00t2.io/sysutils/paths" @@ -29,8 +32,10 @@ func main() { var remaining *netipx.IPSet var buf *bytes.Buffer var res *netsplit.StructuredResults - var splitErr *netsplit.SplitErr = new(netsplit.SplitErr) - var parser *flags.Parser = flags.NewParser(args, flags.Default) + var noStrict bool + var strictErr error + var splitErr = new(netsplit.SplitErr) + var parser = flags.NewParser(args, flags.Default) if _, err = parser.Parse(); err != nil { switch flagsErr := err.(type) { @@ -46,12 +51,30 @@ func main() { } } + if version.Ver, err = version.Version(); err != nil { + log.Panicln(err) + } + + // If args.Version or args.DetailVersion are true, just print them and exit. + if args.DetailVersion || args.Version { + if args.Version { + fmt.Println(version.Ver.Short()) + return + } else if args.DetailVersion { + fmt.Println(version.Ver.Detail()) + return + } + } + switch parser.Active.Name { case "table": - // TODO: print table and exit + buf = new(bytes.Buffer) + if err = tblTpl.ExecuteTemplate(buf, "table.tpl", args.Table.tableOpts); err != nil { + log.Panicln(err) + } + os.Stdout.Write(buf.Bytes()) return case "parse": - // TODO: parse file/bytes, unmarshal, and render with new options then exit if strings.TrimSpace(args.Parse.InFile) == "-" { buf = new(bytes.Buffer) if _, err = io.Copy(buf, os.Stdin); err != nil { @@ -102,26 +125,32 @@ func main() { } cmnArgs = args.SplitHost.common splitter = &netsplit.HostSplitter{ - BaseSplitter: new(netsplit.BaseSplitter), NumberHosts: args.SplitHost.Hosts, + Strict: args.SplitHost.Strict, + BaseSplitter: new(netsplit.BaseSplitter), } + noStrict = !args.SplitHost.Strict + strictErr = netsplit.ErrBadNumHosts case "split-nets": if err = validate.Struct(args.SplitSubnets); err != nil { log.Panicln(err) } cmnArgs = args.SplitSubnets.common splitter = &netsplit.SubnetSplitter{ - BaseSplitter: new(netsplit.BaseSplitter), + Strict: args.SplitSubnets.Strict, NumberSubnets: args.SplitSubnets.NumNets, + BaseSplitter: new(netsplit.BaseSplitter), } + noStrict = !args.SplitSubnets.Strict + strictErr = netsplit.ErrNoNetSpace case "split-cidr": if err = validate.Struct(args.SplitCIDR); err != nil { log.Panicln(err) } cmnArgs = args.SplitCIDR.common splitter = &netsplit.CIDRSplitter{ - BaseSplitter: new(netsplit.BaseSplitter), PrefixLength: args.SplitCIDR.Prefix, + BaseSplitter: new(netsplit.BaseSplitter), } case "vlsm": if err = validate.Struct(args.VLSM); err != nil { @@ -129,9 +158,9 @@ func main() { } cmnArgs = args.VLSM.common splitter = &netsplit.VLSMSplitter{ - BaseSplitter: new(netsplit.BaseSplitter), Ascending: args.VLSM.Asc, PrefixLengths: args.VLSM.Sizes, + BaseSplitter: new(netsplit.BaseSplitter), } } if origPfx, err = netip.ParsePrefix(cmnArgs.Network.Network); err != nil { @@ -147,8 +176,12 @@ func main() { splitter.SetParent(*pfx) if nets, remaining, err = splitter.Split(); err != nil { if errors.As(err, &splitErr) { - printSplitErr(splitErr) - os.Exit(1) + if noStrict && errors.Is(splitErr.Wrapped, strictErr) { + err = nil + } else { + printSplitErr(splitErr) + os.Exit(1) + } } else { log.Panicln(err) } diff --git a/cmd/subnetter/types.go b/cmd/subnetter/types.go index 24cfc73..0ca8001 100644 --- a/cmd/subnetter/types.go +++ b/cmd/subnetter/types.go @@ -1,8 +1,134 @@ package main import ( + `math/big` + `net` "net/netip" ) // subnetResult is only used for human/"pretty" printing. type subnetResult netip.Prefix + +type tableOpts struct { + Plain bool `short:"p" long:"plain" description:"Show plain table output."` + Legacy bool `short:"l" long:"legacy" description:"Include legacy/obsolete/deprecated information."` + NoV4Mask bool `short:"M" long:"no-mask" description:"Do not include netmasks for IPv4."` + NoIpv6 bool `short:"4" long:"ipv4" description:"Only show IPv4 table(s)."` + NoIpv4 bool `short:"6" long:"ipv6" description:"Only show IPv6 table(s)."` +} + +type tableAddrRet struct { + Sizer *tableAddrSizer + Rows []tableAddr +} + +type tableAddr struct { + Prefix uint8 + Bits uint8 + Addresses *big.Int + Hosts *big.Int + NetPrefix netip.Prefix `render:"-"` +} + +// tableAddrSizer is used to control spacing/sizing of a tableAddr table's columns. +type tableAddrSizer struct { + Prefix uint8 + Bits uint8 + Addresses uint8 + Hosts uint8 +} + +type tableMask4Ret struct { + Sizer *tableMask4Sizer + Rows []tableMask4 +} + +// tableMask4 is used to hold string representation of netmask information. +type tableMask4 struct { + Prefix uint8 + Netmask string + Hex string + Dec uint32 + Bin string + Mask net.IPMask `render:"-"` +} + +// tableMask4Sizer, like tableAddrSizer, is used to control spacing/sizing of a tableMask4 table's columns. +type tableMask4Sizer struct { + Prefix uint8 + Netmask uint8 + Hex uint8 + Dec uint8 + Bin uint8 +} + +type tableLegacy4Ret struct { + Sizer *tableLegacy4Sizer + Rows []tableLegacy4 +} + +// tableLegacy4 contains a spec for a class in the legacy "classed" IPv4 networking. +type tableLegacy4 struct { + Class string + CIDR string + Start string + End string + NetStart netip.Addr `render:"-"` + NetEnd netip.Addr `render:"-"` + NetCIDR netip.Prefix `render:"-"` +} + +// tableLegacy4Sizer is used to size tableLegacy4 entries. +type tableLegacy4Sizer struct { + Class uint8 + CIDR uint8 + Start uint8 + End uint8 +} + +// tableFormatter is used for "rendering" table output. +type tableFormatter struct { + // Headers... + // First char, first line + TopLeftHdr string + // Char to fill between TopLeftHdr and TopColSepHdr. + TopFillHdr string + // Column separator, first line + TopColSepHdr string + // Last char, first line + TopRightHdr string + // Column separator for both column separators (title line) and left/right-most lines. + ColSepHdr string + // First char, last line of header + BottomLeftHdr string + // Char to fill between BottomLeftHdr and BottomColSepHdr. + BottomFillHdr string + // Column separator, last line of header + BottomColSepHdr string + // Last char, last line of header + BottomRightHdr string + // Rows... + // Left-most line (border). + Left string + // Fill the cell lines for values + Fill string + // Separate line/cell border columns + LineColSep string + // "In-between" rows; left-most. + LineLeft string + // "In-between" rows, right-most. + LineRight string + // Separate value columns + ColSep string + // Right-most line (border) + Right string + // Last lines get special treatment as they are also a border. + LastLeft string + LastFill string + LastSep string + LastRight string + // This is mostly for experimentation for Plain output, but... + SuppressLineSep bool + NoUpperTitle bool + NoBoldTitle bool +} diff --git a/go.mod b/go.mod index eef2685..185f214 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,13 @@ go 1.23.2 toolchain go1.23.5 require ( + github.com/TwiN/go-color v1.4.1 github.com/go-playground/validator/v10 v10.24.0 github.com/goccy/go-yaml v1.15.16 github.com/jessevdk/go-flags v1.6.1 github.com/projectdiscovery/mapcidr v1.1.34 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba + golang.org/x/mod v0.22.0 r00t2.io/sysutils v1.12.0 ) @@ -19,17 +21,17 @@ require ( github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/gorilla/css v1.0.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/microcosm-cc/bluemonday v1.0.25 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/projectdiscovery/blackrock v0.0.1 // indirect - github.com/projectdiscovery/utils v0.0.85 // indirect + github.com/projectdiscovery/utils v0.4.8 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - r00t2.io/goutils v1.7.1 // indirect + r00t2.io/goutils v1.7.2 // indirect ) diff --git a/go.sum b/go.sum index 76a4117..e73aa3c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= +github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -18,14 +20,14 @@ github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1 github.com/goccy/go-yaml v1.15.16 h1:PMTVcGI9uNPIn7KLs0H7KC1rE+51yPl5YNh4i8rGuRA= github.com/goccy/go-yaml v1.15.16/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= -github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -34,8 +36,8 @@ github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= github.com/projectdiscovery/mapcidr v1.1.34 h1:udr83vQ7oz3kEOwlsU6NC6o08leJzSDQtls1wmXN/kM= github.com/projectdiscovery/mapcidr v1.1.34/go.mod h1:1+1R6OkKSAKtWDXE9RvxXtXPoajXTYX0eiEdkqlhQqQ= -github.com/projectdiscovery/utils v0.0.85 h1:JpCVc9GJwJLNHy1MBPmAHJcE6rs7bRv72Trb3u84OHE= -github.com/projectdiscovery/utils v0.0.85/go.mod h1:ttiPgS2LmLFd+VRBUdgfLKMMdrF98zX7z5W+K71MX40= +github.com/projectdiscovery/utils v0.4.8 h1:/Xd38fP8xc6kifZayjrhcYALenJrjO3sHO7lg+I8ZGk= +github.com/projectdiscovery/utils v0.4.8/go.mod h1:S314NzLcXVCbLbwYCoorAJYcnZEwv7Uhw2d3aF5fJ4s= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -44,6 +46,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= @@ -56,8 +60,8 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -r00t2.io/goutils v1.7.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc= -r00t2.io/goutils v1.7.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= +r00t2.io/goutils v1.7.2 h1:dJ+pzY/U1yVi2V6eKoxe/4roM+Tb3d0umMEL9Dx4+Lw= +r00t2.io/goutils v1.7.2/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= r00t2.io/sysutils v1.12.0 h1:Ce3qUOyLixE1ZtFT/+SVwOT5kSkzg5+l1VloGeGugrU= r00t2.io/sysutils v1.12.0/go.mod h1:bNTKNBk9MnUhj9coG9JBNicSi5FrtJHEM645um85pyw= diff --git a/netsplit/errs.go b/netsplit/errs.go index 690e5c9..b0a612e 100644 --- a/netsplit/errs.go +++ b/netsplit/errs.go @@ -4,6 +4,7 @@ import "errors" var ( ErrBadBoundary error = errors.New("subnet does not align on bit boundary") + ErrBadNumHosts error = errors.New("bad number of hosts; cannot split into prefix exactly") ErrBadPrefix error = errors.New("prefix is invalid") ErrBadPrefixLen error = errors.New("prefix length exceeds maximum possible for prefix's inet family") ErrBadSplitter error = errors.New("invalid or unknown splitter when containing") diff --git a/netsplit/funcs.go b/netsplit/funcs.go index 609cc75..f599c26 100644 --- a/netsplit/funcs.go +++ b/netsplit/funcs.go @@ -4,11 +4,12 @@ import ( "encoding/json" "encoding/xml" "fmt" - "github.com/goccy/go-yaml" - "go4.org/netipx" "net" "net/netip" "strings" + + "github.com/goccy/go-yaml" + "go4.org/netipx" ) /* @@ -96,9 +97,9 @@ Set as 0 or `segSep` to an empty string to do no string segmentation. func AddrFmt(ip netip.Addr, f, sep, segSep string, every, everySeg uint) (s string) { var numSegs int - var doSep bool = every > 0 - var fs string = "%" + f - var sb *strings.Builder = new(strings.Builder) + var doSep = every > 0 + var fs = "%" + f + var sb = new(strings.Builder) if ip.IsUnspecified() || !ip.IsValid() { return @@ -152,7 +153,7 @@ func AddrInvert(ip netip.Addr) (inverted netip.Addr) { func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSet, splitter NetSplitter) (s *StructuredResults, err error) { var rem []netip.Prefix - var sr StructuredResults = StructuredResults{ + var sr = StructuredResults{ Original: origPfx, } @@ -243,9 +244,9 @@ Its parameters hold the same significance as in AddrFmt. func MaskFmt(mask net.IPMask, f, sep, segSep string, every, everySeg uint) (s string) { var numSegs int - var doSep bool = every > 0 - var fs string = "%" + f - var sb *strings.Builder = new(strings.Builder) + var doSep = every > 0 + var fs = "%" + f + var sb = new(strings.Builder) if mask == nil || len(mask) == 0 { return diff --git a/netsplit/funcs_cidrsplitter.go b/netsplit/funcs_cidrsplitter.go index 0009187..19d8aca 100644 --- a/netsplit/funcs_cidrsplitter.go +++ b/netsplit/funcs_cidrsplitter.go @@ -1,14 +1,58 @@ package netsplit import ( - "go4.org/netipx" "net/netip" + + "go4.org/netipx" ) -// Split splits the network defined in a CIDRSplitter alongside its configuration and performs the subnetting. +/* + Split splits the network defined in a CIDRSplitter alongside its configuration and performs the subnetting. + This strategy attempts to split a network into subnets of a single uniform explicit size. +*/ func (c *CIDRSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) { - // TODO + var ok bool + var base netip.Prefix + var sub netip.Prefix + var subPtr *netip.Prefix + var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + + if c == nil || c.PrefixLength == 0 || c.BaseSplitter == nil || c.network == nil { + return + } + + if base, ok = netipx.FromStdIPNet(c.network); !ok { + err = ErrBadBoundary + return + } + if !base.IsValid() { + err = ErrBadBoundary + return + } + + if c.PrefixLength > uint8(base.Bits()) { + err = ErrBigPrefix + return + } + + ipsb.AddPrefix(base) + if remaining, err = ipsb.IPSet(); err != nil { + return + } + + for { + if sub, remaining, ok = remaining.RemoveFreePrefix(c.PrefixLength); !ok { + if !sub.IsValid() { + // No error; it's literally impossible since we network on boundaries. + // We just hit the end of the prefix. + break + } + subPtr = new(netip.Prefix) + *subPtr = sub + nets = append(nets, subPtr) + } + } return } diff --git a/netsplit/funcs_hostsplitter.go b/netsplit/funcs_hostsplitter.go index 40f2d08..e33b793 100644 --- a/netsplit/funcs_hostsplitter.go +++ b/netsplit/funcs_hostsplitter.go @@ -1,14 +1,68 @@ package netsplit import ( - "go4.org/netipx" + `math/big` + `net` "net/netip" + + `github.com/projectdiscovery/mapcidr` + "go4.org/netipx" ) -// Split splits the network defined in a HostSplitter alongside its configuration and performs the subnetting. +/* + Split splits the network defined in a HostSplitter alongside its configuration and performs the subnetting. + This strategy attempts to split the network into subnets of equal number of hosts. + + remaining may or may not be nil depending on if the number of hosts can fit cleanly within equal network sizes on boundaries. + + An ErrBadNumHosts will be returned if the number of hosts does not match the *addressable* range in a prefix. +*/ func (h *HostSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) { - // TODO + var tgt *big.Int + var hosts *big.Int + var sub netip.Prefix + var subPtr *netip.Prefix + var split []*net.IPNet + var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + + if h == nil || h.NumberHosts == 0 || h.BaseSplitter == nil || h.network == nil { + return + } + + if split, err = mapcidr.SplitIPNetByNumber(h.network, int(h.NumberHosts)); err != nil { + return + } + + tgt = big.NewInt(0) + tgt.SetUint64(uint64(h.NumberHosts)) + + nets = make([]*netip.Prefix, len(split)) + for idx, n := range split { + sub, _ = netipx.FromStdIPNet(n) + hosts = mapcidr.CountIPsInCIDR(false, false, n) + if hosts == nil || tgt.Cmp(hosts) != 0 { + err = &SplitErr{ + Wrapped: ErrBadNumHosts, + Nets: nets, + Remaining: remaining, + LastSubnet: &sub, + RequestedPrefixLen: uint8(sub.Bits()), + } + ipsb.AddPrefix(sub) + } else { + subPtr = new(netip.Prefix) + *subPtr = sub + nets = append(nets, subPtr) + } + + nets[idx] = new(netip.Prefix) + *nets[idx] = sub + } + + if remaining, err = ipsb.IPSet(); err != nil { + return + } return } diff --git a/netsplit/funcs_structuredresults.go b/netsplit/funcs_structuredresults.go index 0f8da06..27ef017 100644 --- a/netsplit/funcs_structuredresults.go +++ b/netsplit/funcs_structuredresults.go @@ -1,8 +1,9 @@ package netsplit import ( - "go4.org/netipx" "net/netip" + + "go4.org/netipx" ) /* diff --git a/netsplit/funcs_subnetsplitter.go b/netsplit/funcs_subnetsplitter.go index 68507db..8816e6e 100644 --- a/netsplit/funcs_subnetsplitter.go +++ b/netsplit/funcs_subnetsplitter.go @@ -1,14 +1,106 @@ package netsplit import ( - "go4.org/netipx" + `net` "net/netip" + + `github.com/projectdiscovery/mapcidr` + "go4.org/netipx" ) -// Split splits the network defined in a SubnetSplitter alongside its configuration and performs the subnetting. +/* + Split splits the network defined in a SubnetSplitter alongside its configuration and performs the subnetting. + This strategy allows for splitting a network into exactly evenly sized specified number of subnets. + + remaining may or may not be nil depending on if the specified number of subnets fit cleanly into the network boundaries. + + An ErrNoNetSpace error will be returned if subnetting size exhaustion occurs before the specified number of subnets is reached + (but nets will be populated and remaining will contain any left over subnets). +*/ func (s *SubnetSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) { - // TODO + var ok bool + var pfxLen int + var base netip.Prefix + var sub netip.Prefix + var subPtr *netip.Prefix + var split []*net.IPNet + var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + + if s == nil || s.BaseSplitter == nil || s.network == nil || s.NumberSubnets == 0 { + return + } + + if base, ok = netipx.FromStdIPNet(s.network); !ok { + err = ErrBadBoundary + return + } + if !base.IsValid() { + err = ErrBadBoundary + return + } + + if split, err = mapcidr.SplitIPNetIntoN(s.network, int(s.NumberSubnets)); err != nil { + return + } + + for _, n := range split { + if sub, ok = netipx.FromStdIPNet(n); !ok { + // We bail early on this error. + err = &SplitErr{ + Wrapped: ErrBadBoundary, + Nets: nets, + Remaining: remaining, + LastSubnet: subPtr, + RequestedPrefixLen: 0, + } + err = ErrBadBoundary + return + } + if sub.String() == base.String() { + continue + } + if pfxLen == 0 { + pfxLen = sub.Bits() + if nets == nil { + nets = make([]*netip.Prefix, 0) + } + subPtr = new(netip.Prefix) + *subPtr = sub + nets = append(nets, subPtr) + } else { + if sub.Bits() != pfxLen { + if err == nil { + // Return this err but don't return early; wait for the populate. + err = &SplitErr{ + Wrapped: ErrNoNetSpace, + Nets: nets, + Remaining: remaining, + LastSubnet: subPtr, + RequestedPrefixLen: uint8(pfxLen), + } + } + ipsb.AddPrefix(sub) + } else { + subPtr = new(netip.Prefix) + *subPtr = sub + nets = append(nets, subPtr) + } + } + } + + if remaining, err = ipsb.IPSet(); err != nil { + return + } + + if len(nets) < int(s.NumberSubnets) { + err = &SplitErr{ + Wrapped: ErrNoNetSpace, + Nets: nets, + Remaining: remaining, + } + return + } return } diff --git a/netsplit/funcs_vlsmsplitter.go b/netsplit/funcs_vlsmsplitter.go index 5a344fa..005e8a8 100644 --- a/netsplit/funcs_vlsmsplitter.go +++ b/netsplit/funcs_vlsmsplitter.go @@ -1,12 +1,18 @@ package netsplit import ( - "go4.org/netipx" "net/netip" "sort" + + "go4.org/netipx" ) -// Split splits the network defined in a VLSMSplitter alongside its configuration and performs the subnetting. +/* + Split splits the network defined in a VLSMSplitter alongside its configuration and performs the subnetting. + This strategy allows for multiple subnets of differing sizes to be specified. + + remaining may or may not be nil depending on if all desired subnet sizes fit cleanly into the network boundaries. +*/ func (v *VLSMSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) { var ok bool @@ -15,7 +21,7 @@ func (v *VLSMSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e var base netip.Prefix var sub netip.Prefix var subPtr *netip.Prefix - var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + var ipsb = new(netipx.IPSetBuilder) if err = ValidateSizes(v.network, v.PrefixLengths...); err != nil { return diff --git a/netsplit/init.go b/netsplit/init.go new file mode 100644 index 0000000..c02b75c --- /dev/null +++ b/netsplit/init.go @@ -0,0 +1,3 @@ +package netsplit + +// TODO? diff --git a/netsplit/types.go b/netsplit/types.go index d3e6836..d5771e3 100644 --- a/netsplit/types.go +++ b/netsplit/types.go @@ -2,9 +2,10 @@ package netsplit import ( "encoding/xml" - "go4.org/netipx" "net" "net/netip" + + "go4.org/netipx" ) // SplitErr is used to wrap an error with context surrounding when/how that error was encountered. @@ -48,7 +49,9 @@ It attempts to evenly distribute addresses amoungs subnets. */ type HostSplitter struct { // NumberHosts is the number of hosts to be placed in each subnet to split out. - NumberHosts uint `json:"hosts" xml:"hosts,attr" yaml:"Number of Hosts Per Subnet"` + NumberHosts uint `json:"hosts" xml:"hosts,attr" yaml:"Number of Hosts Per Subnet"` + // Strict, if true, will return an error from Split if the network cannot split into subnets of NumberHosts-addressable networks exactly. + Strict bool `json:"strict" xml:"strict,attr,omitempty" yaml:"Strictly Equal Hosts Per Subnet"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"` } @@ -59,6 +62,8 @@ as cleanly as poossible. type SubnetSplitter struct { // NumberSubnets indicates the number of subnets to split the network into. NumberSubnets uint `json:"nets" xml:"nets,attr" yaml:"Number of Target Subnets"` + // Strict, if true, will return an error from Split if the network sizes cannot split into equally-sized networks. + Strict bool `json:"strict" xml:"strict,attr,omitempty" yaml:"Strictly Equal Subnet Sizes"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"` } diff --git a/version/consts.go b/version/consts.go new file mode 100644 index 0000000..cad435d --- /dev/null +++ b/version/consts.go @@ -0,0 +1,63 @@ +/* + * BSD 3-Clause License + * Copyright (c) 2024, NetFire™ + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package version + +import ( + "regexp" +) + +/* +These variables are automatically handled by the build script. + +DO NOT MODIFY THESE VARIABLES. +Refer to /build.sh for how these are generated at build time and populated. +*/ +var ( + sourceControl string = "git" + repoUri string = "(unknown)" + version string = "(unknown)" + commitHash string + commitShort string + numCommitsAfterTag string + isDirty string + buildTime string + buildUser string + buildSudoUser string + buildHost string +) + +var ( + patchRe *regexp.Regexp = regexp.MustCompile(`^(?P[0-9+])(?P
-[0-9A-Za-z.-]+)?(?P\+[0-9A-Za-z.-]+)?$`)
+	patchReIsolated *regexp.Regexp = regexp.MustCompile(`^([0-9]+)(?:[-+](.*)?)?$`)
+)
+
+// Ver is populated by main() from the build script and used in other places.
+var Ver *BuildInfo
diff --git a/version/funcs.go b/version/funcs.go
new file mode 100644
index 0000000..c295f58
--- /dev/null
+++ b/version/funcs.go
@@ -0,0 +1,180 @@
+/*
+ * BSD 3-Clause License
+ * Copyright (c) 2024, NetFire™ 
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ *  are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ *  may be used to endorse or promote products derived from this software without
+ *  specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ *  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ *  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ *  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package version
+
+import (
+	`fmt`
+	`runtime`
+	`strconv`
+	`strings`
+	`time`
+
+	`golang.org/x/mod/semver`
+)
+
+// Version returns the build information. See build.sh.
+func Version() (b *BuildInfo, err error) {
+
+	var n int
+	var s string
+	var sb strings.Builder
+	var ok bool
+	var canonical string
+	var build strings.Builder
+	// Why a map?
+	// I forget but I had a reason for it once upon a time.
+	// If you remove it and this func breaks, now you know why.
+	// TODO: how much of this can be replaced by (runtime/debug).ReadBuildInfo()?
+	var raw map[string]string = map[string]string{
+		"repoUri":        repoUri,
+		"sourceControl":  sourceControl,
+		"tag":            version,
+		"hash":           commitHash,
+		"shortHash":      commitShort,
+		"postTagCommits": numCommitsAfterTag,
+		"dirty":          isDirty,
+		"time":           buildTime,
+		"user":           buildUser,
+		"sudoUser":       buildSudoUser,
+		"host":           buildHost,
+	}
+	var i BuildInfo = BuildInfo{
+		SourceControl: raw["sourceControl"],
+		GoVersion:     runtime.Version(),
+		RepoURI:       raw["repoUri"],
+		TagVersion:    raw["tag"],
+		// PostTagCommits: 0,
+		CommitHash:    raw["hash"],
+		CommitId:      raw["shortHash"],
+		BuildUser:     raw["user"],
+		RealBuildUser: raw["sudoUser"],
+		// BuildTime:     time.Time{},
+		BuildHost: raw["host"],
+		Dirty:     false,
+		isDefined: false,
+		raw:       raw,
+	}
+
+	if s, ok = raw["postTagCommits"]; ok && strings.TrimSpace(s) != "" {
+		if n, err = strconv.Atoi(s); err == nil {
+			i.PostTagCommits = uint(n)
+		}
+	}
+
+	if s, ok = raw["time"]; ok && strings.TrimSpace(s) != "" {
+		if n, err = strconv.Atoi(s); err == nil {
+			i.BuildTime = time.Unix(int64(n), 0).UTC()
+		}
+	}
+
+	switch strings.ToLower(raw["dirty"]) {
+	case "1":
+		i.Dirty = true
+	case "0", "":
+		i.Dirty = false
+	}
+
+	// Build the short form. We use this for both BuildInfo.short and BuildInfo.verSplit.
+	if i.TagVersion == "" {
+		sb.WriteString(i.SourceControl)
+	} else {
+		sb.WriteString(i.TagVersion)
+	}
+	/*
+		Now the mess. In order to conform to SemVer 2.0 (the spec this code targets):
+
+		1.) MAJOR.
+		2.) MINOR.
+		3.) PATCH
+		4.) -PRERELEASE (OPTIONAL)
+			(git commit, if building against a commit made past 1-3. Always included if untagged.)
+		5.) +BUILDINFO (OPTIONAL)
+			("+x[.y]", where x is # of commits past 4, or tag commit if 4 is empty. 0 is valid.
+			 y is optional, and is the string "dirty" if it is a "dirty" build - that is, uncommitted/unstaged changes.
+			 if x and y would be 0 and empty, respectively, then 5 is not included.)
+
+		1-3 are already written, or the source control software used if not.
+
+		Technically 4 and 5 are only included if 3 is present. We force patch to 0 if it's a tagged release and patch isn't present --
+		so this is not relevant.
+	*/
+	// PRERELEASE
+	if i.TagVersion == "" || i.PostTagCommits > 0 {
+		// We use the full commit hash for git versions, short identifier for tagged releases.
+		if i.TagVersion == "" {
+			i.Pre = i.CommitHash
+		} else {
+			i.Pre = i.CommitId
+		}
+		sb.WriteString(fmt.Sprintf("-%v", i.Pre))
+	}
+	// BUILD
+	if i.PostTagCommits > 0 || i.Dirty {
+		build.WriteString(strconv.Itoa(int(i.PostTagCommits)))
+		if i.Dirty {
+			build.WriteString(".dirty")
+		}
+		i.Build = build.String()
+		sb.WriteString(fmt.Sprintf("+%v", i.Build))
+	}
+
+	i.short = sb.String()
+	if semver.IsValid(i.short) {
+		// DON'T DO THIS. It strips the prerelease and build info.
+		// i.short = semver.Canonical(i.short)
+		// Do this instead.
+		canonical = semver.Canonical(i.short)
+		// Numeric versions...
+		if n, err = strconv.Atoi(strings.TrimPrefix(semver.Major(canonical), "v")); err != nil {
+			err = nil
+		} else {
+			i.Major = uint(n)
+		}
+		if n, err = strconv.Atoi(strings.Split(semver.MajorMinor(canonical), ".")[1]); err != nil {
+			err = nil
+		} else {
+			i.Minor = uint(n)
+		}
+		if n, err = strconv.Atoi(patchReIsolated.FindStringSubmatch(strings.Split(canonical, ".")[2])[1]); err != nil {
+			err = nil
+		} else {
+			i.Patch = uint(n)
+		}
+		// The other tag assignments were performed above.
+	}
+	// The default is 0 for the numerics, so no big deal.
+
+	i.isDefined = true
+
+	b = &i
+
+	return
+}
diff --git a/version/funcs_buildinfo.go b/version/funcs_buildinfo.go
new file mode 100644
index 0000000..4e6728a
--- /dev/null
+++ b/version/funcs_buildinfo.go
@@ -0,0 +1,133 @@
+/*
+ * BSD 3-Clause License
+ * Copyright (c) 2024, NetFire™ 
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ *  are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ *  may be used to endorse or promote products derived from this software without
+ *  specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ *  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ *  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ *  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package version
+
+import (
+	`fmt`
+	`strings`
+
+	`golang.org/x/mod/semver`
+)
+
+// Detail returns a multiline string containing every possible piece of information we collect.
+func (b *BuildInfo) Detail() (ver string) {
+
+	var sb strings.Builder
+
+	sb.WriteString(fmt.Sprintf("%v\n\n", b.short))
+	sb.WriteString(fmt.Sprintf("====\nSource Control: %v\nRepo URI: %v\n", b.SourceControl, b.RepoURI))
+	if b.TagVersion != "" {
+		if b.PostTagCommits > 0 {
+			sb.WriteString(fmt.Sprintf("Version Base: %v\nCommit Hash: %v\n", b.TagVersion, b.CommitHash))
+		} else {
+			sb.WriteString(fmt.Sprintf("Version: %v\n", b.TagVersion))
+		}
+	} else {
+		sb.WriteString(fmt.Sprintf("Version: (Unversioned)\nCommit Hash: %v\n", b.CommitHash))
+	}
+
+	// Post-commits
+	if b.TagVersion != "" {
+		sb.WriteString(fmt.Sprintf("# of Commits Since %v: %v\n", b.TagVersion, b.PostTagCommits))
+	} else {
+		sb.WriteString(fmt.Sprintf("# of Commits Since %v: %v\n", b.CommitId, b.PostTagCommits))
+	}
+
+	sb.WriteString("Uncommitted/Unstaged Changes: ")
+	if b.Dirty {
+		sb.WriteString("yes (dirty/monkeypatched build)\n")
+	} else {
+		sb.WriteString("no (clean build)\n")
+	}
+
+	if b.TagVersion != "" {
+		sb.WriteString(
+			fmt.Sprintf(
+				"====\nMajor: %v\nMinor: %v\nPatch: %v\n",
+				b.Major, b.Minor, b.Patch,
+			),
+		)
+	}
+	sb.WriteString("====\n")
+	sb.WriteString(b.Meta())
+
+	ver = sb.String()
+
+	return
+}
+
+// Short returns a uniquely identifiable version string.
+func (b *BuildInfo) Short() (ver string) {
+
+	ver = b.short
+
+	return
+}
+
+// Meta returns the build/compile-time info.
+func (b *BuildInfo) Meta() (meta string) {
+
+	var sb strings.Builder
+
+	if b.RealBuildUser != b.BuildUser && b.RealBuildUser != "" {
+		sb.WriteString(fmt.Sprintf("Real Build User: %v\n", b.RealBuildUser))
+		sb.WriteString(fmt.Sprintf("Sudo Build User: %v\n", b.BuildUser))
+	} else {
+		sb.WriteString(fmt.Sprintf("Build User: %v\n", b.BuildUser))
+	}
+	sb.WriteString(fmt.Sprintf("Build Time: %v\nBuild Host: %v\nGo Version: %v\n", b.BuildTime, b.BuildHost, b.GoVersion))
+
+	meta = sb.String()
+
+	return
+}
+
+// getReMap gets a regex map of map[pattern]match.
+func (b *BuildInfo) getReMap() (matches map[string]string) {
+
+	var s string = b.Short()
+	var sections []string
+
+	if !semver.IsValid(s) {
+		return
+	}
+
+	sections = strings.Split(s, ".")
+
+	// The split should contain everything in the third element.
+	// Or, if using a "simplified" semver, the last element.
+	matches = make(map[string]string)
+	for idx, str := range patchRe.FindStringSubmatch(sections[len(sections)-1]) {
+		matches[patchRe.SubexpNames()[idx]] = str
+	}
+
+	return
+}
diff --git a/version/types.go b/version/types.go
new file mode 100644
index 0000000..4e4ba02
--- /dev/null
+++ b/version/types.go
@@ -0,0 +1,85 @@
+/*
+ * BSD 3-Clause License
+ * Copyright (c) 2024, NetFire™ 
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ *  are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its contributors
+ *  may be used to endorse or promote products derived from this software without
+ *  specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ *  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ *  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ *  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package version
+
+import (
+	`time`
+)
+
+// BuildInfo contains nativized version information.
+type BuildInfo struct {
+	// RepoURI is where the source is from.
+	RepoURI string
+	// GoVersion is the version of the Go compiler used.
+	GoVersion string
+	// TagVersion is the most recent tag name on the current branch.
+	TagVersion string
+	// PostTagCommits is the number of commits after BuildInfo.TagVersion's commit on the current branch.
+	PostTagCommits uint
+	// CommitHash is the full commit hash.
+	CommitHash string
+	// CommitId is the "short" version of BuildInfo.CommitHash.
+	CommitId string
+	// BuildUser is the user the program was compiled under.
+	BuildUser string
+	// If compiled under sudo, BuildInfo.RealBuildUser is the user that called sudo.
+	RealBuildUser string
+	// BuildTime is the time and date of the program's build time.
+	BuildTime time.Time
+	// BuildHost is the host the binary was compiled on.
+	BuildHost string
+	// Dirty specifies if the source was "dirty" (uncommitted/unstaged etc. files) at the time of compilation.
+	Dirty bool
+	// SourceControl is the source control version used. Only relevant if not a "clean" build or untagged.
+	SourceControl string
+	// Major is the major version, expressed as an uint per spec.
+	Major uint
+	// Minor is the minor version, expressed as an uint per spec.
+	Minor uint
+	// Patch is the patch version, expressed as an uint per spec.
+	Patch uint
+	// Pre
+	Pre string
+	// Build
+	Build string
+	// isDefined specifies if this version was retrieved from the built-in values.
+	isDefined bool
+	// raw is the raw variable values.
+	raw map[string]string
+	/*
+		verSplit is a slice of []string{Major, Minor, Patch, PreRelease, Build}
+
+		If using an actual point release, PreRelease and Build are probably blank.
+	*/
+	verSplit [5]string
+	// short is the condensed version of verSplit.
+	short string
+}