package main import ( "bufio" "errors" "fmt" "io" "log" "os" "strconv" "github.com/jessevdk/go-flags" ) var opts struct { OffsetFormat string `short:"t" long:"radix" default:"x" choice:"d" choice:"o" choice:"x" description:"Print offset in [d|o|x] format"` Binary bool `short:"b" long:"binary" description:"output in binary format (01010101) incompatible with plain, reverse and include"` Reverse bool `short:"r" long:"reverse" description:"re-assemble hexdump output back into binary"` Autoskip bool `short:"a" long:"autoskip" description:"toggle autoskip (replaces blank lines with a *)"` Bars bool `short:"B" long:"bars" description:"delimiter bars in ascii table"` Seek int64 `short:"s" long:"seek" description:"start at bytes"` Len int64 `short:"l" long:"len" description:"stop after octets"` Columns int `short:"c" long:"columns" description:"column count"` GroupSize int `short:"g" long:"groups" description:"group count"` Plain bool `short:"p" long:"plain" description:"plain output without ascii table and offset row [often used with hexxy -r]"` Upper bool `short:"u" long:"upper" description:"output hex in UPPERCASE format"` CInclude bool `short:"i" long:"include" description:"output in C include format"` OutputFile string `short:"o" long:"output" description:"automatically output to file instead of STDOUT"` Separator string `long:"separator" default:"|" description:"separator character for the ascii character table"` ForceColor bool `short:"F" long:"force-color" description:"color is automatically disabled if output is a pipe, this option forces color output"` NoColor bool `short:"N" long:"no-color" description:"do not print output with color"` Verbose bool `short:"v" long:"verbose" description:"print debugging information and verbose output"` } var Debug = func(string, ...interface{}) {} const ( dumpHex = iota dumpBinary dumpCformat dumpPlain ) const ( ldigits = "0123456789abcdef" udigits = "0123456789ABCDEF" ) var ( dumpType int space = []byte(" ") doubleSpace = []byte(" ") dot = []byte(".") newLine = []byte("\n") zeroHeader = []byte("0000000: ") unsignedChar = []byte("unsigned char ") unsignedInt = []byte("};\nunsigned int ") lenEquals = []byte("_len = ") brackets = []byte("[] = {") asterisk = []byte("*") commaSpace = []byte(", ") comma = []byte(",") semiColonNl = []byte(";\n") bar = []byte("|") ) func binaryEncode(dst, src []byte) { d := uint(0) _, _ = src[0], dst[7] for i := 7; i >= 0; i-- { if src[0]&(1<= 16 && i <= 20) || (i >= 232 && i <= 242) if lowVis { fg = WHITEB + "\x1b[38;5;" + "255" + "m" bg = "\x1b[48;5;" + strconv.Itoa(int(i)) + "m" } else { fg = "\x1b[38;5;" + strconv.Itoa(int(i)) + "m" bg = "" } c.values[i] = bg + fg } } func (c *Color) Colorize(s string, clr byte) string { const NOCOLOR = "\x1b[0m" return c.values[clr] + s + NOCOLOR } func inputIsPipe() bool { stat, _ := os.Stdin.Stat() return stat.Mode()&os.ModeCharDevice != os.ModeCharDevice } func outputIsPipe() bool { stat, _ := os.Stdout.Stat() return stat.Mode()&os.ModeCharDevice != os.ModeCharDevice } func HexdumpPlain(file *os.File) error { var i uint64 reader := bufio.NewReaderSize(file, 10*1024) for { b, err := reader.ReadByte() if errors.Is(err, io.EOF) { break } if err != nil { return fmt.Errorf("Failed to read %v: %w\n", file.Name(), err) } if i%30 == 0 { println() } fmt.Printf("%02x", b) i++ } return nil } // func plain2Binary(file *os.File) error { // return reverse(os.Stdout, os.Stdin) // } // func getOffsetFormat() error { // var prefix string // var suffix string // var format string // switch opts.OffsetFormat { // case "d": // format = prefix + "%08d " + suffix // case "o": // format = prefix + "%08o " + suffix // case "x": // format = prefix + "%08x " + suffix // default: // return fmt.Errorf("Offset format must be [d|o|x]") // } // return nil // } func XXD(r io.Reader, w io.Writer, filename string) error { var ( lineOffset int64 hexOffset = make([]byte, 6) groupSize int cols int octs int caps = ldigits doCheader = true doCEnd bool varDeclChar = make([]byte, 14+len(filename)+6) // for "unsigned char NAME_FORMAT[] = {" varDeclInt = make([]byte, 16+len(filename)+7) // enough room for "unsigned int NAME_FORMAT = " nulLine int64 totalOcts int64 ) if dumpType == dumpCformat { _ = copy(varDeclChar[0:14], unsignedChar[:]) _ = copy(varDeclInt[0:14], lenEquals[:]) for i := 0; i < len(filename); i++ { if filename[i] != '.' { varDeclChar[14+i] = filename[i] varDeclInt[16+i] = filename[i] } else { varDeclChar[14+i] = '_' varDeclInt[16+i] = '_' } } // copy "[] = {" and "_len = " _ = copy(varDeclChar[14+len(filename):], brackets[:]) _ = copy(varDeclInt[16+len(filename):], lenEquals[:]) } if opts.Upper { caps = udigits } if opts.Columns == -1 { switch dumpType { case dumpPlain: cols = 30 case dumpCformat: cols = 12 case dumpBinary: cols = 6 default: cols = 16 } } else { cols = opts.Columns } switch dumpType { case dumpBinary: octs = 8 groupSize = 1 case dumpPlain: octs = 0 case dumpCformat: octs = 4 default: octs = 2 groupSize = 2 } if opts.GroupSize != -1 { groupSize = opts.GroupSize } if opts.Len != -1 { if opts.Len < int64(cols) { cols = int(opts.Len) } } if octs < 1 { octs = cols } // allocate their size based on the users specs, hence why its declared here var ( line = make([]byte, cols) char = make([]byte, octs) ) c := int64(0) nl := int64(0) r = bufio.NewReader(r) var ( n int err error ) for { n, err = io.ReadFull(r, line) if err != nil && errors.Is(err, io.EOF) && errors.Is(err, io.ErrUnexpectedEOF) { return fmt.Errorf("hexxy: %v", err) } if dumpType == dumpPlain && n != 0 { for i := 0; i < n; i++ { hexEncode(char, line[i:i+1], caps) w.Write(char) c++ } continue } if n == 0 { if dumpType == dumpPlain { w.Write(newLine) } if dumpType == dumpCformat { doCEnd = true } else { return nil } } if opts.Len != -1 { if totalOcts == opts.Len { break } totalOcts += opts.Len } if opts.Autoskip && empty(&line) { if nulLine == 1 { w.Write(asterisk) w.Write(newLine) } nulLine++ if nulLine > 1 { lineOffset++ continue } } // hex or binary formats only if dumpType <= dumpBinary { // line offset hexOffset = strconv.AppendInt(hexOffset[0:0], lineOffset, 16) w.Write(zeroHeader[0:(6 - len(hexOffset))]) w.Write(hexOffset) w.Write(zeroHeader[6:]) lineOffset++ } else if doCheader { w.Write(varDeclChar) w.Write(newLine) doCheader = false } if dumpType == dumpBinary { // binary values for i, k := 0, octs; i < n; i, k = i+1, k+octs { binaryEncode(char, line[i:i+1]) w.Write(char) c++ if k == octs*groupSize { k = 0 w.Write(space) } } } else if dumpType == dumpCformat { if !doCEnd { w.Write(doubleSpace) } for i := 0; i < n; i++ { cfmtEncode(char, line[i:i+1], caps) w.Write(char) c++ // no space at EOL if i != n-1 { w.Write(commaSpace) } else if n == cols { w.Write(comma) } } } else { // hex values -- default for i, k := 0, octs; i < n; i, k = i+1, k+octs { hexEncode(char, line[i:i+1], caps) w.Write(char) c++ if k == octs*groupSize { k = 0 w.Write(space) } } } if doCEnd { w.Write(varDeclInt) w.Write([]byte(strconv.FormatInt(c, 10))) w.Write(semiColonNl) return nil } if n < len(line) && dumpType <= dumpBinary { for i := n * octs; i < len(line)*octs; i++ { w.Write(space) if i%octs == 1 { w.Write(space) } } } if dumpType != dumpCformat { w.Write(space) } if dumpType <= dumpBinary { // character values b := line[:n] // |hello,.world!| if opts.Bars { w.Write(bar) } var v byte for i := 0; i < len(b); i++ { v = b[i] if v > 0x1f && v < 0x7f { w.Write(line[i : i+1]) } else { w.Write(dot) } } if opts.Bars { w.Write(bar) } } w.Write(newLine) nl++ } return nil } func Hexxy(args []string) error { color := &Color{} if opts.NoColor { color.disable = true } if !color.disable { color.Compute() } var infile, outfile *os.File var err error if len(args) < 1 && inputIsPipe() { infile = os.Stdin } else { infile, err = os.Open(args[0]) if err != nil { return fmt.Errorf("hexxy: %v", err.Error()) } } defer infile.Close() if opts.Seek != -1 { _, err = infile.Seek(opts.Seek, io.SeekStart) if err != nil { return fmt.Errorf("hexxy: %v", err.Error()) } } if opts.OutputFile != "" { outfile, err = os.Open(opts.OutputFile) if err != nil { return fmt.Errorf("hexxy: %v", err.Error()) } } else { outfile = os.Stdout } defer outfile.Close() switch { case opts.Binary: dumpType = dumpBinary case opts.CInclude: dumpType = dumpCformat case opts.Plain: dumpType = dumpPlain default: dumpType = dumpHex } out := bufio.NewWriter(outfile) defer out.Flush() if opts.Reverse { } if err := XXD(infile, out, infile.Name()); err != nil { return fmt.Errorf("hexxy: %v", err.Error()) } return nil } const usage_msg = ` hexxy is a command line hex dumping tool Examples: hexxy [OPTIONS] input-file ` // extra usage examples func usage() { fmt.Fprint(os.Stderr, usage_msg) } func init() { opts.Seek = -1 // default no-op value opts.Columns = -1 opts.GroupSize = -1 opts.Len = -1 } func main() { parser := flags.NewParser(&opts, flags.Default) args, err := parser.Parse() if flags.WroteHelp(err) { os.Exit(0) } if err != nil { log.Fatal(err) } if !inputIsPipe() && len(args) == 0 { parser.WriteHelp(os.Stderr) os.Exit(0) } if opts.Verbose { Debug = log.Printf } if err := Hexxy(args); err != nil { log.Fatal(err) } }