defmodule Odinsea.Net.Hex do @moduledoc """ Hex encoding/decoding utilities ported from Java HexTool. """ @doc """ Converts a binary to a hex string (space-separated bytes). ## Examples iex> Odinsea.Net.Hex.encode(<<0x01, 0xAB, 0xFF>>) "01 AB FF" """ @spec encode(binary()) :: String.t() def encode(<<>>), do: "" def encode(binary) when is_binary(binary) do binary |> :binary.bin_to_list() |> Enum.map(&byte_to_hex/1) |> Enum.join(" ") end @doc """ Converts a binary to a hex string (no spaces). ## Examples iex> Odinsea.Net.Hex.to_string_compact(<<0x01, 0xAB, 0xFF>>) "01ABFF" """ @spec to_string_compact(binary()) :: String.t() def to_string_compact(<<>>), do: "" def to_string_compact(binary) when is_binary(binary) do binary |> :binary.bin_to_list() |> Enum.map(&byte_to_hex/1) |> Enum.join() end @doc """ Converts a binary to a hex string with ASCII representation. ## Examples iex> Odinsea.Net.Hex.to_pretty_string(<<0x48, 0x65, 0x6C, 0x6C, 0x6F>>) "48 65 6C 6C 6F | Hello" """ @spec to_pretty_string(binary()) :: String.t() def to_pretty_string(binary) when is_binary(binary) do hex_part = encode(binary) ascii_part = to_ascii(binary) "#{hex_part} | #{ascii_part}" end @doc """ Converts a hex string to binary. ## Examples iex> Odinsea.Net.Hex.from_string("01 AB FF") <<0x01, 0xAB, 0xFF>> """ @spec from_string(String.t()) :: binary() def from_string(hex_string) when is_binary(hex_string) do hex_string |> String.replace(~r/\s+/, "") |> String.downcase() |> do_from_string() end defp do_from_string(<<>>), do: <<>> defp do_from_string(<>) do byte = hex_to_byte(<>) <> end defp do_from_string(<<_>>), do: raise(ArgumentError, "Invalid hex string length") @doc """ Converts a byte to its 2-character hex representation. """ @spec byte_to_hex(byte()) :: String.t() def byte_to_hex(byte) when is_integer(byte) and byte >= 0 and byte <= 255 do byte |> Integer.to_string(16) |> String.pad_leading(2, "0") |> String.upcase() end @doc """ Converts a 2-character hex string to a byte. """ @spec hex_to_byte(String.t()) :: byte() def hex_to_byte(<>) do parse_hex_digit(hex1) * 16 + parse_hex_digit(hex2) end defp parse_hex_digit(?0), do: 0 defp parse_hex_digit(?1), do: 1 defp parse_hex_digit(?2), do: 2 defp parse_hex_digit(?3), do: 3 defp parse_hex_digit(?4), do: 4 defp parse_hex_digit(?5), do: 5 defp parse_hex_digit(?6), do: 6 defp parse_hex_digit(?7), do: 7 defp parse_hex_digit(?8), do: 8 defp parse_hex_digit(?9), do: 9 defp parse_hex_digit(?a), do: 10 defp parse_hex_digit(?b), do: 11 defp parse_hex_digit(?c), do: 12 defp parse_hex_digit(?d), do: 13 defp parse_hex_digit(?e), do: 14 defp parse_hex_digit(?f), do: 15 defp parse_hex_digit(_), do: raise(ArgumentError, "Invalid hex digit") @doc """ Converts a binary to its ASCII representation (non-printable chars as dots). """ @spec to_ascii(binary()) :: String.t() def to_ascii(<<>>), do: "" def to_ascii(<>) do char = if byte >= 32 and byte <= 126, do: <>, else: <<".">> char <> to_ascii(rest) end @doc """ Converts a short (2 bytes) to a hex string. """ @spec short_to_hex(integer()) :: String.t() def short_to_hex(value) when is_integer(value) do <> |> encode() end @doc """ Converts an int (4 bytes) to a hex string. """ @spec int_to_hex(integer()) :: String.t() def int_to_hex(value) when is_integer(value) do <> |> encode() end @doc """ Formats a binary as a hex dump with offsets. """ @spec hex_dump(binary(), non_neg_integer()) :: String.t() def hex_dump(binary, offset \\ 0) do binary |> :binary.bin_to_list() |> Enum.chunk_every(16) |> Enum.with_index() |> Enum.map(fn {chunk, idx} -> offset_str = Integer.to_string(offset + idx * 16, 16) |> String.pad_leading(8, "0") hex_str = format_chunk(chunk) ascii_str = to_ascii(:binary.list_to_bin(chunk)) "#{offset_str} #{hex_str} |#{ascii_str}|" end) |> Enum.join("\n") end defp format_chunk(chunk) do chunk |> Enum.map(&byte_to_hex/1) |> Enum.map(&String.pad_trailing(&1, 2)) |> Enum.chunk_every(8) |> Enum.map(&Enum.join(&1, " ")) |> Enum.join(" ") |> String.pad_trailing(48) end end