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()