lib/bandit/http2/frame.ex

defmodule Bandit.HTTP2.Frame do
  @moduledoc false

  alias Bandit.HTTP2.{Constants, Frame, Serializable}

  # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
  def deserialize(
        <<length::24, type::8, flags::8, _reserved::1, stream_id::31,
          payload::binary-size(length), rest::binary>>,
        max_frame_size
      )
      when length <= max_frame_size do
    type
    |> case do
      0x0 -> Frame.Data.deserialize(flags, stream_id, payload)
      0x1 -> Frame.Headers.deserialize(flags, stream_id, payload)
      0x2 -> Frame.Priority.deserialize(flags, stream_id, payload)
      0x3 -> Frame.RstStream.deserialize(flags, stream_id, payload)
      0x4 -> Frame.Settings.deserialize(flags, stream_id, payload)
      0x5 -> Frame.PushPromise.deserialize(flags, stream_id, payload)
      0x6 -> Frame.Ping.deserialize(flags, stream_id, payload)
      0x7 -> Frame.Goaway.deserialize(flags, stream_id, payload)
      0x8 -> Frame.WindowUpdate.deserialize(flags, stream_id, payload)
      0x9 -> Frame.Continuation.deserialize(flags, stream_id, payload)
      unknown -> Frame.Unknown.deserialize(unknown, flags, stream_id, payload)
    end
    |> case do
      {:ok, frame} -> {{:ok, frame}, rest}
      {:error, reason} -> {{:error, reason}, rest}
    end
  end

  # This is a little more aggressive than necessary. RFC7540§4.2 says we only need
  # to treat frame size violations as connection level errors if the frame in
  # question would affect the connection as a whole, so we could be more surgical
  # here and send stream level errors in some cases. However, we are well within
  # our rights to consider such errors as connection errors
  def deserialize(
        <<length::24, _type::8, _flags::8, _reserved::1, _stream_id::31,
          _payload::binary-size(length), rest::binary>>,
        max_frame_size
      )
      when length > max_frame_size do
    {{:error,
      {:connection, Constants.frame_size_error(), "Payload size too large (RFC7540§4.2)"}}, rest}
  end

  def deserialize(<<>>, _max_frame_size) do
    nil
  end

  def deserialize(msg, _max_frame_size) do
    {{:more, msg}, <<>>}
  end

  def serialize(frame, max_frame_size) do
    frame
    |> Serializable.serialize(max_frame_size)
    |> Enum.map(fn {type, flags, stream_id, payload} ->
      [<<IO.iodata_length(payload)::24, type::8, flags::8, 0::1, stream_id::31>>, payload]
    end)
  end
end