From 243c5b1b6c4c9354cf3603fe905da69629068a97 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 11:17:00 +0200 Subject: [PATCH] feat(resp): add resp2 protocol parser --- lib/excache/resp/parser.ex | 92 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 lib/excache/resp/parser.ex diff --git a/lib/excache/resp/parser.ex b/lib/excache/resp/parser.ex new file mode 100644 index 0000000..f4b4f7b --- /dev/null +++ b/lib/excache/resp/parser.ex @@ -0,0 +1,92 @@ +defmodule Excache.RESP.Parser do + @moduledoc false + + @type result :: {:ok, [binary], binary} | :incomplete | {:error, String.t()} + + @spec parse_command(binary) :: result + def parse_command(<<>>), do: :incomplete + def parse_command(<<"*", _::binary>> = buffer), do: parse_multibulk(buffer) + def parse_command(buffer), do: parse_inline(buffer) + + defp parse_inline(buffer) do + case read_line(buffer) do + :incomplete -> :incomplete + {:ok, line, rest} -> {:ok, tokenize(line), rest} + end + end + + defp tokenize(line) do + line + |> String.split([" ", "\t"], trim: true) + end + + defp parse_multibulk(buffer) do + case read_line(buffer) do + :incomplete -> + :incomplete + + {:ok, <<"*", digits::binary>>, rest} -> + case Integer.parse(digits) do + {count, ""} when count <= 0 -> {:ok, [], rest} + {count, ""} -> parse_bulks(count, rest, []) + _ -> {:error, "invalid multibulk length"} + end + + {:ok, _other, _rest} -> + {:error, "expected multibulk header"} + end + end + + defp parse_bulks(0, rest, acc), do: {:ok, Enum.reverse(acc), rest} + + defp parse_bulks(remaining, buffer, acc) do + case read_line(buffer) do + :incomplete -> + :incomplete + + {:ok, <<"$", digits::binary>>, rest} -> + case Integer.parse(digits) do + {len, ""} when len >= 0 -> take_bulk(len, rest, remaining, acc) + {-1, ""} -> parse_bulks(remaining - 1, rest, [nil | acc]) + _ -> {:error, "invalid bulk length"} + end + + {:ok, _other, _rest} -> + {:error, "expected bulk header"} + end + end + + defp take_bulk(len, rest, remaining, acc) do + needed = len + 2 + + cond do + byte_size(rest) < needed -> + :incomplete + + binary_part(rest, len, 2) == "\r\n" -> + data = binary_part(rest, 0, len) + tail = binary_part(rest, needed, byte_size(rest) - needed) + parse_bulks(remaining - 1, tail, [data | acc]) + + true -> + {:error, "bulk terminator missing"} + end + end + + defp read_line(buffer) do + case :binary.split(buffer, "\n") do + [line, rest] -> {:ok, strip_cr(line), rest} + [_only] -> :incomplete + end + end + + defp strip_cr(line) do + size = byte_size(line) + + if size > 0 and binary_part(line, size - 1, 1) == "\r" do + binary_part(line, 0, size - 1) + else + line + end + end +end