Skip to content

Commit e6254c8

Browse files
authored
Merge pull request #94 from clj-commons/hls/hex-colors
Color code binary output
2 parents be6824e + 723ffc8 commit e6254c8

File tree

9 files changed

+213
-66
lines changed

9 files changed

+213
-66
lines changed

CHANGES.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ This release moves the library to clj-commons, and changes the root namespace fr
55
the `columns`, `component`, and `logging` namespaces entirely.
66

77
- Stripped out a lot of redundant documentation
8-
- Reworked the `ansi` namespace to primarily expose the `compose` function and not the dozens of constants and functions
9-
- `ansi` determines whether to enable or disable at execution time
8+
- Reworked the `ansi` namespace to primarily expose the `compose` function and not dozens of constants and functions
9+
- `ansi` determines whether to enable or disable ANSI codes at execution time
1010
- `ansi` now honors the `NO_COLOR` environment variable
1111
- Stripped out code for accessing the clipboard from the `repl` namespace
1212
- Some refactoring inside `exceptions` namespace, including changes to the `*fonts*` var
@@ -17,6 +17,7 @@ the `columns`, `component`, and `logging` namespaces entirely.
1717
- `write-exception` was renamed to `print-exception`
1818
- `write-binary` and `write-binary-delta` renamed to `print-binary` and `print-binary-delta`
1919
- `compose` can now pad a span of text with spaces (on the left or right) to a desired width
20+
- Binary output now includes color coding
2021

2122
## 1.4.4 -- 20 Jun 2023
2223

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@ or compare two streams of binary data, a little bit of formatting can go a long
1111
That's what `org.clj-commons/pretty` is for. It adds support for pretty output where it counts:
1212

1313
* Readable output for exceptions
14-
* ANSI font and background color support
15-
* Hex dump of binary data
16-
* Hex dump of binary deltas
17-
* Formatting data into columns
14+
* General ANSI font and background color support
15+
* Readable output for binary sequences
1816

1917
![Example](docs/images/formatted-exception.png)
2018

19+
Pretty can print out a sequence of bytes; it includes color-coding inspired by
20+
[hexyl](https://github.com/sharkdp/hexyl):
21+
22+
![Binary Output](docs/images/binary-output.png)
23+
24+
Pretty can also print out a delta of two byte sequences, using background color
25+
to indicate where the two sequences differ.
26+
27+
![Binary Delta](docs/images/binary-delta.png)
28+
2129
Pretty is compatible with Clojure 1.10 and above.
2230

2331
Parts of Pretty can be used with [Babashka](https://book.babashka.org/#introduction), such as the `clj-commons.ansi`

deps.edn

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
:disable-colors
2929
{:jvm-opts ["-Dclj-commons.ansi.enabled=false"]}
3030

31+
:nrepl
32+
{:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}}
33+
:main-opts ["-m" "nrepl.cmdline" ]}
34+
3135
:repl
3236
{:main-opts ["-m" "clj-commons.pretty.repl"]}}
3337

docs/images/binary-delta.png

9.45 KB
Loading

docs/images/binary-output.png

193 KB
Loading

src/clj_commons/format/binary.clj

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,29 @@
44
[clj-commons.pretty-impl :refer [padding]])
55
(:import (java.nio ByteBuffer)))
66

7+
(def ^:dynamic *fonts*
8+
"Mapping from byte category to a font (color)."
9+
{:offset :bright-black
10+
:null :bright-black
11+
:printable :cyan
12+
:whitespace :green
13+
:other :faint.green
14+
:non-ascii :yellow})
15+
16+
(def ^:private placeholders
17+
{:null ""
18+
:whitespace "_"
19+
:other ""
20+
:non-ascii "×"})
21+
722
(defprotocol BinaryData
823
"Allows various data sources to be treated as a byte-array data type that
924
supports a length and random access to individual bytes.
1025
1126
BinaryData is extended onto byte arrays, java.nio.ByteBuffer, java.lang.String, java.lang.StringBuilder, and onto nil."
1227

1328
(data-length [this] "The total number of bytes available.")
14-
(byte-at [this index] "The byte value at a specific offset."))
29+
^byte (byte-at [this index] "The byte value at a specific offset."))
1530

1631
(extend-type (Class/forName "[B")
1732
BinaryData
@@ -46,30 +61,65 @@
4661
(def ^:private ^:const bytes-per-ascii-line 16)
4762
(def ^:private ^:const bytes-per-line (* 2 bytes-per-diff-line))
4863

49-
(def ^:private printable-chars
50-
(into #{}
51-
(map byte (str "abcdefghijklmnopqrstuvwxyz"
52-
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
53-
"0123456789"
54-
" !@#$%^&*()-_=+[]{}\\|'\";:,./<>?`~"))))
64+
(def ^:private whitespace
65+
#{0x09 0x0a 0x0b 0x0c 0x0d 0x20})
66+
67+
(defn- category-for-byte
68+
[^long value]
69+
(cond
70+
(zero? value)
71+
:null
72+
73+
(< 0x7f value)
74+
:non-ascii
75+
76+
(contains? whitespace value)
77+
:whitespace
5578

56-
(defn- nonprintable-placeholder [] (compose [:bright-magenta-bg " "]))
79+
(<= 0x21 value 0x7e)
80+
:printable
81+
82+
:else
83+
:other))
84+
85+
(defn- font-for-byte
86+
[^long value]
87+
(get *fonts* (category-for-byte value)))
5788

5889
(defn- to-ascii
59-
[b]
60-
(if (printable-chars b)
61-
(char b)
62-
(nonprintable-placeholder)))
90+
[^long b]
91+
(let [category (category-for-byte b)]
92+
[(get *fonts* category)
93+
(if (or (= :printable category)
94+
(= 0x20 b))
95+
(char b)
96+
(get placeholders category))]))
97+
98+
(defn- hex-digit-count
99+
[max-length]
100+
(loop [digits 4
101+
cutoff 0xffff]
102+
(if (<= max-length cutoff)
103+
digits
104+
(recur (+ 2 digits)
105+
(* cutoff 0xff)))))
106+
107+
(defn- make-offset-format
108+
[max-length]
109+
(str "%0" (hex-digit-count max-length) "X:"))
63110

64111
(defn- write-line
65-
[write-ascii? offset data line-count per-line]
112+
[write-ascii? offset-format offset data line-count per-line]
66113
(let [line-bytes (for [i (range line-count)]
67-
(byte-at data (+ offset i)))]
114+
(Byte/toUnsignedLong (byte-at data (+ offset i))))]
68115
(println
69116
(compose
70-
(format "%04X:" offset)
117+
[(:offset *fonts*)
118+
(format offset-format offset)]
71119
(for [b line-bytes]
72-
(format " %02X" b))
120+
(list " "
121+
[(font-for-byte b)
122+
(format "%02X" b)]))
73123
(when write-ascii?
74124
(list
75125
(padding (* 3 (- per-line line-count)))
@@ -102,20 +152,21 @@
102152
0020: 72 65 20 74 68 61 74 20 74 61 6B 65 73 20 79 6F |re that takes yo|
103153
0030: 75 2E |u. |
104154
105-
A placeholder character (a space with magenta background) is used for any non-printable
106-
character."
155+
When ANSI is enabled, the individual bytes and characters are color-coded as per the [[*fonts*]]."
107156
([data]
108157
(print-binary data nil))
109158
([data options]
110159
(let [{show-ascii? :ascii
111160
per-line-option :line-bytes} options
112161
per-line (or per-line-option
113-
(if show-ascii? bytes-per-ascii-line bytes-per-line))]
162+
(if show-ascii? bytes-per-ascii-line bytes-per-line))
163+
input-length (data-length data)
164+
offset-format (make-offset-format input-length)]
114165
(assert (pos? per-line) "must be at least one byte per line")
115166
(loop [offset 0]
116-
(let [remaining (- (data-length data) offset)]
167+
(let [remaining (- input-length offset)]
117168
(when (pos? remaining)
118-
(write-line show-ascii? offset data (min per-line remaining) per-line)
169+
(write-line show-ascii? offset-format offset data (min per-line remaining) per-line)
119170
(recur (long (+ per-line offset)))))))))
120171

121172
(defn format-binary
@@ -133,35 +184,35 @@
133184
(< byte-offset alternate-length)
134185
(== (byte-at data byte-offset) (byte-at alternate byte-offset))))
135186

136-
(defn- to-hex
137-
[byte-array byte-offset]
138-
;; This could be made a lot more efficient!
139-
(format "%02X" (byte-at byte-array byte-offset)))
140-
141-
(defn- write-byte-deltas
142-
[ansi-color pad? offset data-length data alternate-length alternate]
143-
(doseq [i (range bytes-per-diff-line)]
144-
(let [byte-offset (+ offset i)]
187+
(defn- compose-deltas
188+
"Returns a composed value of one line (16 bytes) of data."
189+
[mismatch-font offset data-length data alternate-length alternate]
190+
(for [i (range bytes-per-diff-line)]
191+
(let [byte-offset (+ offset i)
192+
*value (delay
193+
(let [value (long (byte-at data byte-offset))
194+
byte-font (font-for-byte value)]
195+
[byte-font (format "%02X" value)]))]
145196
(cond
146-
;; Exact match on both sides is easy, just print it out.
147-
(match? byte-offset data-length data alternate-length alternate) (print (str " " (to-hex data byte-offset)))
197+
(match? byte-offset data-length data alternate-length alternate)
198+
(list " " @*value)
199+
148200
;; Some kind of mismatch, so decorate with this side's color
149-
(< byte-offset data-length) (print (compose " " [ansi-color (to-hex data byte-offset)]))
201+
(< byte-offset data-length) (list " " [mismatch-font @*value])
150202
;; Are we out of data on this side? Print a "--" decorated with the color.
151-
(< byte-offset alternate-length) (print (compose " "
152-
[ansi-color "--"]))
153-
;; This side must be longer than the alternate side.
154-
;; On the left/green side, we need to pad with spaces
155-
;; On the right/red side, we need nothing.
156-
pad? (print " ")))))
203+
(< byte-offset alternate-length) (list " " [mismatch-font "--"])))))
157204

158205
(defn- print-delta-line
159-
[offset expected-length ^bytes expected actual-length actual]
160-
(printf "%04X:" offset)
161-
(write-byte-deltas :bold.bright-green true offset expected-length expected actual-length actual)
162-
(print " |")
163-
(write-byte-deltas :bold.bright-red false offset actual-length actual expected-length expected)
164-
(println))
206+
[offset-format offset expected-length expected actual-length actual]
207+
(println
208+
(compose
209+
[(:offset *fonts*)
210+
(format offset-format offset)]
211+
[{:pad :right
212+
:width (* 3 bytes-per-diff-line)}
213+
(compose-deltas :bright-green-bg offset expected-length expected actual-length actual)]
214+
" |"
215+
(compose-deltas :bright-red-bg offset actual-length actual expected-length expected))))
165216

166217
(defn print-binary-delta
167218
"Formats a hex dump of the expected data (on the left) and actual data (on the right). Bytes
@@ -175,14 +226,15 @@
175226
[expected actual]
176227
(let [expected-length (data-length expected)
177228
actual-length (data-length actual)
178-
target-length (max actual-length expected-length)]
229+
target-length (max actual-length expected-length)
230+
offset-format (make-offset-format (max actual-length target-length))]
179231
(loop [offset 0]
180232
(when (pos? (- target-length offset))
181-
(print-delta-line offset expected-length expected actual-length actual)
233+
(print-delta-line offset-format offset expected-length expected actual-length actual)
182234
(recur (long (+ bytes-per-diff-line offset)))))))
183235

184236
(defn format-binary-delta
185-
"Formats the delta using [[write-binary-delta]] and returns the result as a string."
237+
"Formats the delta using [[print-binary-delta]] and returns the result as a string."
186238
[expected actual]
187239
(with-out-str
188240
(print-binary-delta expected actual)))

0 commit comments

Comments
 (0)