├── LICENSE ├── README.md ├── assets ├── hexxy.png └── img.png ├── cmd └── hexxy │ ├── color.go │ ├── config │ └── hexxy.ini │ ├── encode.go │ ├── hexxy.go │ └── reverse.go ├── go.mod ├── go.sum └── justfile /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 sweetbbak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

A modern alternative to `xxd` and `hexdump`
4 |

5 | 6 | ![example of hexxy in action](assets/img.png) 7 | 8 | ## Quick install 9 | 10 | requirements: Go 1.20+ (it may build with earlier versions as well but I have not tested them) and git 11 | 12 | ```sh 13 | go install github.com/sweetbbak/hexxy/cmd/hexxy@latest 14 | ``` 15 | 16 | On ArchLinux ([hexxy-git](https://aur.archlinux.org/packages/hexxy-git)), e.g.: 17 | 18 | ``` 19 | pikaur -S hexxy-git 20 | paru -S hexxy-git 21 | yay -S hexxy-git 22 | ``` 23 | 24 | ## Example usage 25 | 26 | ```sh 27 | # normal usage 28 | hexxy /path/to/file.bin 29 | 30 | # output without color 31 | hexxy --no-color /path/to/file.bin 32 | 33 | # read from stdin 34 | cat mybinary | hexxy 35 | 36 | # display plain output 37 | hexxy -p file.bin 38 | 39 | # Include a binary as a C variable 40 | hexxy -i input-file > output.c 41 | 42 | # Use plain non-formatted output 43 | hexxy -p input-file 44 | 45 | # crunch empty lines with a '*' and use uppercase HEX 46 | hexxy -a --upper input-file 47 | 48 | # Reverse plain non-formatted output (reverse plain) 49 | hexxy -rp input-file 50 | 51 | # Show output with a space in between N groups of bytes 52 | hexxy -g1 input-file ... -> outputs: 00000000: 0f 1a ff ff 00 aa 53 | 54 | # display offset in Decimal format 55 | hexxy -td file.bin 56 | 57 | # display offset in Octal format 58 | hexxy -to file.bin 59 | 60 | # configure color 61 | # shows color even when piping to a file or stdout/stderr 62 | hexxy --color=always # or never, auto 63 | 64 | # turn off ascii table color (but keep byte coloring) 65 | hexxy -A 66 | 67 | # write the default config file 68 | hexxy --create-config 69 | 70 | # ignore config file (you can also just delete it) 71 | # it is not required. Command line flags override config flags 72 | hexxy --no-config 73 | 74 | # show ascii table bars 75 | # and set the seperator (great time to set a default in the config file) 76 | hexxy --bars --seperator='|' 77 | ``` 78 | 79 | ## Building 80 | 81 | ```sh 82 | git clone https://github.com/sweetbbak/hexxy.git 83 | cd hexxy 84 | go build -o hexxy -ldflags='-s -w' ./src 85 | # or use just by running 'just' 86 | ``` 87 | 88 | ## Changelog 89 | 90 | - 3/23/25: added a config file and more options 91 | 92 | ## Performance 93 | 94 | `zk` is a 17mb binary 95 | 96 | ```sh 97 | xxd -i ~/bin/zk &> /dev/null 0.66s user 0.02s system 99% cpu 0.677 total 98 | hexxy -i ~/bin/zk &> /dev/null 0.16s user 0.01s system 98% cpu 0.165 total 99 | ``` 100 | 101 | ```sh 102 | # plain XXD 103 | xxd ~/bin/zk &> /dev/null 0.12s user 0.01s system 99% cpu 0.126 total 104 | 105 | # hexxy without color 106 | hexxy -N ~/bin/zk &> /dev/null 0.21s user 0.01s system 100% cpu 0.223 total 107 | 108 | # hexxy with color 109 | hexxy ~/bin/zk &> /dev/null 0.37s user 0.01s system 99% cpu 0.383 total 110 | ``` 111 | 112 | `hexxy` is obviously going to be slower as it is writing a lot more bytes in the form of 113 | ANSI escape sequences. There is potential to optimize this using some deduplication or Huffman 114 | encoding, but that might also be slower. 115 | 116 | ## Credits 117 | 118 | thanks to [felixge](https://github.com/felixge/go-xxd) for showing how this is done quickly 119 | thanks to [igoracmelo](https://github.com/igoracmelo/xx) for the idea to colorize hexdump output with a gradient 120 | 121 | thanks to everyone who has committed to this repo! <3 122 | -------------------------------------------------------------------------------- /assets/hexxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sweetbbak/hexxy/8833335e497069247f330444ddbd64c5ddaa1b36/assets/hexxy.png -------------------------------------------------------------------------------- /assets/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sweetbbak/hexxy/8833335e497069247f330444ddbd64c5ddaa1b36/assets/img.png -------------------------------------------------------------------------------- /cmd/hexxy/color.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | var GREY = []byte("\x1b[38;2;111;111;111m") 9 | var ESC = []byte{0x5c, 0x78, 0x31, 0x62, 0x5b} 10 | var CLEAR = []byte("\x1b[0m") 11 | 12 | // var CLEAR = []byte{0x5c, 0x78, 0x31, 0x62, 0x5b, 0x30, 0x6d} 13 | 14 | type Color struct { 15 | disable bool 16 | values [256]string 17 | cvalues [256][]byte 18 | } 19 | 20 | // check for NO_COLOR env var and block color 21 | func HasNoColorEnvVar() bool { 22 | _, hasEnv := os.LookupEnv("NO_COLOR") 23 | return hasEnv 24 | } 25 | 26 | func (c *Color) Compute() { 27 | const WHITEB = "\x1b[1;37m" 28 | for i := 0; i < 256; i++ { 29 | var fg, bg string 30 | 31 | lowVis := i == 0 || (i >= 16 && i <= 20) || (i >= 232 && i <= 242) 32 | 33 | if lowVis { 34 | fg = WHITEB + "\x1b[38;5;" + "255" + "m" 35 | bg = "\x1b[48;5;" + strconv.Itoa(int(i)) + "m" 36 | } else { 37 | fg = "\x1b[38;5;" + strconv.Itoa(int(i)) + "m" 38 | bg = "" 39 | } 40 | c.values[i] = bg + fg 41 | c.cvalues[i] = []byte(bg + fg) 42 | } 43 | } 44 | 45 | func (c *Color) Colorize(s string, clr byte) string { 46 | const NOCOLOR = "\x1b[0m" 47 | return c.values[clr] + s + NOCOLOR 48 | } 49 | 50 | // function to colorize bytes - avoiding string conversions 51 | func (c *Color) Colorize2(clr byte) ([]byte, []byte) { 52 | return c.cvalues[clr], CLEAR 53 | } 54 | -------------------------------------------------------------------------------- /cmd/hexxy/config/hexxy.ini: -------------------------------------------------------------------------------- 1 | [Application Options] 2 | ;; Add default flags to hexxy 3 | ;; the format is: flag-name=value 4 | ;; flags passed on the command line will override this config file 5 | ;; many of these flags don't make sense to use a config file for 6 | ;; so take care when messing with options here 7 | ;; and the most relevant are at the top 8 | 9 | ; this option forces color output [always|auto|never] 10 | color=always 11 | 12 | ; show delimiter bars in ascii table 13 | bars=true 14 | 15 | ; separator character for the ascii character table 16 | ; "│" Some characters that work well: "▏", "┆", "┊", "⸽", "│" 17 | ; or a "|" for 18 | separator=│ 19 | 20 | ; use color in the ascii table 21 | ; no-ascii-color=false 22 | 23 | ; print offset in [d|o|x] format 24 | ; radix=d 25 | 26 | ; toggle autoskip (replaces blank lines with a *) 27 | ; autoskip=false 28 | 29 | ; output hex in UPPERCASE format 30 | ; upper=false 31 | 32 | ; override the column count 33 | ; columns= 34 | 35 | ; group size of bytes (defaults to sets of 2) 36 | ; groups= 37 | 38 | ; do not print output with color, overrides all other color options 39 | ; no-color=false 40 | ; 41 | ; print debugging information and verbose output 42 | ; verbose=false 43 | 44 | ; output in binary format (01010101) incompatible with plain, reverse and include 45 | ; binary=false 46 | 47 | ; re-assemble hexdump output back into binary 48 | ; reverse=false 49 | 50 | ; start at bytes 51 | ; seek= 52 | 53 | ; stop after octets 54 | ; len= 55 | 56 | ; plain output without ascii table and offset row [often used with hexxy -r] 57 | ; plain= 58 | 59 | ; output in C include format 60 | ; include=false 61 | 62 | ; automatically output to file instead of STDOUT 63 | ; output= 64 | 65 | -------------------------------------------------------------------------------- /cmd/hexxy/encode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | reverseHexTable = "" + 10 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 11 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 12 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 13 | "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\xff\xff\xff\xff\xff\xff" + 14 | "\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 15 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 16 | "\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 17 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 18 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 19 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 20 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 21 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 22 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 23 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 24 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + 25 | "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" 26 | ) 27 | 28 | var ErrLength = errors.New("encoding/hex: odd length hex string") 29 | 30 | // InvalidByteError values describe errors resulting from an invalid byte in a hex string. 31 | type InvalidByteError byte 32 | 33 | func (e InvalidByteError) Error() string { 34 | return fmt.Sprintf("encoding/hex: invalid byte: %#U", rune(e)) 35 | } 36 | 37 | func binaryEncode(dst, src []byte) { 38 | d := uint(0) 39 | _, _ = src[0], dst[7] 40 | for i := 7; i >= 0; i-- { 41 | if src[0]&(1< -1 if space found where k is index of space byte 52 | func binaryDecode(dst, src []byte) int { 53 | var v, d byte 54 | 55 | for i := 0; i < len(src); i++ { 56 | v, d = src[i], d<<1 57 | if isSpace(v) { // found a space, so between groups 58 | if i == 0 { 59 | return 1 60 | } 61 | return i 62 | } 63 | if v == '1' { 64 | d ^= 1 65 | } else if v != '0' { 66 | return i // will catch issues like "000000: " 67 | } 68 | } 69 | 70 | dst[0] = d 71 | return -1 72 | } 73 | 74 | func cfmtEncode(dst, src []byte, hextable string) { 75 | b := src[0] 76 | dst[3] = hextable[b&0x0f] 77 | dst[2] = hextable[b>>4] 78 | dst[1] = 'x' 79 | dst[0] = '0' 80 | } 81 | 82 | // copied from encoding/hex package in order to add support for uppercase hex 83 | func hexEncode(dst, src []byte, hextable string) { 84 | b := src[0] 85 | dst[1] = hextable[b&0x0f] 86 | dst[0] = hextable[b>>4] 87 | } 88 | 89 | // copied from encoding/hex package 90 | // returns -1 on bad byte or space (\t \s \n) 91 | // returns -2 on two consecutive spaces 92 | // returns 0 on success 93 | 94 | func hexDecode(dst, src []byte) (int, error) { 95 | i, j := 0, 1 96 | for ; j < len(src); j += 2 { 97 | p := src[j-1] 98 | q := src[j] 99 | 100 | a := reverseHexTable[p] 101 | b := reverseHexTable[q] 102 | if a > 0x0f { 103 | return i, InvalidByteError(p) 104 | } 105 | if b > 0x0f { 106 | return i, InvalidByteError(q) 107 | } 108 | dst[i] = (a << 4) | b 109 | i++ 110 | } 111 | if len(src)%2 == 1 { 112 | // Check for invalid char before reporting bad length, 113 | // since the invalid char (if present) is an earlier problem. 114 | if reverseHexTable[src[j-1]] > 0x0f { 115 | return i, InvalidByteError(src[j-1]) 116 | } 117 | return i, ErrLength 118 | } 119 | return i, nil 120 | } 121 | 122 | // copied from encoding/hex package 123 | func fromHexChar(c byte) (byte, bool) { 124 | switch { 125 | case '0' <= c && c <= '9': 126 | return c - '0', true 127 | case 'a' <= c && c <= 'f': 128 | return c - 'a' + 10, true 129 | case 'A' <= c && c <= 'F': 130 | return c - 'A' + 10, true 131 | } 132 | 133 | return 0, false 134 | } 135 | 136 | // check if entire line is full of isEmpty []byte{0} bytes (nul in C) 137 | func isEmpty(b *[]byte) bool { 138 | for i := 0; i < len(*b); i++ { 139 | if (*b)[i] != 0 { 140 | return false 141 | } 142 | } 143 | return true 144 | } 145 | 146 | // check if filename character contains problematic characters 147 | func isSpecial(b byte) bool { 148 | switch b { 149 | case '/', '!', '#', '$', '%', '^', '&', '*', '(', ')', ';', ':', '|', '{', '}', '\\', '~', '`': 150 | return true 151 | default: 152 | return false 153 | } 154 | } 155 | 156 | // quick binary tree check 157 | // probably horribly written idk it's late at night 158 | func parseSpecifier(b string) float64 { 159 | lb := len(b) 160 | if lb == 0 { 161 | return 0 162 | } 163 | 164 | var b0, b1 byte 165 | if lb < 2 { 166 | b0 = b[0] 167 | b1 = '0' 168 | } else { 169 | b1 = b[1] 170 | b0 = b[0] 171 | } 172 | 173 | if b1 != '0' { 174 | if b1 == 'b' { // bits, so convert bytes to bits for os.Seek() 175 | if b0 == 'k' || b0 == 'K' { 176 | return 0.0078125 177 | } 178 | 179 | if b0 == 'm' || b0 == 'M' { 180 | return 7.62939453125e-06 181 | } 182 | 183 | if b0 == 'g' || b0 == 'G' { 184 | return 7.45058059692383e-09 185 | } 186 | } 187 | 188 | if b1 == 'B' { // kilo/mega/giga- bytes are assumed 189 | if b0 == 'k' || b0 == 'K' { 190 | return 1024 191 | } 192 | 193 | if b0 == 'm' || b0 == 'M' { 194 | return 1048576 195 | } 196 | 197 | if b0 == 'g' || b0 == 'G' { 198 | return 1073741824 199 | } 200 | } 201 | } else { // kilo/mega/giga- bytes are assumed for single b, k, m, g 202 | if b0 == 'k' || b0 == 'K' { 203 | return 1024 204 | } 205 | 206 | if b0 == 'm' || b0 == 'M' { 207 | return 1048576 208 | } 209 | 210 | if b0 == 'g' || b0 == 'G' { 211 | return 1073741824 212 | } 213 | } 214 | 215 | return 1 // assumes bytes as fallback 216 | } 217 | 218 | // is byte a space? (\t, \n, \s) 219 | func isSpace(b byte) bool { 220 | switch b { 221 | case 32, 12, 9: 222 | return true 223 | default: 224 | return false 225 | } 226 | } 227 | 228 | // are the two bytes hex prefixes? (0x or 0X) 229 | func isPrefix(b []byte) bool { 230 | return b[0] == '0' && (b[1] == 'x' || b[1] == 'X') 231 | } 232 | -------------------------------------------------------------------------------- /cmd/hexxy/hexxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | _ "embed" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "path" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/jessevdk/go-flags" 16 | ) 17 | 18 | var opts struct { 19 | OffsetFormat string `short:"t" long:"radix" default:"x" choice:"d" choice:"o" choice:"x" description:"Print offset in [d|o|x] format"` 20 | Binary bool `short:"b" long:"binary" description:"output in binary format (01010101) incompatible with plain, reverse and include"` 21 | Reverse bool `short:"r" long:"reverse" description:"re-assemble hexdump output back into binary"` 22 | Autoskip bool `short:"a" long:"autoskip" description:"toggle autoskip (replaces blank lines with a *)"` 23 | Bars bool `short:"B" long:"bars" description:"print delimiter bars in ascii table"` 24 | Separator string ` long:"separator" description:"separator character for the ascii character table"` 25 | Seek int64 `short:"s" long:"seek" description:"start at bytes"` 26 | Len int64 `short:"l" long:"len" description:"stop after octets"` 27 | Columns int `short:"c" long:"columns" description:"column count"` 28 | GroupSize int `short:"g" long:"groups" description:"group size of bytes"` 29 | Plain bool `short:"p" long:"plain" description:"plain output without ascii table and offset row [often used with hexxy -r]"` 30 | Upper bool `short:"u" long:"upper" description:"output hex in UPPERCASE format"` 31 | CInclude bool `short:"i" long:"include" description:"output in C include format"` 32 | OutputFile string `short:"o" long:"output" description:"automatically output to file instead of STDOUT"` 33 | Color string `short:"C" long:"color" default:"auto" choice:"always" choice:"auto" choice:"never" description:"this option forces color output [always|auto|never]"` 34 | NoColor bool `short:"n" long:"no-color" description:"do not print output with color"` 35 | Verbose bool `short:"v" long:"verbose" description:"print debugging information and verbose output"` 36 | WriteConfig bool `short:"W" long:"create-config" description:"create the default config file"` 37 | NoConfig bool `short:"N" long:"no-config" description:"create the default config file"` 38 | AsciiColor bool `short:"A" long:"no-ascii-color" description:"use color in the ascii table"` 39 | } 40 | 41 | var Debug = func(string, ...interface{}) {} 42 | 43 | const ( 44 | dumpHex = iota 45 | dumpBinary 46 | dumpCformat 47 | dumpPlain 48 | ) 49 | 50 | const ( 51 | udigits = "0123456789ABCDEF" 52 | ldigits = "0123456789abcdef" 53 | ) 54 | 55 | var ( 56 | dumpType int 57 | space = []byte(" ") 58 | doubleSpace = []byte(" ") 59 | dot = []byte(".") 60 | newLine = []byte("\n") 61 | zeroHeader = []byte("0000000: ") 62 | unsignedChar = []byte("unsigned char ") 63 | unsignedInt = []byte("};\nunsigned int ") 64 | lenEquals = []byte("_len = ") 65 | brackets = []byte("[] = {") 66 | asterisk = []byte("*") 67 | commaSpace = []byte(", ") 68 | comma = []byte(",") 69 | semiColonNl = []byte(";\n") 70 | bar = []byte("┊") 71 | ) 72 | 73 | var ( 74 | USE_COLOR bool 75 | ) 76 | 77 | func inputIsPipe() bool { 78 | stat, _ := os.Stdin.Stat() 79 | return stat.Mode()&os.ModeCharDevice != os.ModeCharDevice 80 | } 81 | 82 | func outputIsPipe() bool { 83 | stat, _ := os.Stdout.Stat() 84 | return stat.Mode()&os.ModeCharDevice != os.ModeCharDevice 85 | } 86 | 87 | func HexxyDump(r io.Reader, w io.Writer, filename string, color *Color) error { 88 | var ( 89 | lineOffset int64 90 | hexOffset = make([]byte, 6) 91 | groupSize int 92 | cols int 93 | octs int 94 | caps = ldigits 95 | doCheader = true 96 | doCEnd bool 97 | varDeclChar = make([]byte, 14+len(filename)+6) // for "unsigned char NAME_FORMAT[] = {" 98 | varDeclInt = make([]byte, 16+len(filename)+7) // enough room for "unsigned int NAME_FORMAT = " 99 | nulLine int64 100 | totalOcts int64 101 | colFmt int 102 | ) 103 | 104 | if dumpType == dumpCformat { 105 | _ = copy(varDeclChar[0:14], unsignedChar[:]) 106 | _ = copy(varDeclInt[0:16], unsignedInt[:]) 107 | 108 | for i := 0; i < len(filename); i++ { 109 | if !isSpecial(filename[i]) { 110 | varDeclChar[14+i] = filename[i] 111 | varDeclInt[16+i] = filename[i] 112 | } else { 113 | varDeclChar[14+i] = '_' 114 | varDeclInt[16+i] = '_' 115 | } 116 | } 117 | // copy "[] = {" and "_len = " 118 | _ = copy(varDeclChar[14+len(filename):], brackets[:]) 119 | _ = copy(varDeclInt[16+len(filename):], lenEquals[:]) 120 | } 121 | 122 | if opts.Upper { 123 | caps = udigits 124 | } 125 | 126 | if opts.Columns == -1 { 127 | switch dumpType { 128 | case dumpPlain: 129 | cols = 30 130 | case dumpCformat: 131 | cols = 12 132 | case dumpBinary: 133 | cols = 6 134 | default: 135 | cols = 16 136 | } 137 | } else { 138 | cols = opts.Columns 139 | } 140 | 141 | switch dumpType { 142 | case dumpBinary: 143 | octs = 8 144 | groupSize = 1 145 | case dumpPlain: 146 | octs = 0 147 | case dumpCformat: 148 | octs = 4 149 | default: 150 | octs = 2 151 | groupSize = 2 152 | } 153 | 154 | if opts.GroupSize != -1 { 155 | groupSize = opts.GroupSize 156 | } 157 | 158 | if opts.Len != -1 { 159 | if opts.Len < int64(cols) { 160 | cols = int(opts.Len) 161 | } 162 | } 163 | 164 | if octs < 1 { 165 | octs = cols 166 | } 167 | 168 | switch opts.OffsetFormat { 169 | case "d": 170 | colFmt = 10 171 | case "o": 172 | colFmt = 8 173 | case "x": 174 | fallthrough 175 | default: 176 | colFmt = 16 177 | } 178 | 179 | // allocate their size based on the users specs, hence why its declared here 180 | var ( 181 | line = make([]byte, cols) 182 | char = make([]byte, octs) 183 | ) 184 | 185 | c := int64(0) 186 | nl := int64(0) 187 | r = bufio.NewReader(r) 188 | 189 | var ( 190 | n int 191 | err error 192 | ) 193 | 194 | for { 195 | n, err = io.ReadFull(r, line) 196 | if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { 197 | return fmt.Errorf("hexxy: %v", err) 198 | } 199 | 200 | // we check early on for if the dump type is "plain" (no formatting, its just a stream of bytes) 201 | // and we don't have to do any hard work 202 | if dumpType == dumpPlain && n != 0 { 203 | for i := 0; i < n; i++ { 204 | hexEncode(char, line[i:i+1], caps) 205 | w.Write(char) 206 | c++ 207 | } 208 | continue 209 | } 210 | 211 | // write the line ending based on the dump "mode" 212 | if n == 0 { 213 | if dumpType == dumpPlain { 214 | w.Write(newLine) 215 | } 216 | 217 | if dumpType == dumpCformat { 218 | doCEnd = true 219 | } else { 220 | return nil 221 | } 222 | } 223 | 224 | if opts.Len != -1 { 225 | if totalOcts == opts.Len { 226 | break 227 | } 228 | totalOcts += opts.Len 229 | } 230 | 231 | if opts.Autoskip && isEmpty(&line) { 232 | if nulLine == 1 { 233 | w.Write(asterisk) 234 | w.Write(newLine) 235 | } 236 | 237 | nulLine++ 238 | 239 | if nulLine > 1 { 240 | lineOffset++ // still increment offset while printing crunched lines with '*' 241 | continue 242 | } 243 | } 244 | 245 | // hex or binary formats only 246 | // writing the 0000000: part 247 | if dumpType <= dumpBinary { 248 | // create line offset 249 | hexOffset = strconv.AppendInt(hexOffset[0:0], lineOffset, colFmt) 250 | 251 | // confusing looking but we are just "slicing" our zero padding buffer and our offset byte buffer together 252 | // ie zeroHeader = 0000000: and hexOffset = 10 -- we are just inserting that 10 in this position '0000010:' 253 | // based on the offsets length 254 | if USE_COLOR { 255 | w.Write([]byte(GREY)) 256 | w.Write(zeroHeader[0:(6 - len(hexOffset))]) 257 | w.Write(hexOffset) 258 | w.Write(zeroHeader[6:]) 259 | w.Write([]byte(CLEAR)) 260 | } else { 261 | w.Write(zeroHeader[0:(6 - len(hexOffset))]) 262 | w.Write(hexOffset) 263 | w.Write(zeroHeader[6:]) 264 | } 265 | 266 | lineOffset++ 267 | } else if doCheader { 268 | w.Write(varDeclChar) 269 | w.Write(newLine) 270 | doCheader = false 271 | } 272 | 273 | if dumpType == dumpBinary { 274 | // dump binary values 275 | for i, k := 0, octs; i < n; i, k = i+1, k+octs { 276 | binaryEncode(char, line[i:i+1]) 277 | 278 | if USE_COLOR { 279 | for _, b := range char { 280 | if b == '1' { 281 | w.Write([]byte("\x1b[32m")) 282 | w.Write([]byte{b}) 283 | w.Write([]byte(CLEAR)) 284 | } else { 285 | w.Write([]byte("\x1b[34m")) 286 | w.Write([]byte{b}) 287 | w.Write([]byte(CLEAR)) 288 | } 289 | } 290 | // w.Write(char) 291 | c++ 292 | } else { 293 | w.Write(char) 294 | c++ 295 | } 296 | 297 | if k == octs*groupSize { 298 | k = 0 299 | w.Write(space) 300 | } 301 | } 302 | } else if dumpType == dumpCformat { 303 | // dump C format 304 | if !doCEnd { 305 | w.Write(doubleSpace) 306 | } 307 | for i := 0; i < n; i++ { 308 | cfmtEncode(char, line[i:i+1], caps) 309 | w.Write(char) 310 | c++ 311 | // no space at EOL 312 | if i != n-1 { 313 | w.Write(commaSpace) 314 | } else if n == cols { 315 | w.Write(comma) 316 | } 317 | } 318 | } else { 319 | // hex values -- default 320 | for i, k := 0, octs; i < n; i, k = i+1, k+octs { 321 | hexEncode(char, line[i:i+1], caps) 322 | 323 | if USE_COLOR { 324 | i := line[i : i+1][0] 325 | b, c := color.Colorize2(i) 326 | w.Write(b) 327 | w.Write(char) 328 | w.Write(c) 329 | } else { 330 | w.Write(char) 331 | } 332 | c++ 333 | 334 | if k == octs*groupSize { 335 | k = 0 336 | w.Write(space) 337 | } 338 | } 339 | } 340 | 341 | if doCEnd { 342 | w.Write(varDeclInt) 343 | w.Write([]byte(strconv.FormatInt(c, 10))) 344 | w.Write(semiColonNl) 345 | return nil 346 | } 347 | 348 | if n < len(line) && dumpType <= dumpBinary { 349 | for i := n * octs; i < len(line)*octs; i++ { 350 | w.Write(space) 351 | 352 | if i%octs == 1 { 353 | w.Write(space) 354 | } 355 | } 356 | } 357 | 358 | if dumpType != dumpCformat { 359 | w.Write(space) 360 | } 361 | 362 | if dumpType <= dumpBinary { 363 | // character values 364 | b := line[:n] 365 | // |hello,.world!| 366 | if opts.Bars { 367 | if USE_COLOR { 368 | w.Write([]byte(GREY)) 369 | w.Write(bar) 370 | w.Write(CLEAR) 371 | } else { 372 | w.Write(bar) 373 | } 374 | } 375 | 376 | var v byte 377 | for i := 0; i < len(b); i++ { 378 | v = b[i] 379 | 380 | if USE_COLOR && !opts.AsciiColor { 381 | if v > 0x1f && v < 0x7f { 382 | charByte := line[i : i+1][0] 383 | b, c := color.Colorize2(charByte) 384 | w.Write(b) 385 | w.Write(line[i : i+1]) 386 | w.Write(c) 387 | } else { 388 | w.Write([]byte(GREY)) 389 | w.Write(dot) 390 | w.Write(CLEAR) 391 | } 392 | } else { 393 | if v > 0x1f && v < 0x7f { 394 | w.Write(line[i : i+1]) 395 | } else { 396 | w.Write(dot) 397 | } 398 | } 399 | } 400 | 401 | if opts.Bars { 402 | if USE_COLOR { 403 | w.Write([]byte(GREY)) 404 | w.Write(bar) 405 | w.Write(CLEAR) 406 | } else { 407 | w.Write(bar) 408 | } 409 | } 410 | } 411 | 412 | w.Write(newLine) 413 | nl++ 414 | } 415 | 416 | return nil 417 | } 418 | 419 | func Hexxy(args []string) error { 420 | color := &Color{} 421 | 422 | if opts.NoColor || !USE_COLOR { 423 | color.disable = true 424 | } 425 | 426 | if !color.disable { 427 | color.Compute() // precompute this at compile time? 428 | } 429 | 430 | var ( 431 | infile *os.File 432 | outfile *os.File 433 | err error 434 | ) 435 | 436 | if len(args) < 1 && inputIsPipe() { 437 | infile = os.Stdin 438 | } else { 439 | infile, err = os.Open(args[0]) 440 | if err != nil { 441 | return fmt.Errorf("hexxy: %v", err.Error()) 442 | } 443 | } 444 | 445 | defer infile.Close() 446 | 447 | if opts.Seek != -1 { 448 | _, err = infile.Seek(opts.Seek, io.SeekStart) 449 | if err != nil { 450 | return fmt.Errorf("hexxy: %v", err.Error()) 451 | } 452 | } 453 | 454 | if opts.OutputFile != "" { 455 | outfile, err = os.Open(opts.OutputFile) 456 | if err != nil { 457 | return fmt.Errorf("hexxy: %v", err.Error()) 458 | } 459 | } else { 460 | outfile = os.Stdout 461 | } 462 | defer outfile.Close() 463 | 464 | switch { 465 | case opts.Binary: 466 | dumpType = dumpBinary 467 | case opts.CInclude: 468 | dumpType = dumpCformat 469 | case opts.Plain: 470 | dumpType = dumpPlain 471 | default: 472 | dumpType = dumpHex 473 | } 474 | 475 | out := bufio.NewWriter(outfile) 476 | defer out.Flush() 477 | 478 | if opts.Reverse { 479 | if err := HexxyReverse(infile, outfile); err != nil { 480 | return fmt.Errorf("hexxy: %v", err.Error()) 481 | } 482 | return nil 483 | } 484 | 485 | if err := HexxyDump(infile, out, infile.Name(), color); err != nil { 486 | return fmt.Errorf("hexxy: %v", err.Error()) 487 | } 488 | 489 | return nil 490 | } 491 | 492 | const usage_msg = ` 493 | hexxy is a command line hex dumping tool 494 | 495 | Examples: 496 | hexxy [OPTIONS] input-file 497 | 498 | # Include a binary as a C variable 499 | hexxy -i input-file > output.c 500 | 501 | # Use plain non-formatted output 502 | hexxy -p input-file 503 | 504 | # Reverse plain non-formatted output (reverse plain) 505 | hexxy -rp input-file 506 | 507 | # Show output with a space in between N groups of bytes 508 | hexxy -g1 input-file ... -> outputs: 00000000: 0f 1a ff ff 00 aa 509 | 510 | # Seek to N bytes in an input file 511 | hexxy -s 12546 input-file 512 | ` 513 | 514 | // extra usage examples 515 | func usage() { 516 | fmt.Fprint(os.Stderr, usage_msg) 517 | } 518 | 519 | // parses the color flag and decides whether color is appropriate or not 520 | func useColor() bool { 521 | // NO_COLOR spec compliance 522 | if HasNoColorEnvVar() { 523 | return false 524 | } 525 | 526 | if opts.NoColor { 527 | return false 528 | } 529 | 530 | switch strings.ToLower(opts.Color) { 531 | case "always": 532 | return true 533 | case "auto": 534 | if opts.NoColor { 535 | return false 536 | } 537 | 538 | return !outputIsPipe() 539 | case "never": 540 | return false 541 | } 542 | return false 543 | } 544 | 545 | func init() { 546 | opts.Seek = -1 // default no-op values 547 | opts.Columns = -1 548 | opts.GroupSize = -1 549 | opts.Len = -1 550 | } 551 | 552 | func configPath() string { 553 | cpath, _ := os.UserConfigDir() 554 | if len(cpath) == 0 { 555 | 556 | hdir, _ := os.UserHomeDir() 557 | if len(hdir) == 0 { 558 | return "hexxy.ini" 559 | } 560 | 561 | return path.Join(hdir, ".hexxy.ini") 562 | } 563 | 564 | return path.Join(cpath, "hexxy", "hexxy.ini") 565 | } 566 | 567 | //go:embed config/hexxy.ini 568 | var defaultConfig string 569 | 570 | func createConfig() error { 571 | conf := configPath() 572 | dirs := path.Dir(conf) 573 | 574 | if err := os.MkdirAll(dirs, 0o755); err != nil { 575 | return err 576 | } 577 | 578 | f, err := os.OpenFile(conf, os.O_RDWR|os.O_CREATE, 0o600) 579 | if err != nil { 580 | return err 581 | } 582 | defer f.Close() 583 | 584 | _, err = f.WriteString(defaultConfig) 585 | return err 586 | } 587 | 588 | // this is jank 589 | func noConfig() bool { 590 | for _, arg := range os.Args { 591 | if arg == "--no-config" { 592 | return true 593 | } 594 | } 595 | return false 596 | } 597 | 598 | func main() { 599 | parser := flags.NewParser(&opts, flags.Default) 600 | 601 | if !noConfig() { 602 | ini := flags.NewIniParser(parser) 603 | // parse config first 604 | if err := ini.ParseFile(configPath()); err != nil { 605 | if !errors.Is(err, os.ErrNotExist) { 606 | log.Printf("error parsing config file: %v", err) 607 | } 608 | } 609 | } 610 | 611 | // overwrites config values if provided by user 612 | args, err := parser.Parse() 613 | if flags.WroteHelp(err) { 614 | usage() 615 | os.Exit(0) 616 | } 617 | if err != nil { 618 | log.Fatal(err) 619 | } 620 | 621 | if opts.WriteConfig { 622 | err := createConfig() 623 | if err != nil { 624 | log.Fatal(err) 625 | } 626 | 627 | log.Printf("wrote config file at %s", configPath()) 628 | os.Exit(0) 629 | } 630 | 631 | // set color based on flags or default to off 632 | USE_COLOR = useColor() 633 | 634 | if !inputIsPipe() && len(args) == 0 { 635 | parser.WriteHelp(os.Stderr) 636 | fmt.Print(usage_msg) 637 | os.Exit(0) 638 | } 639 | 640 | if opts.Verbose { 641 | Debug = log.Printf 642 | } 643 | 644 | if opts.Separator != "" { 645 | bar = []byte(opts.Separator) 646 | } 647 | 648 | if err := Hexxy(args); err != nil { 649 | log.Fatal(err) 650 | } 651 | } 652 | -------------------------------------------------------------------------------- /cmd/hexxy/reverse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | func HexxyReverse(r io.Reader, w io.Writer) error { 11 | var ( 12 | cols int 13 | octs int 14 | char = make([]byte, 1) 15 | ) 16 | 17 | if opts.Columns != -1 { 18 | cols = opts.Columns 19 | } 20 | 21 | switch dumpType { 22 | case dumpBinary: 23 | octs = 8 24 | case dumpCformat: 25 | octs = 4 26 | default: 27 | octs = 2 28 | } 29 | 30 | if opts.Len != -1 { 31 | if opts.Len < int64(cols) { 32 | cols = int(opts.Len) 33 | } 34 | } 35 | 36 | if octs < 1 { 37 | octs = cols 38 | } 39 | 40 | // character count 41 | c := int64(0) 42 | rd := bufio.NewReader(r) 43 | for { 44 | // TODO this is causing issues with plain 45 | line, err := rd.ReadBytes('\n') 46 | n := len(line) 47 | if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { 48 | return fmt.Errorf("hexxy: %v", err) 49 | } 50 | 51 | if n == 0 { 52 | return nil 53 | } 54 | 55 | if dumpType == dumpHex { 56 | // line = line[9:48] 57 | line = line[9 : len(line)-19] 58 | n := len(line) 59 | 60 | for i := 0; i <= n; { 61 | // print(string(line[i : i+4])) 62 | 63 | if rv, _ := hexDecode(char, line[i:i+octs]); rv != 0 { 64 | w.Write(char) 65 | } 66 | 67 | i += 5 68 | // time.Sleep(time.Millisecond * 500) 69 | } 70 | 71 | // for i := 0; n >= octs+1; { 72 | // print(string(line[i : i+octs])) 73 | // time.Sleep(time.Millisecond * 500) 74 | // if rv, _ := hexDecode(char, line[i:i+octs]); rv != 0 { 75 | // w.Write(char) 76 | // i += 2 77 | // n -= 2 78 | // c++ 79 | // } else if rv == -1 { 80 | // i++ 81 | // n-- 82 | // } else { 83 | // // rv == -2 84 | // i += 2 85 | // n -= 2 86 | // } 87 | // } 88 | } else if dumpType == dumpBinary { 89 | for i := 0; n >= octs; { 90 | if binaryDecode(char, line[i:i+octs]) != -1 { 91 | i++ 92 | n-- 93 | continue 94 | } else { 95 | w.Write(char) 96 | i += 8 97 | n -= 8 98 | c++ 99 | } 100 | } 101 | } else if dumpType == dumpPlain { 102 | for i := 0; n >= octs; i += octs { 103 | if rv, _ := hexDecode(char, line[i:i+octs]); rv != 0 { 104 | w.Write(char) 105 | c++ 106 | } 107 | n -= octs 108 | } 109 | } else if dumpType == dumpCformat { 110 | for i := 0; n >= octs; { 111 | if rv, _ := hexDecode(char, line[i:i+octs]); rv == 0 { 112 | w.Write(char) 113 | i += 4 114 | n -= 4 115 | c++ 116 | } else if rv == -1 { 117 | i++ 118 | n-- 119 | } else { // rv == -2 120 | i += 2 121 | n -= 2 122 | } 123 | } 124 | } 125 | 126 | if c == int64(cols) && cols > 0 { 127 | return nil 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sweetbbak/hexxy 2 | 3 | go 1.21.5 4 | 5 | require github.com/jessevdk/go-flags v1.5.0 6 | 7 | require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 2 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 3 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= 4 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: 2 | go build -ldflags='-s -w' ./src 3 | --------------------------------------------------------------------------------