Files
2026-03-16 12:44:06 -06:00

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