├── 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 | 
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 |
--------------------------------------------------------------------------------