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(<>) 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(<>) do {:ok, str, rest} end defp decode_string_payload(_), do: {:error, :invalid_string} # --- Payload decoders by tag type --- defp decode_payload(@tag_byte, <>), do: {:ok, val, rest} defp decode_payload(@tag_short, <>), do: {:ok, val, rest} defp decode_payload(@tag_int, <>), do: {:ok, val, rest} defp decode_payload(@tag_long, <>), do: {:ok, val, rest} defp decode_payload(@tag_float, <>), do: {:ok, val, rest} defp decode_payload(@tag_double, <>), do: {:ok, val, rest} defp decode_payload( @tag_byte_array, <> ) 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, <>) 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, <>) do decode_int_array(len, rest, []) end defp decode_payload(@tag_long_array, <>) 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(<>, 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, <>, 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, <>, 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