defmodule Msgpack do @moduledoc """ An implementation of the MessagePack serialization format. This module provides the main API for encoding Elixir terms into MessagePack binaries and decoding MessagePack binaries back into Elixir terms. ## Usage The primary functions are `encode/2` and `decode/2` (and their exception-raising variants `encode!/2` and `decode!/2`). ### Example ```elixir iex> data = %{"id" => 1, "name" => "Elixir"} iex> {:ok, encoded} = Msgpack.encode(data) iex> Msgpack.decode(encoded) {:ok, %{"id" => 1, "name" => "Elixir"}} ``` """ alias Msgpack.Encoder alias Msgpack.Decoder alias Msgpack.EncodeError alias Msgpack.DecodeError @type error_reason :: # Encoding errors {:unsupported_type, term()} | {:unsupported_atom, atom()} # Decoding errors | :unexpected_eof | {:unknown_prefix, byte()} | {:trailing_bytes, binary()} | {:max_depth_reached, non_neg_integer()} | {:max_byte_size_exceeded, non_neg_integer()} | :invalid_timestamp @doc """ Encodes an Elixir term into a MessagePack binary. Returns `{:ok, binary}` on success, or `{:error, reason}` on failure. ## Options * `:atoms` - Controls how atoms are encoded. * `:string` (default) - Encodes atoms as MessagePack strings. * `:error` - Returns an `{:error, {:unsupported_atom, atom}}` tuple if an atom is encountered. * `:string_validation` - Controls whether to perform UTF-8 validation on binaries. * `true` (default) - Validates binaries and encodes them as the `str` type if they are valid UTF-8, otherwise encodes them as the `bin` type * `false` - Skips validation and encodes all binaries as the `str` type. This provides a significant performance increase but should only be used if you are certain that your data does not contain invalid UTF-8 strings. ## Examples iex> Msgpack.encode(%{hello: "world"}) {:ok, <<129, 165, 104, 101, 108, 108, 111, 165, 119, 111, 114, 108, 100>>} iex> Msgpack.encode(:my_atom, atoms: :error) {:error, {:unsupported_atom, :my_atom}} """ @spec encode(term(), keyword()) :: {:ok, binary()} | {:error, error_reason()} def encode(term, opts \\ []) do :telemetry.span( [:msgpack, :encode], %{opts: opts, term: term}, fn -> result = Encoder.encode(term, opts) case result do {:ok, iodata} -> binary = IO.iodata_to_binary(iodata) {{:ok, binary}, %{outcome: :ok, byte_size: byte_size(binary)}} {:error, reason} -> {{:error, reason}, %{outcome: :error}} end end ) end @doc """ Encodes an Elixir term into a MessagePack binary, raising an error on failure. ## Options Accepts the same options as `encode/2`. ## Raises * `Msgpack.EncodeError` - if an unsupported Elixir term is encountered. * `Msgpack.UnsupportedAtomError` - if an atom is encountered and the `:atoms` option is set to `:error`. ## Examples iex> Msgpack.encode!(%{hello: "world"}) <<129, 165, 104, 101, 108, 108, 111, 165, 119, 111, 114, 108, 100>> """ @spec encode!(term(), keyword()) :: binary() def encode!(term, opts \\ []) do case encode(term, opts) do {:ok, binary} -> binary {:error, {:unsupported_type, type}} -> raise %EncodeError{message: "cannot encode unsupported type: #{inspect(type)}"} {:error, {:unsupported_atom, atom}} -> raise Msgpack.UnsupportedAtomError, atom: atom end end @doc """ Decodes a MessagePack binary into an Elixir term. Returns `{:ok, term}` on success, or `{:error, reason}` on failure. ## Options * `:max_depth` - Sets a limit on the nesting level of arrays and maps to prevent stack exhaustion from maliciously crafted inputs. Defaults to `100`. * `:max_byte_size` - Sets a limit on the declared byte size of any single string, binary, array, or map to prevent memory exhaustion attacks. Defaults to `10_000_000` (10MB). ## Examples iex> encoded = <<129, 165, 104, 101, 108, 108, 111, 165, 119, 111, 114, 108, 100>> iex> Msgpack.decode(encoded) {:ok, %{"hello" => "world"}} iex> Msgpack.decode(<<192, 42>>) {:error, {:trailing_bytes, <<42>>}} iex> Msgpack.decode(<<0x91, 0x91, 1>>, max_depth: 1) {:error, {:max_depth_reached, 1}} iex> Msgpack.decode(<<0xDB, 0xFFFFFFFF::32>>, max_byte_size: 1_000_000) {:error, {:max_byte_size_exceeded, 1_000_000}} """ @spec decode(binary(), keyword()) :: {:ok, term()} | {:error, error_reason()} def decode(binary, opts \\ []) do :telemetry.span( [:msgpack, :decode], %{opts: opts, byte_size: byte_size(binary)}, fn -> result = Decoder.decode(binary, opts) case result do {:ok, term} -> {{:ok, term}, %{outcome: :ok}} {:error, reason} -> {{:error, reason}, %{outcome: :error}} end end ) end @doc """ Decodes a MessagePack binary, raising a `Msgpack.DecodeError` on failure. ## Options Accepts the same options as `decode/2`. ## Raises * `Msgpack.DecodeError` - if the binary is malformed, contains an unknown prefix, or has trailing bytes. ## Examples iex> encoded = <<129, 165, 104, 101, 108, 108, 111, 165, 119, 111, 114, 108, 100>> iex> Msgpack.decode!(<<255>>) ** (Msgpack.DecodeError) unknown prefix: 255 """ @spec decode!(binary(), keyword()) :: term() def decode!(binary, opts \\ []) do case decode(binary, opts) do {:ok, term} -> term {:error, reason} -> raise DecodeError, reason: reason end end end