Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
583 changes: 143 additions & 440 deletions lib/protobuf/decoder.ex

Large diffs are not rendered by default.

83 changes: 8 additions & 75 deletions lib/protobuf/encoder.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule Protobuf.Encoder do
@moduledoc false
import Protobuf.WireTypes
import Bitwise, only: [bsr: 2, band: 2, bsl: 2, bor: 2]
import Protobuf.Wire.Types
import Bitwise, only: [bsl: 2, bor: 2]

alias Protobuf.{MessageProps, FieldProps}
alias Protobuf.{FieldProps, MessageProps, Wire, Wire.Varint}

@spec encode(atom, map | struct, keyword) :: iodata
def encode(mod, msg, opts) do
Expand Down Expand Up @@ -98,7 +98,7 @@ defmodule Protobuf.Encoder do
@spec encode_field(atom, any, FieldProps.t()) :: iodata
defp encode_field(:normal, val, %{encoded_fnum: fnum, type: type, repeated?: is_repeated}) do
repeated_or_not(val, is_repeated, fn v ->
[fnum | encode_type(type, v)]
[fnum | Wire.from_proto(type, v)]
end)
end

Expand All @@ -114,14 +114,14 @@ defmodule Protobuf.Encoder do
# so that oneof {:atom, v} can be encoded
encoded = encode(type, v, iolist: true)
byte_size = IO.iodata_length(encoded)
[fnum | encode_varint(byte_size)] ++ encoded
[fnum | Varint.encode(byte_size)] ++ encoded
end)
end

defp encode_field(:packed, val, %{type: type, encoded_fnum: fnum}) do
encoded = Enum.map(val, fn v -> encode_type(type, v) end)
encoded = Enum.map(val, fn v -> Wire.from_proto(type, v) end)
byte_size = IO.iodata_length(encoded)
[fnum | encode_varint(byte_size)] ++ encoded
[fnum | Varint.encode(byte_size)] ++ encoded
end

@spec class_field(map) :: atom
Expand All @@ -143,77 +143,10 @@ defmodule Protobuf.Encoder do
fnum
|> bsl(3)
|> bor(wire_type)
|> encode_varint()
|> Varint.encode()
|> IO.iodata_to_binary()
end

@doc false
@spec encode_type(atom, any) :: iodata
def encode_type(:int32, n) when n >= -0x80000000 and n <= 0x7FFFFFFF, do: encode_varint(n)

def encode_type(:int64, n) when n >= -0x8000000000000000 and n <= 0x7FFFFFFFFFFFFFFF,
do: encode_varint(n)

def encode_type(:string, n), do: encode_type(:bytes, n)
def encode_type(:uint32, n) when n >= 0 and n <= 0xFFFFFFFF, do: encode_varint(n)
def encode_type(:uint64, n) when n >= 0 and n <= 0xFFFFFFFFFFFFFFFF, do: encode_varint(n)
def encode_type(:bool, true), do: encode_varint(1)
def encode_type(:bool, false), do: encode_varint(0)
def encode_type({:enum, type}, n) when is_atom(n), do: n |> type.value() |> encode_varint()
def encode_type({:enum, _}, n), do: encode_varint(n)
def encode_type(:float, :infinity), do: [0, 0, 128, 127]
def encode_type(:float, :negative_infinity), do: [0, 0, 128, 255]
def encode_type(:float, :nan), do: [0, 0, 192, 127]
def encode_type(:float, n), do: <<n::32-float-little>>
def encode_type(:double, :infinity), do: [0, 0, 0, 0, 0, 0, 240, 127]
def encode_type(:double, :negative_infinity), do: [0, 0, 0, 0, 0, 0, 240, 255]
def encode_type(:double, :nan), do: [1, 0, 0, 0, 0, 0, 248, 127]
def encode_type(:double, n), do: <<n::64-float-little>>

def encode_type(:bytes, n) do
len = n |> IO.iodata_length() |> encode_varint()
len ++ n
end

def encode_type(:sint32, n) when n >= -0x80000000 and n <= 0x7FFFFFFF,
do: n |> encode_zigzag |> encode_varint

def encode_type(:sint64, n) when n >= -0x8000000000000000 and n <= 0x7FFFFFFFFFFFFFFF,
do: n |> encode_zigzag |> encode_varint

def encode_type(:fixed64, n) when n >= 0 and n <= 0xFFFFFFFFFFFFFFFF, do: <<n::64-little>>

def encode_type(:sfixed64, n) when n >= -0x8000000000000000 and n <= 0x7FFFFFFFFFFFFFFF,
do: <<n::64-signed-little>>

def encode_type(:fixed32, n) when n >= 0 and n <= 0xFFFFFFFF, do: <<n::32-little>>

def encode_type(:sfixed32, n) when n >= -0x80000000 and n <= 0x7FFFFFFF,
do: <<n::32-signed-little>>

def encode_type(type, n) do
raise Protobuf.TypeEncodeError, message: "#{inspect(n)} is invalid for type #{type}"
end

@spec encode_zigzag(integer) :: integer
defp encode_zigzag(val) when val >= 0, do: val * 2
defp encode_zigzag(val) when val < 0, do: val * -2 - 1

@doc false
@spec encode_varint(integer) :: iolist
def encode_varint(n) when n < 0 do
<<n::64-unsigned-native>> = <<n::64-signed-native>>
encode_varint(n)
end

def encode_varint(n) when n <= 127 do
[n]
end

def encode_varint(n) do
[<<1::1, band(n, 127)::7>> | encode_varint(bsr(n, 7))]
end

@doc false
@spec wire_type(atom) :: integer
def wire_type(:int32), do: wire_varint()
Expand Down
136 changes: 136 additions & 0 deletions lib/protobuf/wire.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
defmodule Protobuf.Wire do
@moduledoc """
Utilities to convert data from wire format to protobuf and back.
"""

alias Protobuf.Wire.{Varint, Zigzag}

require Logger

@type proto_type ::
:int32
| :int64
| :fixed32
| :fixed64
| :uint32
| :uint64
| :sfixed32
| :sfixed64
| :sint32
| :sint64
| :float
| :double
| :bool
| :string
| :bytes
| {:enum, any}

@type proto_float :: :infinity | :negative_infinity | :nan | float

@type proto_value :: binary | integer | bool | proto_float | atom

@sint32_range -0x80000000..0x7FFFFFFF
@sint64_range -0x8000000000000000..0x7FFFFFFFFFFFFFFF
@uint32_range 0..0xFFFFFFFF
@uint64_range 0..0xFFFFFFFFFFFFFFFF

@spec from_proto(proto_type, proto_value) :: iodata
# Returns improper list, but still valid iodata.
def from_proto(type, binary) when type in [:string, :bytes] do
len = binary |> IO.iodata_length() |> Varint.encode()
len ++ binary
end

def from_proto(:int32, n) when n in @sint32_range, do: Varint.encode(n)
def from_proto(:int64, n) when n in @sint64_range, do: Varint.encode(n)
def from_proto(:uint32, n) when n in @uint32_range, do: Varint.encode(n)
def from_proto(:uint64, n) when n in @uint64_range, do: Varint.encode(n)

def from_proto(:bool, true), do: Varint.encode(1)
def from_proto(:bool, false), do: Varint.encode(0)

def from_proto({:enum, enum}, key) when is_atom(key), do: Varint.encode(enum.value(key))
def from_proto({:enum, _}, n) when is_integer(n), do: Varint.encode(n)

def from_proto(:float, :infinity), do: [0, 0, 128, 127]
def from_proto(:float, :negative_infinity), do: [0, 0, 128, 255]
def from_proto(:float, :nan), do: [0, 0, 192, 127]
def from_proto(:float, n), do: <<n::32-float-little>>

def from_proto(:double, :infinity), do: [0, 0, 0, 0, 0, 0, 240, 127]
def from_proto(:double, :negative_infinity), do: [0, 0, 0, 0, 0, 0, 240, 255]
def from_proto(:double, :nan), do: [1, 0, 0, 0, 0, 0, 248, 127]
def from_proto(:double, n), do: <<n::64-float-little>>

def from_proto(:sint32, n) when n in @sint32_range, do: Varint.encode(Zigzag.encode(n))
def from_proto(:sint64, n) when n in @sint64_range, do: Varint.encode(Zigzag.encode(n))
def from_proto(:fixed32, n) when n in @uint32_range, do: <<n::32-little>>
def from_proto(:fixed64, n) when n in @uint64_range, do: <<n::64-little>>
def from_proto(:sfixed32, n) when n in @sint32_range, do: <<n::32-signed-little>>
def from_proto(:sfixed64, n) when n in @sint64_range, do: <<n::64-signed-little>>

def from_proto(type, n) do
raise Protobuf.TypeEncodeError, message: "#{inspect(n)} is invalid for type #{type}"
end

@spec to_proto(proto_type, binary | integer) :: proto_value
def to_proto(type, val) when type in [:string, :bytes], do: val

def to_proto(:int32, val) do
<<n::signed-integer-32>> = <<val::32>>
n
end

def to_proto(:int64, val) do
<<n::signed-integer-64>> = <<val::64>>
n
end

def to_proto(:uint32, val) do
<<n::unsigned-integer-32>> = <<val::32>>
n
end

def to_proto(:uint64, val) do
<<n::unsigned-integer-64>> = <<val::64>>
n
end

def to_proto(:bool, val), do: val != 0

def to_proto({:enum, enum}, val) do
enum.key(val)
rescue
FunctionClauseError ->
Logger.warn("unknown enum value #{val} when decoding for #{inspect(enum)}")
val
end

def to_proto(:float, <<n::little-float-32>>), do: n
# little endianness, should be 0b0_11111111_000000000...
def to_proto(:float, <<0, 0, 0b1000_0000::8, 0b01111111::8>>), do: :infinity
# little endianness, should be 0b1_11111111_000000000...
def to_proto(:float, <<0, 0, 0b1000_0000::8, 0b11111111::8>>), do: :negative_infinity
# should be 0b*_11111111_not_zero...
def to_proto(:float, <<a::16, 1::1, b::7, _::1, 0b1111111::7>>) when a != 0 or b != 0,
do: :nan

def to_proto(:double, <<n::little-float-64>>), do: n
# little endianness, should be 0b0_11111111111_000000000...
def to_proto(:double, <<0::48, 0b1111::4, 0::4, 0b01111111::8>>), do: :infinity
# little endianness, should be 0b1_11111111111_000000000...
def to_proto(:double, <<0::48, 0b1111::4, 0::4, 0b11111111::8>>), do: :negative_infinity

def to_proto(:double, <<a::48, 0b1111::4, b::4, _::1, 0b1111111::7>>) when a != 0 or b != 0,
do: :nan

def to_proto(type, val) when type in [:sint32, :sint64], do: Zigzag.decode(val)
def to_proto(:fixed32, <<n::little-32>>), do: n
def to_proto(:fixed64, <<n::little-64>>), do: n
def to_proto(:sfixed32, <<n::little-signed-32>>), do: n
def to_proto(:sfixed64, <<n::little-signed-64>>), do: n

def to_proto(type, val) do
raise Protobuf.DecodeError, message: "can't decode #{inspect(val)} into type #{type}"
end
end
2 changes: 1 addition & 1 deletion lib/protobuf/wire_types.ex → lib/protobuf/wire/types.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Protobuf.WireTypes do
defmodule Protobuf.Wire.Types do
@moduledoc false

defmacro wire_varint, do: 0
Expand Down
Loading