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 """ def format_hexdump_output(term, opts \\ @default_hexdump_inspect_opts) do {:ok, string_io} = StringIO.open(term) result = string_io |> IO.binstream(2) |> Stream.chunk_every(8) |> take_or_infinity(opts.printable_limit) |> 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 <> do if Enum.member?(@printable_range, char), do: <>, else: "." end } ) |> Stream.with_index() |> Enum.map_join(@newline, fn {{chunk, original_text}, index} -> [ 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, " "), @column_divider, original_text ] 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