From 77f06521f497b70c6bcd0d26618f65375e012aaf Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 09:13:38 +0200 Subject: [PATCH] fix(zarith): raise ArgumentError on malformed input Now consume/1 validates upfront (non-empty, even-length hex) and find_int + read_next raise on truncation (continuation bit set with no following byte). read/1 now branches on the halt bit explicitly so single-byte zarith inputs still terminate cleanly. --- lib/zarith.ex | 24 +++++++++++++++--------- test/zarith_test.exs | 24 +++++++++++++++++++++++- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/lib/zarith.ex b/lib/zarith.ex index 38882ed..4a80a67 100644 --- a/lib/zarith.ex +++ b/lib/zarith.ex @@ -164,9 +164,13 @@ defmodule Tezex.Zarith do """ @spec consume(zarith_hex()) :: {%{int: String.t()}, pos_integer()} def consume(binary_input) when is_binary(binary_input) do + if binary_input == "" or rem(byte_size(binary_input), 2) != 0 do + raise ArgumentError, "Zarith input must be a non-empty even-length hex string" + end + {carved_int, rest} = find_int(binary_input) - consumed = String.length(binary_input) - String.length(rest) + consumed = byte_size(binary_input) - byte_size(rest) binary = carved_int @@ -194,16 +198,18 @@ defmodule Tezex.Zarith do end end - defp find_int("", acc) do - {Enum.reverse(acc) |> Enum.join(""), ""} + defp find_int("", _acc) do + raise ArgumentError, "Zarith input truncated: continuation bit set with no following byte" end - defp read([<<_halt::1, sign::1, tail::integer-size(6)>> | rest]) do - bits = - for bits_part <- read_next(rest, [<>]), into: <<>> do - bits_part + defp read([<> | rest]) do + parts = + case halt do + 0 -> [<>] + 1 -> read_next(rest, [<>]) end + bits = for p <- parts, into: <<>>, do: p bits_count = bit_size(bits) <> = bits @@ -221,8 +227,8 @@ defmodule Tezex.Zarith do [<> | acc] end - defp read_next(_, acc) do - acc + defp read_next([], _acc) do + raise ArgumentError, "Zarith input truncated: continuation bit set with no following byte" end defp hex_to_dec(hex) do diff --git a/test/zarith_test.exs b/test/zarith_test.exs index eec5b1e..9c0f852 100644 --- a/test/zarith_test.exs +++ b/test/zarith_test.exs @@ -6,7 +6,8 @@ defmodule Tezex.ZarithTest do test "encode/decode" do n = 1_000_000_000_000 - random = fn -> trunc(:rand.uniform(n) - n / 2) end + h = n / 2 + random = fn -> trunc(:rand.uniform(n) - h) end Enum.each(1..5000, fn _ -> number = random.() @@ -72,4 +73,25 @@ defmodule Tezex.ZarithTest do assert {%{int: "-610913435200"}, byte_size(packed)} == Zarith.consume(packed) end end + + describe "malformed input" do + test "raises on truncated input with continuation bit set" do + # 0x80: continuation=1, sign=0, tail=0, claims more bytes follow but none do + assert_raise ArgumentError, fn -> Zarith.decode("80") end + # 0xff: continuation=1, sign=1, tail=63, same truncation + assert_raise ArgumentError, fn -> Zarith.decode("ff") end + # Multi-byte truncation: first byte continues, second byte continues, then ends + assert_raise ArgumentError, fn -> Zarith.decode("8080") end + end + + test "raises on empty input" do + assert_raise ArgumentError, fn -> Zarith.consume("") end + assert_raise ArgumentError, fn -> Zarith.decode("") end + end + + test "raises on odd-length hex input" do + assert_raise ArgumentError, fn -> Zarith.consume("a1d22") end + assert_raise ArgumentError, fn -> Zarith.decode("a1d22") end + end + end end