Skip to content

Instantly share code, notes, and snippets.

@dkuku
Created June 25, 2024 16:35
Show Gist options
  • Save dkuku/1519eb714b93af201f159a97651b67b8 to your computer and use it in GitHub Desktop.
Save dkuku/1519eb714b93af201f159a97651b67b8 to your computer and use it in GitHub Desktop.
protobuf decoder

Untitled notebook

Section

defmodule ProtobufParser do
  import Bitwise
  require Logger

  def parse(binary) do
    {result, _rest} = parse_message(binary)
    result
  end

  defp parse_message(binary, acc \\ %{}) do
    case parse_field(binary) do
      {:error, rest} ->
        {acc, rest}

      {field_number, value, rest} ->
        new_acc =
          Map.update(acc, field_number, value, fn existing ->
            case {existing, value} do
              {%{} = existing_map, %{} = new_map} -> Map.merge(existing_map, new_map)
              {_existing, new_value} -> new_value
            end
          end)

        parse_message(rest, new_acc)
    end
  end

  defp parse_field(<<>>) do
    {:error, <<>>}
  end

  defp parse_field(binary) do
    case parse_varint(binary) do
      {:error, rest} ->
        Logger.warning("Failed to parse varint, treating rest as binary: #{inspect(rest)}")
        {:error, rest}

      {key, rest} ->
        wire_type = key &&& 7
        field_number = key >>> 3

        case parse_value(wire_type, rest) do
          {:error, msg} ->
            Logger.warning("Failed to parse value: #{msg}")
            {:error, rest}

          {value, new_rest} ->
            {field_number, value, new_rest}
        end
    end
  end

  defp parse_varint(<<1::1, bits::7, rest::binary>>) do
    {value, rest} = do_parse_varint(rest, bits, 7)
    {value, rest}
  end

  defp parse_varint(<<0::1, value::7, rest::binary>>) do
    {value, rest}
  end

  defp do_parse_varint(<<1::1, bits::7, rest::binary>>, acc, shift) do
    do_parse_varint(rest, acc ||| bits <<< shift, shift + 7)
  end

  defp do_parse_varint(<<0::1, bits::7, rest::binary>>, acc, shift) do
    {acc ||| bits <<< shift, rest}
  end

  # Wire type 0: Varint
  defp parse_value(0, binary) do
    parse_varint(binary)
  end

  # Wire type 1: 64-bit
  defp parse_value(1, <<value::float-little-64, rest::binary>>) do
    {value, rest}
  end

  # Wire type 2: Length-delimited
  defp parse_value(2, binary) do
    case parse_varint(binary) do
      {:error, _} ->
        {:error, "Failed to parse length for delimited field"}

      {length, rest} ->
        case rest do
          <<value::binary-size(length), new_rest::binary>> ->
            if printable_string?(value) do
              {to_string(value), new_rest}
            else
              {parse(value), new_rest}
            end

          _ ->
            {:error, "Insufficient data for delimited field"}
        end
    end
  end

  # Wire type 5: 32-bit
  defp parse_value(5, <<value::float-little-32, rest::binary>>) do
    {value, rest}
  end

  defp parse_value(wire_type, _) do
    {:error, "Unsupported wire type: #{wire_type}"}
  end

  defp printable_string?(binary) do
    binary
    |> :binary.bin_to_list()
    |> Enum.all?(fn byte -> byte >= 32 and byte <= 126 end)
  end
end
proto =
  "0A 05 41 6C 69 63 65 10 7B 1A 11 61 6C 69 63 65 40 65 78 61 6D 70 6C 65 2E 63 6F 6D 22 0A 0A 08 35 35 35 2D 31 32 33 34 10 00"
  |> String.replace(" ", "")
  |> Base.decode16!()
  |> ProtobufParser.parse()
<<8, 10, 16, 44, 24, 20, 32, 15, 40, 34, 48, 5, 58, 23, 97, 112, 112, 111, 105, 110, 116, 109,
  101, 110, 116, 32, 114, 101, 118, 105, 101, 119, 32, 116, 101, 120, 116, 64, 1, 122, 27, 50, 48,
  50, 52, 45, 48, 54, 45, 50, 53, 84, 49, 54, 58, 48, 48, 58, 48, 55, 46, 48, 55, 50, 56, 57, 50,
  90, 194, 62, 11, 8, 30, 18, 7, 67, 121, 112, 114, 101, 115, 115, 202, 62, 14, 8, 23, 16, 27, 26,
  8, 48, 56, 49, 99, 101, 50, 99, 57>>
|> ProtobufParser.parse()
<<42, 50, 10, 48, 16, 191, 22, 24, 153, 11, 32, 148, 2, 50, 15, 8, 149, 2, 16, 225, 16, 26, 7, 10,
  5, 50, 48, 46, 50, 51, 66, 20, 50, 48, 50, 52, 45, 48, 53, 45, 49, 50, 84, 49, 48, 58, 49, 48,
  58, 49, 48, 90>>
|> ProtobufParser.parse()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment