this is cool and all but the tables don't render properly

This commit is contained in:
2025-02-01 23:15:54 -05:00
parent b09cb83017
commit 3a7ed5973b
28 changed files with 1917 additions and 88 deletions

View File

@@ -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 }}

View File

@@ -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 {

View File

@@ -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 <fixed left pad> and <fixed right pad> == fixedPad/2.
*/
fixedPad int = 2
/*
padChars is what fills the pads in table cells.
At the *LEAST*, a cell will be "<fixedPad/2 * padChars><str><fixedPad/2 * padChars>"
*/
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[<is plain>]*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,
},
}
)

9
cmd/subnetter/errs.go Normal file
View File

@@ -0,0 +1,9 @@
package main
import (
`errors`
)
var (
errBadNet error = errors.New("bad inet/addr family/version")
)

View File

@@ -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,
`<?xml version="1.0" encoding="UTF-8"?>`+
"<!--\n"+
" Generated by subnetter.\n"+
"\n<!--\n"+
" Generated by subnetter %s\n"+
" %s\n"+
"-->\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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

447
cmd/subnetter/funcs_tpl.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}