|
4 | 4 | [clj-commons.pretty-impl :refer [padding]])
|
5 | 5 | (:import (java.nio ByteBuffer)))
|
6 | 6 |
|
| 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 | + |
7 | 22 | (defprotocol BinaryData
|
8 | 23 | "Allows various data sources to be treated as a byte-array data type that
|
9 | 24 | supports a length and random access to individual bytes.
|
10 | 25 |
|
11 | 26 | BinaryData is extended onto byte arrays, java.nio.ByteBuffer, java.lang.String, java.lang.StringBuilder, and onto nil."
|
12 | 27 |
|
13 | 28 | (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.")) |
15 | 30 |
|
16 | 31 | (extend-type (Class/forName "[B")
|
17 | 32 | BinaryData
|
|
46 | 61 | (def ^:private ^:const bytes-per-ascii-line 16)
|
47 | 62 | (def ^:private ^:const bytes-per-line (* 2 bytes-per-diff-line))
|
48 | 63 |
|
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 |
55 | 78 |
|
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))) |
57 | 88 |
|
58 | 89 | (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:")) |
63 | 110 |
|
64 | 111 | (defn- write-line
|
65 |
| - [write-ascii? offset data line-count per-line] |
| 112 | + [write-ascii? offset-format offset data line-count per-line] |
66 | 113 | (let [line-bytes (for [i (range line-count)]
|
67 |
| - (byte-at data (+ offset i)))] |
| 114 | + (Byte/toUnsignedLong (byte-at data (+ offset i))))] |
68 | 115 | (println
|
69 | 116 | (compose
|
70 |
| - (format "%04X:" offset) |
| 117 | + [(:offset *fonts*) |
| 118 | + (format offset-format offset)] |
71 | 119 | (for [b line-bytes]
|
72 |
| - (format " %02X" b)) |
| 120 | + (list " " |
| 121 | + [(font-for-byte b) |
| 122 | + (format "%02X" b)])) |
73 | 123 | (when write-ascii?
|
74 | 124 | (list
|
75 | 125 | (padding (* 3 (- per-line line-count)))
|
|
102 | 152 | 0020: 72 65 20 74 68 61 74 20 74 61 6B 65 73 20 79 6F |re that takes yo|
|
103 | 153 | 0030: 75 2E |u. |
|
104 | 154 |
|
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*]]." |
107 | 156 | ([data]
|
108 | 157 | (print-binary data nil))
|
109 | 158 | ([data options]
|
110 | 159 | (let [{show-ascii? :ascii
|
111 | 160 | per-line-option :line-bytes} options
|
112 | 161 | 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)] |
114 | 165 | (assert (pos? per-line) "must be at least one byte per line")
|
115 | 166 | (loop [offset 0]
|
116 |
| - (let [remaining (- (data-length data) offset)] |
| 167 | + (let [remaining (- input-length offset)] |
117 | 168 | (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) |
119 | 170 | (recur (long (+ per-line offset)))))))))
|
120 | 171 |
|
121 | 172 | (defn format-binary
|
|
133 | 184 | (< byte-offset alternate-length)
|
134 | 185 | (== (byte-at data byte-offset) (byte-at alternate byte-offset))))
|
135 | 186 |
|
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)]))] |
145 | 196 | (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 | + |
148 | 200 | ;; 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]) |
150 | 202 | ;; 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 "--"]))))) |
157 | 204 |
|
158 | 205 | (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)))) |
165 | 216 |
|
166 | 217 | (defn print-binary-delta
|
167 | 218 | "Formats a hex dump of the expected data (on the left) and actual data (on the right). Bytes
|
|
175 | 226 | [expected actual]
|
176 | 227 | (let [expected-length (data-length expected)
|
177 | 228 | 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))] |
179 | 231 | (loop [offset 0]
|
180 | 232 | (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) |
182 | 234 | (recur (long (+ bytes-per-diff-line offset)))))))
|
183 | 235 |
|
184 | 236 | (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." |
186 | 238 | [expected actual]
|
187 | 239 | (with-out-str
|
188 | 240 | (print-binary-delta expected actual)))
|
0 commit comments