package main import ( `encoding/binary` `fmt` `net` `net/netip` `reflect` `strconv` `strings` `github.com/TwiN/go-color` `github.com/projectdiscovery/mapcidr` `go4.org/netipx` `r00t2.io/subnetter/netsplit` ) // renderHdr renders a header. Note that the first line does *not* include the indent, but subsequent ones do. func renderHdr(titles []string, colSizes []uint8, indent string, plain bool, sb *strings.Builder) { var idx int var width uint8 var title string var lastTitleIdx int var tfmt *tableFormatter = tblFmts[plain] if titles == nil || len(titles) == 0 || colSizes == nil || len(colSizes) == 0 { return } lastTitleIdx = len(titles) - 1 // Top-most line/border. sb.WriteString(tfmt.TopLeftHdr) for idx, width = range colSizes { sb.WriteString(strings.Repeat(tfmt.TopFillHdr, int(width))) if idx == lastTitleIdx { sb.WriteString(tfmt.TopRightHdr) } else { sb.WriteString(tfmt.TopColSepHdr) } } sb.WriteString("\n") // Title names. sb.WriteString(indent) sb.WriteString(tfmt.Left) for idx, title = range titles { width = colSizes[idx] if !tfmt.NoUpperTitle { title = strings.ToUpper(title) } if tfmt.NoBoldTitle { sb.WriteString(padStr(title, width)) } else { sb.WriteString(color.InBold(padStr(title, width))) } if idx == lastTitleIdx { sb.WriteString(tfmt.Right) } else { sb.WriteString(tfmt.ColSepHdr) } } sb.WriteString("\n") // Bottom header border. sb.WriteString(indent) sb.WriteString(tfmt.BottomLeftHdr) for idx, width = range colSizes { sb.WriteString(strings.Repeat(tfmt.BottomFillHdr, int(width))) if idx == lastTitleIdx { sb.WriteString(tfmt.BottomRightHdr) } else { sb.WriteString(tfmt.BottomColSepHdr) } } sb.WriteString("\n") return } // renderRow renders a row. row should be a struct or non-nil struct ptr. func renderRow(row reflect.Value, colFields []int, colSizes []uint8, indent string, plain, isLast bool, sb *strings.Builder) { var idx int var width uint8 var fieldIdx int var valStr string var lastColIdx int var field reflect.Value var tfmt *tableFormatter = tblFmts[plain] if colFields == nil || len(colFields) == 0 || colSizes == nil || len(colSizes) == 0 { return } if row.Kind() == reflect.Ptr && (row.IsNil() || row.Kind() != reflect.Struct) { return } row = reflect.Indirect(row) if row.Kind() != reflect.Struct { return } lastColIdx = len(colFields) - 1 sb.WriteString(indent) sb.WriteString(tfmt.Left) for idx, fieldIdx = range colFields { width = colSizes[idx] field = row.Field(fieldIdx) switch field.Kind() { case reflect.String: valStr = field.String() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: valStr = strconv.FormatInt(field.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: valStr = strconv.FormatUint(field.Uint(), 10) case reflect.Ptr: // *big.Int if field.IsNil() { continue } field = field.MethodByName("String").Call(nil)[0] valStr = field.String() } sb.WriteString(padStr(valStr, width)) if idx == lastColIdx { sb.WriteString(tfmt.Right) } else { sb.WriteString(tfmt.ColSep) } } sb.WriteString("\n") if !tfmt.SuppressLineSep || isLast { sb.WriteString(indent) if isLast { sb.WriteString(tfmt.LastLeft) } else { sb.WriteString(tfmt.LineLeft) } for idx, width = range colSizes { if isLast { sb.WriteString(strings.Repeat(tfmt.LastFill, int(width))) } else { sb.WriteString(strings.Repeat(tfmt.Fill, int(width))) } if idx == lastColIdx { if isLast { sb.WriteString(tfmt.LastRight) } else { sb.WriteString(tfmt.LineRight) } } else { if isLast { sb.WriteString(tfmt.LastSep) } else { sb.WriteString(tfmt.LineColSep) } } } sb.WriteString("\n") } return } // tplFmtBold renders text as bold-faced. func tplFmtBold(text string) (out string) { out = color.InBold(text) return } /* tplTableLegacy4 renders a table of classes in the legacy "classed" IPv4 networking. Note that indent is *not* applied to the first line. */ func tplTableLegacy4(indent string, plain bool) (out string, err error) { // This whole thing feels dirty. // It's like adding a microcontroller to a rock. // But it works. var idx int var cls string var isLast bool var colFields []int var colSizes []uint8 var colTitles []string var pfx *net.IPNet var row tableLegacy4 var rows []tableLegacy4 var rowVal reflect.Value var classNets []*netip.Prefix var netRange netipx.IPRange var sb *strings.Builder = new(strings.Builder) 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 } rows = make([]tableLegacy4, 5) for idx, cls = range []string{ "A", "B", "C", "D", "E", } { rows[idx] = tableLegacy4{ Class: cls, CIDR: classNets[idx].String(), NetCIDR: *classNets[idx], } netRange = netipx.RangeOfPrefix(rows[idx].NetCIDR) rows[idx].NetStart = netRange.From() rows[idx].NetEnd = netRange.To() rows[idx].Start = rows[idx].NetStart.String() rows[idx].End = rows[idx].NetEnd.String() } colFields, colTitles, colSizes = sizeStructs(rows) // Header renderHdr(colTitles, colSizes, indent, plain, sb) // Rows for idx, row = range rows { rowVal = reflect.ValueOf(row) isLast = idx == len(rows)-1 renderRow(rowVal, colFields, colSizes, indent, plain, isLast, sb) } out = sb.String() return } /* tplTableMasks4 renders a table of netmasks for IPv4. IPv6 doesn't really use netmasks, instead relying almost entirely upon CIDR. Note that indent is *not* applied to the first line. */ func tplTableMasks4(indent string, plain bool) (out string, err error) { var idx int var isLast bool var colFields []int var colSizes []uint8 var colTitles []string var row tableMask4 var rows []tableMask4 var dummyNet *net.IPNet var dummyAddr netip.Addr var rowVal reflect.Value var sb *strings.Builder = new(strings.Builder) if dummyAddr, err = netip.ParseAddr("0.0.0.0"); err != nil { return } rows = make([]tableMask4, dummyAddr.BitLen()+1) for idx = 0; idx <= dummyAddr.BitLen(); idx++ { rows[idx] = tableMask4{ Prefix: fmt.Sprintf("/%d", idx), } if rows[idx].NetPrefix, err = dummyAddr.Prefix(idx); err != nil { return } dummyNet = netipx.PrefixIPNet(rows[idx].NetPrefix.Masked()) rows[idx].Mask = dummyNet.Mask rows[idx].Netmask = netsplit.MaskFmt( dummyNet.Mask, "d", ".", "", 1, 0, ) rows[idx].Hex = dummyNet.Mask.String() rows[idx].Dec = binary.BigEndian.Uint32(dummyNet.Mask) rows[idx].Bin = netsplit.MaskFmt( dummyNet.Mask, "08b", ".", "", 1, 0, ) } colFields, colTitles, colSizes = sizeStructs(rows) // Header renderHdr(colTitles, colSizes, indent, plain, sb) // Rows for idx, row = range rows { rowVal = reflect.ValueOf(row) isLast = idx == len(rows)-1 renderRow(rowVal, colFields, colSizes, indent, plain, isLast, sb) } out = sb.String() return } /* tplTableNotes renders a table of prefix notes IP version/inet family ipVer (4 or 6). Note that indent is *not* applied to the first line. */ func tplTableNotes(ipVer uint8, indent string, plain bool) (out string, err error) { var idx int var isLast bool var ok bool var note string var colFields []int var colSizes []uint8 var colTitles []string var row tablePrefixNote var rows []tablePrefixNote var dummyAddr netip.Addr var rowVal reflect.Value var sb *strings.Builder = new(strings.Builder) 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 } rows = make([]tablePrefixNote, 0, dummyAddr.BitLen()+1) for idx = 0; idx <= dummyAddr.BitLen(); idx++ { if note, ok = netNotes[ipVer][uint8(idx)]; !ok { continue } row = tablePrefixNote{ Prefix: fmt.Sprintf("/%d", idx), Note: note, } if row.NetPrefix, err = dummyAddr.Prefix(idx); err != nil { return } rows = append(rows, row) } colFields, colTitles, colSizes = sizeStructs(rows) // Header renderHdr(colTitles, colSizes, indent, plain, sb) // Rows for idx, row = range rows { rowVal = reflect.ValueOf(row) isLast = idx == len(rows)-1 renderRow(rowVal, colFields, colSizes, indent, plain, isLast, sb) } out = sb.String() return } /* tplTablePrefixes renders a table of prefixes for IP version/inet family ipVer (4 or 6). Note that indent is *not* applied to the first line. */ func tplTablePrefixes(ipVer uint8, indent string, plain bool) (out string, err error) { var idx int var isLast bool var colFields []int var colSizes []uint8 var colTitles []string var row tablePrefix var rows []tablePrefix var dummyNet *net.IPNet var dummyAddr netip.Addr var rowVal reflect.Value var sb *strings.Builder = new(strings.Builder) 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 } rows = make([]tablePrefix, dummyAddr.BitLen()+1) for idx = 0; idx <= dummyAddr.BitLen(); idx++ { rows[idx] = tablePrefix{ Prefix: fmt.Sprintf("/%d", idx), Bits: uint8(dummyAddr.BitLen() - idx), } if rows[idx].NetPrefix, err = dummyAddr.Prefix(idx); err != nil { return } dummyNet = netipx.PrefixIPNet(rows[idx].NetPrefix.Masked()) rows[idx].Addresses = mapcidr.CountIPsInCIDR(true, true, dummyNet) rows[idx].Hosts = mapcidr.CountIPsInCIDR(false, false, dummyNet) } colFields, colTitles, colSizes = sizeStructs(rows) // Header renderHdr(colTitles, colSizes, indent, plain, sb) // Rows for idx, row = range rows { rowVal = reflect.ValueOf(row) isLast = idx == len(rows)-1 renderRow(rowVal, colFields, colSizes, indent, plain, isLast, sb) } out = sb.String() return } // rows should be a slice of structs/struct ptrs of all the same type (you're going to get errors/panics otherwise). func sizeStructs(rows any) (colFieldIdx []int, colTitles []string, colSizes []uint8) { var title string var size int var numFields int var colIdx int var fieldIdx int var valLen int var valStr string var rowVals []reflect.Value var val reflect.Value var valType reflect.Type var field reflect.StructField var fieldVal reflect.Value // Here be dragons. This is *NOT SAFE* for generic (heh) consumption. val = reflect.ValueOf(rows) if val.IsNil() || val.Type().Kind() != reflect.Slice || val.Len() == 0 { return } rowVals = make([]reflect.Value, val.Len()) for idx := 0; idx < val.Len(); idx++ { rowVals[idx] = val.Index(idx) } // I *think* this could potentially cause a panic if rows[0] is nil, BUT this func can't be used externally so it's Fine(TM). // Usage of it is tightly scoped. valType = reflect.Indirect(rowVals[0]).Type() if valType.Kind() != reflect.Struct { return } numFields = reflect.Indirect(rowVals[0]).NumField() colTitles = make([]string, 0, numFields) colSizes = make([]uint8, 0, numFields) colFieldIdx = make([]int, 0, numFields) // Populate from the struct type first. for i := 0; i < numFields; i++ { field = valType.Field(i) if field.Tag.Get("render") == "-" { continue } title = field.Tag.Get("renderTitle") if title == "" { title = field.Name } size = len(title) + (fixedPad * len(padChars)) colTitles = append(colTitles, title) colSizes = append(colSizes, uint8(size)) colFieldIdx = append(colFieldIdx, i) } // colTitles is done. // check each field in each row to see if its value's size is greater than the current. for _, row := range rowVals { val = reflect.Indirect(row) if val.Kind() != reflect.Struct { continue } for colIdx, fieldIdx = range colFieldIdx { fieldVal = val.Field(fieldIdx) // The row struct types only implement these primitives. switch fieldVal.Kind() { case reflect.String: valStr = fieldVal.String() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: valStr = strconv.FormatInt(fieldVal.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: valStr = strconv.FormatUint(fieldVal.Uint(), 10) case reflect.Ptr: // *big.Int. No need to/don't fieldVal.Indirect(); .String() is a ptr method. // Sidenote, I didn't even know until today that reflection can *do* this. fieldVal = fieldVal.MethodByName("String").Call(nil)[0] valStr = fieldVal.String() } valLen = len(valStr) + (fixedPad * len(padChars)) if valLen > int(colSizes[colIdx]) { colSizes[colIdx] = uint8(valLen) } } } return }