Files
nexus/internal/leviathan/parsers/formats_more.go
T
2026-06-16 11:51:07 +01:00

663 lines
20 KiB
Go

// Additional structure parsers that round out the structures panel into a
// forensic-grade set: gzip, tar, SQLite, TIFF, pcap/pcapng and the OLE/CFB
// compound-file container used by legacy Office documents and MSI installers.
//
// Each parser follows the same contract as the parsers in formats.go: it
// returns a *Field tree anchored at absolute document offsets, never panics on
// truncated input, and bounds every loop so a hostile file cannot wedge the UI.
package parsers
import (
"encoding/binary"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/andydixon/nexus/internal/leviathan/document"
)
// leaf builds a terminal Field with no children.
func leaf(name, typ string, off, length int64, value string) *Field {
return &Field{Name: name, Type: typ, Offset: off, Length: length, Value: value}
}
// ---------- gzip (RFC 1952) ----------
// ParseGZIP decodes the gzip member header, optional name/comment/extra fields
// and the trailing CRC-32 / ISIZE footer.
func ParseGZIP(d *document.Document) (*Field, error) {
size := d.Size()
hdr, err := d.Range(0, min64(10, size))
if err != nil || len(hdr) < 10 {
return nil, errors.New("gzip too short")
}
if hdr[0] != 0x1f || hdr[1] != 0x8b {
return nil, errors.New("not gzip")
}
root := &Field{Name: "GZIP", Type: "container", Offset: 0, Length: size}
method := hdr[2]
methodName := "deflate"
if method != 8 {
methodName = fmt.Sprintf("unknown(%d)", method)
}
flg := hdr[3]
mtime := binary.LittleEndian.Uint32(hdr[4:8])
root.Children = append(root.Children,
leaf("id", "bytes", 0, 2, "\\x1f\\x8b"),
leaf("method", "u8", 2, 1, methodName),
leaf("flags", "u8", 3, 1, gzipFlags(flg)),
leaf("mtime", "u32le", 4, 4, gzipTime(mtime)),
leaf("extra_flags", "u8", 8, 1, fmt.Sprintf("%d", hdr[8])),
leaf("os", "u8", 9, 1, gzipOS(hdr[9])),
)
off := int64(10)
// FEXTRA
if flg&0x04 != 0 && off+2 <= size {
xb, _ := d.Range(off, 2)
xlen := int64(binary.LittleEndian.Uint16(xb))
root.Children = append(root.Children, leaf("extra", "bytes", off, 2+xlen, fmt.Sprintf("%d bytes", xlen)))
off += 2 + xlen
}
// FNAME
if flg&0x08 != 0 {
s, n := readCStringField(d, off, size)
root.Children = append(root.Children, leaf("name", "asciiz", off, n, s))
off += n
}
// FCOMMENT
if flg&0x10 != 0 {
s, n := readCStringField(d, off, size)
root.Children = append(root.Children, leaf("comment", "asciiz", off, n, s))
off += n
}
// FHCRC
if flg&0x02 != 0 && off+2 <= size {
hb, _ := d.Range(off, 2)
root.Children = append(root.Children, leaf("header_crc16", "u16le", off, 2, fmt.Sprintf("0x%04x", binary.LittleEndian.Uint16(hb))))
off += 2
}
if size >= off+8 {
root.Children = append(root.Children, leaf("compressed_data", "bytes", off, size-off-8, fmt.Sprintf("%d bytes", size-off-8)))
ft, _ := d.Range(size-8, 8)
root.Children = append(root.Children,
leaf("crc32", "u32le", size-8, 4, fmt.Sprintf("0x%08x", binary.LittleEndian.Uint32(ft[0:4]))),
leaf("isize", "u32le", size-4, 4, fmt.Sprintf("%d bytes (mod 2^32)", binary.LittleEndian.Uint32(ft[4:8]))),
)
}
return root, nil
}
func gzipFlags(f byte) string {
var on []string
for _, b := range []struct {
mask byte
name string
}{{0x01, "TEXT"}, {0x02, "HCRC"}, {0x04, "EXTRA"}, {0x08, "NAME"}, {0x10, "COMMENT"}} {
if f&b.mask != 0 {
on = append(on, b.name)
}
}
if len(on) == 0 {
return fmt.Sprintf("0x%02x", f)
}
return fmt.Sprintf("0x%02x (%s)", f, strings.Join(on, "|"))
}
func gzipOS(b byte) string {
names := map[byte]string{0: "FAT", 1: "Amiga", 2: "VMS", 3: "Unix", 6: "HPFS", 7: "Macintosh", 11: "NTFS", 255: "unknown"}
if n, ok := names[b]; ok {
return fmt.Sprintf("%d (%s)", b, n)
}
return fmt.Sprintf("%d", b)
}
func gzipTime(t uint32) string {
if t == 0 {
return "0 (no timestamp)"
}
return fmt.Sprintf("%d (%s)", t, time.Unix(int64(t), 0).UTC().Format("2006-01-02 15:04:05 UTC"))
}
// ---------- tar (POSIX ustar) ----------
// ParseTAR walks the 512-byte record headers of a ustar/GNU tar archive.
func ParseTAR(d *document.Document) (*Field, error) {
size := d.Size()
root := &Field{Name: "TAR", Type: "container", Offset: 0}
off := int64(0)
entries := 0
for off+512 <= size && entries < 10000 {
blk, err := d.Range(off, 512)
if err != nil {
break
}
if allZero(blk) {
root.Children = append(root.Children, leaf("end_of_archive", "bytes", off, 512, "zero block"))
off += 512
break
}
name := trimZeroStr(blk[0:100])
prefix := trimZeroStr(blk[345:500])
full := name
if prefix != "" {
full = prefix + "/" + name
}
fsize := octal(blk[124:136])
mtime := octal(blk[136:148])
typ := blk[156]
e := &Field{
Name: full, Type: "entry", Offset: off,
Length: 512 + roundUp(fsize, 512),
Value: fmt.Sprintf("%s size=%d", tarType(typ), fsize),
Children: []*Field{
leaf("name", "asciiz", off, 100, name),
leaf("mode", "octal", off+100, 8, fmt.Sprintf("0%o", octal(blk[100:108]))),
leaf("uid", "octal", off+108, 8, fmt.Sprintf("%d", octal(blk[108:116]))),
leaf("gid", "octal", off+116, 8, fmt.Sprintf("%d", octal(blk[116:124]))),
leaf("size", "octal", off+124, 12, fmt.Sprintf("%d", fsize)),
leaf("mtime", "octal", off+136, 12, tarTime(mtime)),
leaf("typeflag", "u8", off+156, 1, tarType(typ)),
leaf("magic", "ascii", off+257, 8, trimZeroStr(blk[257:265])),
leaf("uname", "asciiz", off+265, 32, trimZeroStr(blk[265:297])),
leaf("gname", "asciiz", off+297, 32, trimZeroStr(blk[297:329])),
},
}
if prefix != "" {
e.Children = append(e.Children, leaf("prefix", "asciiz", off+345, 155, prefix))
}
root.Children = append(root.Children, e)
off += 512 + roundUp(fsize, 512)
entries++
}
root.Length = off
if len(root.Children) == 0 {
return nil, errors.New("no tar entries")
}
return root, nil
}
func tarType(t byte) string {
switch t {
case 0, '0':
return "file"
case '1':
return "hardlink"
case '2':
return "symlink"
case '3':
return "char-device"
case '4':
return "block-device"
case '5':
return "directory"
case '6':
return "fifo"
case '7':
return "contiguous"
case 'x':
return "pax-header"
case 'g':
return "pax-global"
case 'L':
return "gnu-longname"
case 'K':
return "gnu-longlink"
default:
return fmt.Sprintf("type-%q", t)
}
}
func tarTime(t int64) string {
if t == 0 {
return "0"
}
return fmt.Sprintf("%d (%s)", t, time.Unix(t, 0).UTC().Format("2006-01-02 15:04:05 UTC"))
}
// ---------- SQLite 3 ----------
// ParseSQLite decodes the 100-byte SQLite database header.
func ParseSQLite(d *document.Document) (*Field, error) {
hdr, err := d.Range(0, min64(100, d.Size()))
if err != nil || len(hdr) < 100 {
return nil, errors.New("sqlite header too short")
}
if string(hdr[0:16]) != "SQLite format 3\x00" {
return nil, errors.New("not a SQLite database")
}
be := binary.BigEndian
pageSize := int64(be.Uint16(hdr[16:18]))
if pageSize == 1 {
pageSize = 65536
}
enc := map[uint32]string{1: "UTF-8", 2: "UTF-16le", 3: "UTF-16be"}[be.Uint32(hdr[56:60])]
if enc == "" {
enc = "unknown"
}
root := &Field{
Name: "SQLite", Type: "container", Offset: 0, Length: d.Size(),
Value: fmt.Sprintf("page=%d pages=%d %s", pageSize, be.Uint32(hdr[28:32]), enc),
Children: []*Field{
leaf("magic", "ascii", 0, 16, "SQLite format 3"),
leaf("page_size", "u16be", 16, 2, fmt.Sprintf("%d", pageSize)),
leaf("write_version", "u8", 18, 1, sqliteFmtVer(hdr[18])),
leaf("read_version", "u8", 19, 1, sqliteFmtVer(hdr[19])),
leaf("reserved_space", "u8", 20, 1, fmt.Sprintf("%d", hdr[20])),
leaf("change_counter", "u32be", 24, 4, fmt.Sprintf("%d", be.Uint32(hdr[24:28]))),
leaf("page_count", "u32be", 28, 4, fmt.Sprintf("%d", be.Uint32(hdr[28:32]))),
leaf("freelist_head", "u32be", 32, 4, fmt.Sprintf("%d", be.Uint32(hdr[32:36]))),
leaf("freelist_pages", "u32be", 36, 4, fmt.Sprintf("%d", be.Uint32(hdr[36:40]))),
leaf("schema_cookie", "u32be", 40, 4, fmt.Sprintf("%d", be.Uint32(hdr[40:44]))),
leaf("schema_format", "u32be", 44, 4, fmt.Sprintf("%d", be.Uint32(hdr[44:48]))),
leaf("text_encoding", "u32be", 56, 4, enc),
leaf("user_version", "u32be", 60, 4, fmt.Sprintf("%d", be.Uint32(hdr[60:64]))),
leaf("vacuum_mode", "u32be", 64, 4, fmt.Sprintf("%d", be.Uint32(hdr[64:68]))),
leaf("application_id", "u32be", 68, 4, fmt.Sprintf("0x%08x", be.Uint32(hdr[68:72]))),
leaf("version_valid_for", "u32be", 92, 4, fmt.Sprintf("%d", be.Uint32(hdr[92:96]))),
leaf("sqlite_version", "u32be", 96, 4, sqliteVersion(be.Uint32(hdr[96:100]))),
},
}
return root, nil
}
func sqliteFmtVer(b byte) string {
switch b {
case 1:
return "1 (legacy/rollback)"
case 2:
return "2 (WAL)"
default:
return fmt.Sprintf("%d", b)
}
}
func sqliteVersion(v uint32) string {
// SQLITE_VERSION_NUMBER = major*1_000_000 + minor*1000 + patch.
return fmt.Sprintf("%d (%d.%d.%d)", v, v/1000000, (v/1000)%1000, v%1000)
}
// ---------- TIFF ----------
// ParseTIFF decodes the TIFF header and the entries of each image file
// directory (IFD), following the next-IFD chain.
func ParseTIFF(d *document.Document) (*Field, error) {
size := d.Size()
hdr, err := d.Range(0, min64(8, size))
if err != nil || len(hdr) < 8 {
return nil, errors.New("tiff too short")
}
var bo binary.ByteOrder
switch string(hdr[0:2]) {
case "II":
bo = binary.LittleEndian
case "MM":
bo = binary.BigEndian
default:
return nil, errors.New("not TIFF")
}
if bo.Uint16(hdr[2:4]) != 42 {
return nil, errors.New("bad TIFF magic")
}
order := "little-endian"
if string(hdr[0:2]) == "MM" {
order = "big-endian"
}
root := &Field{
Name: "TIFF", Type: "container", Offset: 0, Length: size, Value: order,
Children: []*Field{
leaf("byte_order", "ascii", 0, 2, order),
leaf("magic", "u16", 2, 2, "42"),
leaf("first_ifd", "u32", 4, 4, fmt.Sprintf("0x%x", bo.Uint32(hdr[4:8]))),
},
}
ifdOff := int64(bo.Uint32(hdr[4:8]))
for n := 0; ifdOff > 0 && ifdOff+2 <= size && n < 64; n++ {
cb, err := d.Range(ifdOff, 2)
if err != nil {
break
}
count := int64(bo.Uint16(cb))
ifd := &Field{
Name: fmt.Sprintf("IFD[%d]", n), Type: "directory", Offset: ifdOff,
Length: 2 + count*12 + 4, Value: fmt.Sprintf("%d entries", count),
}
for i := int64(0); i < count && i < 4096; i++ {
eoff := ifdOff + 2 + i*12
eb, err := d.Range(eoff, 12)
if err != nil || len(eb) < 12 {
break
}
tag := bo.Uint16(eb[0:2])
typ := bo.Uint16(eb[2:4])
cnt := bo.Uint32(eb[4:8])
val := bo.Uint32(eb[8:12])
ifd.Children = append(ifd.Children, leaf(
fmt.Sprintf("tag 0x%04x %s", tag, tiffTag(tag)), "ifd_entry", eoff, 12,
fmt.Sprintf("type=%s count=%d value=%d", tiffType(typ), cnt, val)))
}
root.Children = append(root.Children, ifd)
nb, err := d.Range(ifdOff+2+count*12, 4)
if err != nil || len(nb) < 4 {
break
}
ifdOff = int64(bo.Uint32(nb))
}
return root, nil
}
func tiffType(t uint16) string {
names := map[uint16]string{1: "BYTE", 2: "ASCII", 3: "SHORT", 4: "LONG", 5: "RATIONAL", 6: "SBYTE", 7: "UNDEFINED", 8: "SSHORT", 9: "SLONG", 10: "SRATIONAL", 11: "FLOAT", 12: "DOUBLE"}
if n, ok := names[t]; ok {
return n
}
return fmt.Sprintf("type%d", t)
}
func tiffTag(t uint16) string {
names := map[uint16]string{
256: "ImageWidth", 257: "ImageLength", 258: "BitsPerSample", 259: "Compression",
262: "PhotometricInterpretation", 273: "StripOffsets", 277: "SamplesPerPixel",
278: "RowsPerStrip", 279: "StripByteCounts", 282: "XResolution", 283: "YResolution",
296: "ResolutionUnit", 305: "Software", 306: "DateTime", 315: "Artist",
271: "Make", 272: "Model", 274: "Orientation", 34665: "ExifIFD", 34853: "GPSIFD",
}
if n, ok := names[t]; ok {
return n
}
return ""
}
// ---------- pcap / pcapng ----------
// ParsePCAP decodes a classic libpcap global header and walks packet records.
func ParsePCAP(d *document.Document) (*Field, error) {
size := d.Size()
hdr, err := d.Range(0, min64(24, size))
if err != nil || len(hdr) < 24 {
return nil, errors.New("pcap too short")
}
magic := binary.LittleEndian.Uint32(hdr[0:4])
var bo binary.ByteOrder
nano := false
switch magic {
case 0xa1b2c3d4:
bo = binary.LittleEndian
case 0xd4c3b2a1:
bo = binary.BigEndian
case 0xa1b23c4d:
bo, nano = binary.LittleEndian, true
case 0x4d3cb2a1:
bo, nano = binary.BigEndian, true
default:
return nil, errors.New("not pcap")
}
res := "microsecond"
if nano {
res = "nanosecond"
}
linktype := bo.Uint32(hdr[20:24])
root := &Field{
Name: "PCAP", Type: "container", Offset: 0, Length: size,
Value: fmt.Sprintf("%s, %s", res, pcapLinkType(linktype)),
Children: []*Field{
leaf("magic", "u32", 0, 4, fmt.Sprintf("0x%08x (%s)", magic, res)),
leaf("version", "u16", 4, 4, fmt.Sprintf("%d.%d", bo.Uint16(hdr[4:6]), bo.Uint16(hdr[6:8]))),
leaf("thiszone", "i32", 8, 4, fmt.Sprintf("%d", int32(bo.Uint32(hdr[8:12])))),
leaf("sigfigs", "u32", 12, 4, fmt.Sprintf("%d", bo.Uint32(hdr[12:16]))),
leaf("snaplen", "u32", 16, 4, fmt.Sprintf("%d", bo.Uint32(hdr[16:20]))),
leaf("linktype", "u32", 20, 4, fmt.Sprintf("%d (%s)", linktype, pcapLinkType(linktype))),
},
}
off := int64(24)
for n := 0; off+16 <= size && n < 100000; n++ {
rb, err := d.Range(off, 16)
if err != nil {
break
}
ts := bo.Uint32(rb[0:4])
us := bo.Uint32(rb[4:8])
inclLen := int64(bo.Uint32(rb[8:12]))
origLen := bo.Uint32(rb[12:16])
root.Children = append(root.Children, leaf(
fmt.Sprintf("packet[%d]", n), "record", off, 16+inclLen,
fmt.Sprintf("ts=%d.%06d incl=%d orig=%d", ts, us, inclLen, origLen)))
off += 16 + inclLen
}
return root, nil
}
// ParsePCAPNG decodes the pcapng block structure (section header + block walk).
func ParsePCAPNG(d *document.Document) (*Field, error) {
size := d.Size()
hb, err := d.Range(0, min64(12, size))
if err != nil || len(hb) < 12 {
return nil, errors.New("pcapng too short")
}
if binary.LittleEndian.Uint32(hb[0:4]) != 0x0a0d0d0a {
return nil, errors.New("not pcapng")
}
// Byte-order magic lives in the SHB body at offset 8.
var bo binary.ByteOrder
switch binary.LittleEndian.Uint32(hb[8:12]) {
case 0x1a2b3c4d:
bo = binary.LittleEndian
case 0x4d3c2b1a:
bo = binary.BigEndian
default:
return nil, errors.New("bad pcapng byte-order magic")
}
root := &Field{Name: "PCAPNG", Type: "container", Offset: 0, Length: size}
off := int64(0)
for n := 0; off+12 <= size && n < 100000; n++ {
bh, err := d.Range(off, 12)
if err != nil {
break
}
btype := bo.Uint32(bh[0:4])
blen := int64(bo.Uint32(bh[4:8]))
if blen < 12 || off+blen > size {
break
}
root.Children = append(root.Children, leaf(
fmt.Sprintf("block[%d] %s", n, pcapngBlock(btype)), "block", off, blen,
fmt.Sprintf("type=0x%08x length=%d", btype, blen)))
off += blen
}
return root, nil
}
func pcapngBlock(t uint32) string {
switch t {
case 0x0a0d0d0a:
return "SectionHeader"
case 0x00000001:
return "InterfaceDescription"
case 0x00000003:
return "SimplePacket"
case 0x00000004:
return "NameResolution"
case 0x00000005:
return "InterfaceStatistics"
case 0x00000006:
return "EnhancedPacket"
default:
return "block"
}
}
func pcapLinkType(t uint32) string {
names := map[uint32]string{0: "NULL", 1: "ETHERNET", 6: "TOKEN_RING", 9: "PPP", 101: "RAW", 105: "IEEE802_11", 113: "LINUX_SLL", 127: "IEEE802_11_RADIOTAP", 228: "IPV4", 229: "IPV6", 276: "LINUX_SLL2"}
if n, ok := names[t]; ok {
return n
}
return fmt.Sprintf("linktype%d", t)
}
// ---------- OLE / Compound File Binary (MS-CFB) ----------
var cfbMagic = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1}
// ParseCFB decodes the compound-file header and lists the entries in the first
// directory sector (legacy .doc/.xls/.ppt/.msi container).
func ParseCFB(d *document.Document) (*Field, error) {
size := d.Size()
hdr, err := d.Range(0, min64(512, size))
if err != nil || len(hdr) < 76 {
return nil, errors.New("cfb too short")
}
if string(hdr[0:8]) != string(cfbMagic) {
return nil, errors.New("not a compound file")
}
le := binary.LittleEndian
major := le.Uint16(hdr[26:28])
sectorShift := le.Uint16(hdr[30:32])
sectorSize := int64(1) << sectorShift
firstDir := le.Uint32(hdr[48:52])
root := &Field{
Name: "CFB/OLE", Type: "container", Offset: 0, Length: size,
Value: fmt.Sprintf("v%d.%d sector=%d", major, le.Uint16(hdr[24:26]), sectorSize),
Children: []*Field{
leaf("signature", "bytes", 0, 8, "D0 CF 11 E0 A1 B1 1A E1"),
leaf("minor_version", "u16le", 24, 2, fmt.Sprintf("%d", le.Uint16(hdr[24:26]))),
leaf("major_version", "u16le", 26, 2, fmt.Sprintf("%d", major)),
leaf("byte_order", "u16le", 28, 2, fmt.Sprintf("0x%04x", le.Uint16(hdr[28:30]))),
leaf("sector_shift", "u16le", 30, 2, fmt.Sprintf("%d (%d bytes)", sectorShift, sectorSize)),
leaf("mini_sector_shift", "u16le", 32, 2, fmt.Sprintf("%d (%d bytes)", le.Uint16(hdr[32:34]), int64(1)<<le.Uint16(hdr[32:34]))),
leaf("num_dir_sectors", "u32le", 40, 4, fmt.Sprintf("%d", le.Uint32(hdr[40:44]))),
leaf("num_fat_sectors", "u32le", 44, 4, fmt.Sprintf("%d", le.Uint32(hdr[44:48]))),
leaf("first_dir_sector", "u32le", 48, 4, fmt.Sprintf("%d", firstDir)),
leaf("mini_stream_cutoff", "u32le", 56, 4, fmt.Sprintf("%d", le.Uint32(hdr[56:60]))),
leaf("first_minifat_sector", "u32le", 60, 4, fmt.Sprintf("%d", le.Uint32(hdr[60:64]))),
leaf("num_minifat_sectors", "u32le", 64, 4, fmt.Sprintf("%d", le.Uint32(hdr[64:68]))),
leaf("first_difat_sector", "u32le", 68, 4, fmt.Sprintf("%d", le.Uint32(hdr[68:72]))),
leaf("num_difat_sectors", "u32le", 72, 4, fmt.Sprintf("%d", le.Uint32(hdr[72:76]))),
},
}
// First directory sector holds up to sectorSize/128 directory entries.
dirOff := int64(firstDir+1) * sectorSize
if dir := cfbDirectory(d, dirOff, sectorSize, size); dir != nil {
root.Children = append(root.Children, dir)
}
return root, nil
}
func cfbDirectory(d *document.Document, off, sectorSize, size int64) *Field {
if off <= 0 || off+128 > size {
return nil
}
dir := &Field{Name: "directory", Type: "directory", Offset: off}
per := sectorSize / 128
count := int64(0)
for i := int64(0); i < per; i++ {
eoff := off + i*128
eb, err := d.Range(eoff, 128)
if err != nil || len(eb) < 128 {
break
}
nameLen := int64(binary.LittleEndian.Uint16(eb[64:66]))
objType := eb[66]
if objType == 0 { // unallocated entry
continue
}
name := ""
if nameLen >= 2 && nameLen <= 64 {
name = decodeUTF16LE(eb[0 : nameLen-2])
}
streamSize := binary.LittleEndian.Uint64(eb[120:128])
dir.Children = append(dir.Children, leaf(
cfbEntryLabel(name, count), "dir_entry", eoff, 128,
fmt.Sprintf("%s size=%d", cfbObjType(objType), streamSize)))
count++
}
dir.Length = sectorSize
dir.Value = fmt.Sprintf("%d entries", count)
if count == 0 {
return nil
}
return dir
}
func cfbEntryLabel(name string, i int64) string {
if name == "" {
return fmt.Sprintf("entry[%d]", i)
}
// Strip the control prefix Office uses on some stream names for display.
clean := strings.Map(func(r rune) rune {
if r < 0x20 {
return -1
}
return r
}, name)
if clean == "" {
clean = name
}
return fmt.Sprintf("entry[%d] %s", i, clean)
}
func cfbObjType(t byte) string {
switch t {
case 1:
return "storage"
case 2:
return "stream"
case 5:
return "root"
default:
return fmt.Sprintf("type%d", t)
}
}
// ---------- shared helpers ----------
func readCStringField(d *document.Document, off, size int64) (string, int64) {
var sb strings.Builder
n := int64(0)
for off+n < size && n < 4096 {
b, err := d.At(off + n)
if err != nil {
break
}
n++
if b == 0 {
break
}
sb.WriteByte(b)
}
return sb.String(), n
}
func allZero(b []byte) bool {
for _, x := range b {
if x != 0 {
return false
}
}
return true
}
func trimZeroStr(b []byte) string {
return strings.TrimRight(string(b), "\x00 ")
}
// octal parses a space/NUL-padded octal ASCII field (tar number encoding).
func octal(b []byte) int64 {
s := strings.TrimSpace(strings.Trim(string(b), "\x00"))
if s == "" {
return 0
}
v, err := strconv.ParseInt(s, 8, 64)
if err != nil {
return 0
}
return v
}
func roundUp(n, to int64) int64 {
if n <= 0 {
return 0
}
return ((n + to - 1) / to) * to
}