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"
@doc """
restores previous inspect function
"""
def off do
Inspect.Opts.default_inspect_fun(&Inspect.inspect/2)
end
@default_hexdump_inspect_opts %Inspect.Opts{printable_limit: 500, 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
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
def format_hexdump_output(term, opts \\ @default_hexdump_inspect_opts) do
{:ok, string_io} = StringIO.open(term)
result =
string_io
|> IO.binstream(2)
|> Stream.take(opts.printable_limit)
|> Stream.chunk_every(8)
|> Stream.map(
&{
# generates the text: AABB CCDD EEFF 1122 3344 5566 7788 9900
Enum.map_join(&1, " ", fn two_chars -> Base.encode16(two_chars) end),
# generates the text: abc...def1234567
for <<char::size(8) <- Enum.join(&1, "")>> do
if Enum.member?(@printable_range, char), do: <<char>>, else: "."
end
}
)
|> Stream.with_index()
|> Enum.map_join(@newline, fn {{chunk, original_text}, index} ->
[
# 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, " "),
@column_divider,
original_text
]
end)
StringIO.close(string_io)
result
end
end