136 lines
4.4 KiB
Elixir
136 lines
4.4 KiB
Elixir
defmodule CobblemonUi.CobblemonFS.NBT do
|
|
@moduledoc """
|
|
Decoder for Minecraft's Named Binary Tag (NBT) format.
|
|
|
|
Supports both gzip-compressed and uncompressed NBT files.
|
|
All multi-byte integers are big-endian and signed unless noted.
|
|
"""
|
|
|
|
# NBT tag type constants
|
|
@tag_end 0
|
|
@tag_byte 1
|
|
@tag_short 2
|
|
@tag_int 3
|
|
@tag_long 4
|
|
@tag_float 5
|
|
@tag_double 6
|
|
@tag_byte_array 7
|
|
@tag_string 8
|
|
@tag_list 9
|
|
@tag_compound 10
|
|
@tag_int_array 11
|
|
@tag_long_array 12
|
|
|
|
@doc """
|
|
Decodes an NBT binary (possibly gzip-compressed) into an Elixir map.
|
|
|
|
Returns `{:ok, {name, value}}` or `{:error, reason}`.
|
|
"""
|
|
@spec decode(binary()) :: {:ok, {String.t(), map()}} | {:error, term()}
|
|
def decode(data) when is_binary(data) do
|
|
data = maybe_decompress(data)
|
|
|
|
case decode_named_tag(data) do
|
|
{:ok, {name, value}, _rest} -> {:ok, {name, value}}
|
|
{:error, _} = err -> err
|
|
end
|
|
rescue
|
|
e -> {:error, {:decode_failed, Exception.message(e)}}
|
|
end
|
|
|
|
defp maybe_decompress(<<0x1F, 0x8B, _rest::binary>> = data), do: :zlib.gunzip(data)
|
|
defp maybe_decompress(data), do: data
|
|
|
|
# Decodes a single named tag: type byte + name + payload
|
|
defp decode_named_tag(<<type, rest::binary>>) do
|
|
with {:ok, name, rest} <- decode_string_payload(rest),
|
|
{:ok, value, rest} <- decode_payload(type, rest) do
|
|
{:ok, {name, value}, rest}
|
|
end
|
|
end
|
|
|
|
defp decode_named_tag(_), do: {:error, :unexpected_eof}
|
|
|
|
# String payload: 2-byte unsigned length prefix + UTF-8 bytes
|
|
defp decode_string_payload(<<len::unsigned-big-16, str::binary-size(len), rest::binary>>) do
|
|
{:ok, str, rest}
|
|
end
|
|
|
|
defp decode_string_payload(_), do: {:error, :invalid_string}
|
|
|
|
# --- Payload decoders by tag type ---
|
|
|
|
defp decode_payload(@tag_byte, <<val::signed-8, rest::binary>>), do: {:ok, val, rest}
|
|
defp decode_payload(@tag_short, <<val::signed-big-16, rest::binary>>), do: {:ok, val, rest}
|
|
defp decode_payload(@tag_int, <<val::signed-big-32, rest::binary>>), do: {:ok, val, rest}
|
|
defp decode_payload(@tag_long, <<val::signed-big-64, rest::binary>>), do: {:ok, val, rest}
|
|
defp decode_payload(@tag_float, <<val::float-big-32, rest::binary>>), do: {:ok, val, rest}
|
|
defp decode_payload(@tag_double, <<val::float-big-64, rest::binary>>), do: {:ok, val, rest}
|
|
|
|
defp decode_payload(
|
|
@tag_byte_array,
|
|
<<len::signed-big-32, bytes::binary-size(len), rest::binary>>
|
|
) do
|
|
{:ok, :binary.bin_to_list(bytes), rest}
|
|
end
|
|
|
|
defp decode_payload(@tag_string, data), do: decode_string_payload(data) |> wrap_string_result()
|
|
|
|
defp decode_payload(@tag_list, <<elem_type::8, len::signed-big-32, rest::binary>>) do
|
|
decode_list_items(elem_type, len, rest, [])
|
|
end
|
|
|
|
defp decode_payload(@tag_compound, data), do: decode_compound(data, %{})
|
|
|
|
defp decode_payload(@tag_int_array, <<len::signed-big-32, rest::binary>>) do
|
|
decode_int_array(len, rest, [])
|
|
end
|
|
|
|
defp decode_payload(@tag_long_array, <<len::signed-big-32, rest::binary>>) do
|
|
decode_long_array(len, rest, [])
|
|
end
|
|
|
|
defp decode_payload(type, _), do: {:error, {:unsupported_tag_type, type}}
|
|
|
|
# --- Compound decoder ---
|
|
|
|
defp decode_compound(<<@tag_end, rest::binary>>, acc), do: {:ok, acc, rest}
|
|
|
|
defp decode_compound(<<type, rest::binary>>, acc) do
|
|
with {:ok, name, rest} <- decode_string_payload(rest),
|
|
{:ok, value, rest} <- decode_payload(type, rest) do
|
|
decode_compound(rest, Map.put(acc, name, value))
|
|
end
|
|
end
|
|
|
|
defp decode_compound(<<>>, _acc), do: {:error, :unexpected_eof_in_compound}
|
|
|
|
# --- List decoder ---
|
|
|
|
defp decode_list_items(_type, 0, rest, acc), do: {:ok, Enum.reverse(acc), rest}
|
|
|
|
defp decode_list_items(type, n, rest, acc) when n > 0 do
|
|
with {:ok, value, rest} <- decode_payload(type, rest) do
|
|
decode_list_items(type, n - 1, rest, [value | acc])
|
|
end
|
|
end
|
|
|
|
# --- Array decoders ---
|
|
|
|
defp decode_int_array(0, rest, acc), do: {:ok, Enum.reverse(acc), rest}
|
|
|
|
defp decode_int_array(n, <<val::signed-big-32, rest::binary>>, acc) do
|
|
decode_int_array(n - 1, rest, [val | acc])
|
|
end
|
|
|
|
defp decode_long_array(0, rest, acc), do: {:ok, Enum.reverse(acc), rest}
|
|
|
|
defp decode_long_array(n, <<val::signed-big-64, rest::binary>>, acc) do
|
|
decode_long_array(n - 1, rest, [val | acc])
|
|
end
|
|
|
|
# Helper to wrap string decode result to match expected return shape
|
|
defp wrap_string_result({:ok, str, rest}), do: {:ok, str, rest}
|
|
defp wrap_string_result(err), do: err
|
|
end
|