defmodule Hexdump do
@moduledoc """
Hexdump makes it easier to work with binary data
By default elixir display binaries as a list of integers in the range from 0..255
This make it problematic to spot binary patterns
our example binary:
```
term = <<0,1,2,3,4>> <> "123abcdefxyz" <> <<253,254,255>>
```
```
<<0, 1, 2, 3, 4, 49, 50, 51, 97, 98, 99, 100, 101, 102, 120, 121, 122, 253, 254,
255>>
```
You can pass a param to IO.inspect(term, base: :hex) to print the same term as hex,
this makes it a bit easier.
```
<<0x0, 0x1, 0x2, 0x3, 0x4, 0x31, 0x32, 0x33, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
0x78, 0x79, 0x7A, 0xFD, 0xFE, 0xFF>>
```
With Hexdump you can see similar output like hex editors have:
The first column is offset
second shows a row of 16 bits in binary
last column shows printable characers
```
0000000 0001 0203 0431 3233 6162 6364 6566 7879 .....123abcdefxy
0000010 7AFD FEFF z...
```
You can switch between hexdump output by calling:
```
Hexdump.on()
Hexdump.off()
Hexdump.on(binaries: :infer)
Hexdump.on(binaries: :as_strings)
```
"""
@printable_range 0x20..0x7F
@column_divider " "
@newline "\n"
@header " offset 0 1 2 3 4 5 6 7 8 9 A B C D E F printable data\n"
@doc """
Restores the standard inspect function
"""
def off do
Inspect.Opts.default_inspect_fun(&Inspect.inspect/2)
end
@doc """
Enables the custom inspect function
"""
@default_hexdump_inspect_opts %Inspect.Opts{printable_limit: 5, binaries: :as_binaries}
def on(opts \\ @default_hexdump_inspect_opts) do
opts =
case opts do
opts when is_struct(opts) -> Map.from_struct(opts)
opts when is_list(opts) -> Map.new(opts)
opts -> opts
end
Inspect.Opts.default_inspect_fun(&hexdump_inspect_fun(&1, struct(&2, opts)))
end
@doc """
Custom inspect function
"""
def hexdump_inspect_fun(term, opts) when not is_binary(term) do
Inspect.inspect(term, %{opts | base: :hex})
end
def hexdump_inspect_fun(term, opts) do
%Inspect.Opts{binaries: bins, printable_limit: printable_limit} = opts
if bins == :as_strings or
(bins == :infer and String.printable?(term, printable_limit)) do
Inspect.inspect(term, opts)
else
@newline <> format_hexdump_output(term, opts) <> @newline
end
end
@doc """
Formatter used in the custom inspect function
colors meaning:
- grey: zero byte 0x00
- green: whitespace
- yellow: ascii non printable
- red: non ascii char
- cyan: printable character
"""
def format_hexdump_output(term, opts \\ @default_hexdump_inspect_opts) do
{:ok, string_io} = StringIO.open(term)
result =
string_io
|> IO.binstream(1)
|> Stream.chunk_every(16)
|> take_or_infinity(opts.printable_limit)
|> Stream.map(
&for char <- &1 do
<<ascii>> = char
cond do
ascii == 0x00 ->
[IO.ANSI.light_black(), Base.encode16(char), "⋄"]
ascii == 0x20 ->
[IO.ANSI.reset(), Base.encode16(char), " "]
ascii in [0x09, 0x0A, 0x0C, 0x0D] ->
[IO.ANSI.green(), Base.encode16(char), "_"]
ascii > 0x7F ->
[IO.ANSI.light_red(), Base.encode16(char), "×"]
Enum.member?(
@printable_range,
ascii
) ->
[IO.ANSI.cyan(), Base.encode16(char), char]
true ->
[IO.ANSI.yellow(), Base.encode16(char), "•"]
end
end
)
|> Stream.with_index()
|> Enum.map_join(@newline, fn {chunk, index} ->
length = length(chunk)
chunk =
if length < 16 do
chunk ++ Enum.map(1..(16 - length), fn _ -> ["", " ", ""] end)
else
chunk
end
{binary_representation, original_text} =
chunk
|> Enum.with_index()
|> Enum.map(fn {[ascii_color, binary, printable], index} ->
space = if rem(index, 2) == 1, do: " ", else: ""
{[ascii_color, binary, space], [ascii_color, printable]}
end)
|> Enum.unzip()
[
IO.ANSI.light_black(),
if index == 0 do
@header
else
[]
end,
@column_divider,
# generates the first column 00001
String.pad_leading("#{index}", 6, "0"),
# last 0 and divider in the first column
"0:",
@column_divider,
# empty spaces for the last row when it's not full width
# String.pad_trailing(chunk, 40, " "),
binary_representation,
@column_divider,
original_text,
IO.ANSI.reset()
]
end)
StringIO.close(string_io)
result
end
defp take_or_infinity(stream, :infinity), do: stream
defp take_or_infinity(stream, limit), do: Stream.take(stream, limit)
end