feat(command): add bitmap command handlers

This commit is contained in:
dualfroz committed 2026-08-26 09:00:00 +02:00
1 parent 4438b77b2f
commit 0507577a0d
1 file changed
+245
+245
View File
@@ -0,0 +1,245 @@
defmodule Excache.Command.Bitmaps do
@moduledoc false
alias Excache.Store
alias Excache.Command.Reply
def dispatch("SETBIT", [key, offsetRaw, valueRaw], state) do
with {offset, ""} when offset >= 0 <- Integer.parse(offsetRaw),
value when value in [0, 1] <- parseBitValue(valueRaw) do
resultReply(Store.setbit(key, offset, value), state)
else
_ -> reply({:error, "ERR bit offset is not an integer or out of range"}, state)
end
end
def dispatch("GETBIT", [key, offsetRaw], state) do
case Integer.parse(offsetRaw) do
{offset, ""} when offset >= 0 -> resultReply(Store.getbit(key, offset), state)
_ -> reply({:error, "ERR bit offset is not an integer or out of range"}, state)
end
end
def dispatch("BITCOUNT", [key], state), do: resultReply(Store.bitcount(key, :whole), state)
def dispatch("BITCOUNT", [key, startRaw, endRaw | rest], state) do
with {:ok, unit} <- bitcountUnit(rest),
{start, ""} <- Integer.parse(startRaw),
{stop, ""} <- Integer.parse(endRaw) do
resultReply(Store.bitcount(key, {unit, start, stop}), state)
else
_ -> reply(Reply.syntax(), state)
end
end
def dispatch("BITPOS", [key, bitRaw], state) do
case parseBitValue(bitRaw) do
bit when bit in [0, 1] -> resultReply(Store.bitpos(key, bit, :whole, false), state)
_ -> reply({:error, "ERR The bit argument must be 1 or 0."}, state)
end
end
def dispatch("BITPOS", [key, bitRaw, startRaw | rest], state) do
with bit when bit in [0, 1] <- parseBitValue(bitRaw),
{start, ""} <- Integer.parse(startRaw),
{:ok, range, endGiven} <- bitposRange(start, rest) do
resultReply(Store.bitpos(key, bit, range, endGiven), state)
else
_ -> reply(Reply.syntax(), state)
end
end
def dispatch("BITOP", [opRaw, dest | [_ | _] = sources], state) do
case parseBitop(opRaw) do
{:ok, :not} when length(sources) != 1 ->
reply({:error, "ERR BITOP NOT must be called with a single source key."}, state)
{:ok, operation} ->
resultReply(Store.bitop(operation, dest, sources), state)
:error ->
reply(Reply.syntax(), state)
end
end
def dispatch("BITFIELD", [key | operations], state) do
case parseBitfield(operations) do
{:ok, parsed, writes?} ->
case Store.bitfield(key, parsed, writes?) do
{:ok, results} ->
reply({:array, Enum.map(results, &bitfieldResult/1)}, state)
{:error, reason} ->
reply(bitError(reason), state)
end
:error ->
reply(Reply.syntax(), state)
end
end
def dispatch(_name, _args, _state), do: :unhandled
defp parseBitValue("0"), do: 0
defp parseBitValue("1"), do: 1
defp parseBitValue(_), do: :error
defp bitcountUnit([]), do: {:ok, :byte}
defp bitcountUnit([unit]) when unit in ["BYTE", "byte"], do: {:ok, :byte}
defp bitcountUnit([unit]) when unit in ["BIT", "bit"], do: {:ok, :bit}
defp bitcountUnit(_), do: :error
defp bitposRange(start, []), do: {:ok, {:byte, start, -1}, false}
defp bitposRange(start, [endRaw]) do
case Integer.parse(endRaw) do
{stop, ""} -> {:ok, {:byte, start, stop}, true}
_ -> :error
end
end
defp bitposRange(start, [endRaw, unit]) do
with {stop, ""} <- Integer.parse(endRaw),
{:ok, kind} <- posUnit(unit) do
{:ok, {kind, start, stop}, true}
else
_ -> :error
end
end
defp bitposRange(_start, _rest), do: :error
defp posUnit(unit) when unit in ["BYTE", "byte"], do: {:ok, :byte}
defp posUnit(unit) when unit in ["BIT", "bit"], do: {:ok, :bit}
defp posUnit(_), do: :error
defp parseBitop(op) do
case String.upcase(op) do
"AND" -> {:ok, :and}
"OR" -> {:ok, :or}
"XOR" -> {:ok, :xor}
"NOT" -> {:ok, :not}
_ -> :error
end
end
defp parseBitfield(tokens), do: parseBitfield(tokens, :wrap, [], false)
defp parseBitfield([], _overflow, acc, writes?), do: {:ok, Enum.reverse(acc), writes?}
defp parseBitfield([token | rest], overflow, acc, writes?) do
case String.upcase(token) do
"OVERFLOW" ->
case rest do
[modeRaw | remaining] ->
case overflowMode(modeRaw) do
{:ok, mode} -> parseBitfield(remaining, mode, acc, writes?)
:error -> :error
end
[] ->
:error
end
"GET" ->
case rest do
[typeRaw, offsetRaw | remaining] ->
with {:ok, signed, bits} <- parseType(typeRaw),
{:ok, offset} <- parseOffset(offsetRaw, bits) do
parseBitfield(remaining, overflow, [{:get, signed, bits, offset} | acc], writes?)
else
_ -> :error
end
_ ->
:error
end
"SET" ->
case rest do
[typeRaw, offsetRaw, valueRaw | remaining] ->
with {:ok, signed, bits} <- parseType(typeRaw),
{:ok, offset} <- parseOffset(offsetRaw, bits),
{value, ""} <- Integer.parse(valueRaw) do
parseBitfield(
remaining,
overflow,
[{:set, signed, bits, offset, value, overflow} | acc],
true
)
else
_ -> :error
end
_ ->
:error
end
"INCRBY" ->
case rest do
[typeRaw, offsetRaw, valueRaw | remaining] ->
with {:ok, signed, bits} <- parseType(typeRaw),
{:ok, offset} <- parseOffset(offsetRaw, bits),
{delta, ""} <- Integer.parse(valueRaw) do
parseBitfield(
remaining,
overflow,
[{:incrby, signed, bits, offset, delta, overflow} | acc],
true
)
else
_ -> :error
end
_ ->
:error
end
_ ->
:error
end
end
defp overflowMode(mode) do
case String.upcase(mode) do
"WRAP" -> {:ok, :wrap}
"SAT" -> {:ok, :sat}
"FAIL" -> {:ok, :fail}
_ -> :error
end
end
defp parseType(<<prefix, digits::binary>>) when prefix in [?i, ?I, ?u, ?U] do
case Integer.parse(digits) do
{bits, ""} when bits >= 1 and bits <= 64 -> {:ok, prefix in [?i, ?I], bits}
_ -> :error
end
end
defp parseType(_), do: :error
defp parseOffset(<<"#", digits::binary>>, bits) do
case Integer.parse(digits) do
{multiple, ""} when multiple >= 0 -> {:ok, multiple * bits}
_ -> :error
end
end
defp parseOffset(raw, _bits) do
case Integer.parse(raw) do
{offset, ""} when offset >= 0 -> {:ok, offset}
_ -> :error
end
end
defp bitfieldResult(nil), do: {:bulk, nil}
defp bitfieldResult(value), do: {:integer, value}
defp resultReply({:ok, value}, state), do: reply({:integer, value}, state)
defp resultReply({:error, reason}, state), do: reply(bitError(reason), state)
defp bitError(:wrongtype), do: Reply.wrongtype()
defp bitError(other), do: {:error, "ERR #{other}"}
defp reply(term, state), do: {[term], state}
end