diff --git a/lib/nimble_parsec.ex b/lib/nimble_parsec.ex index 170b0fb..27e22d6 100644 --- a/lib/nimble_parsec.ex +++ b/lib/nimble_parsec.ex @@ -237,6 +237,7 @@ defmodule NimbleParsec do @typep bound_combinator :: {:bin_segment, [inclusive_range], [exclusive_range], bin_modifier} | {:string, binary} + | {:bytes, pos_integer} | :eos @typep maybe_bound_combinator :: @@ -380,6 +381,11 @@ defmodule NimbleParsec do generate(parsecs, mod, gen_times(t, Enum.random(0..max), mod, acc)) end + defp generate([{:bytes, count} | parsecs], mod, acc) do + bytes = bytes_random(count) + generate(parsecs, mod, [bytes | acc]) + end + defp generate([], _mod, acc), do: Enum.reverse(acc) defp gen_export(mod, fun) do @@ -437,6 +443,10 @@ defmodule NimbleParsec do defp weighted_random([_ | list], [weight | weights], chosen), do: weighted_random(list, weights, chosen - weight) + defp bytes_random(count) when is_integer(count) do + :crypto.strong_rand_bytes(count) + end + @doc ~S""" Returns an empty combinator. @@ -1774,6 +1784,30 @@ defmodule NimbleParsec do choice(combinator, [optional, empty()]) end + @doc """ + Defines a combinator to consume the next `n` bytes from the input. + + ## Examples + + defmodule MyParser do + import NimbleParsec + + defparsec :three_bytes, bytes(3) + end + + MyParser.three_bytes("abc") + #=> {:ok, ["abc"], "", %{}, {1, 0}, 3} + + MyParser.three_bytes("ab") + #=> {:error, "expected 3 bytes", "ab", %{}, {1, 0}, 0} + """ + @spec bytes(pos_integer) :: t + @spec bytes(t, pos_integer) :: t + def bytes(combinator \\ empty(), count) + when is_combinator(combinator) and is_integer(count) and count > 0 do + [{:bytes, count} | combinator] + end + ## Helpers defp validate_min_and_max!(count_or_opts, required_min \\ 0) diff --git a/lib/nimble_parsec/compiler.ex b/lib/nimble_parsec/compiler.ex index 68fd1da..7da9c99 100644 --- a/lib/nimble_parsec/compiler.ex +++ b/lib/nimble_parsec/compiler.ex @@ -900,6 +900,15 @@ defmodule NimbleParsec.Compiler do end end + defp bound_combinator({:bytes, count}, metadata) do + %{counter: counter, offset: offset} = metadata + {var, counter} = build_var(counter) + input = quote do: unquote(var) :: binary - size(unquote(count)) + offset = add_offset(offset, count) + metadata = %{metadata | counter: counter, offset: offset} + {:ok, [input], [], [var], metadata} + end + defp bound_combinator(_, _) do :error end @@ -1025,6 +1034,10 @@ defmodule NimbleParsec.Compiler do Atom.to_string(name) end + defp label({:bytes, count}) do + "#{inspect(count)} bytes" + end + ## Bin segments defp compile_bin_ranges(var, ors, ands) do diff --git a/test/nimble_generator_test.exs b/test/nimble_generator_test.exs index 4a48034..2902d0b 100644 --- a/test/nimble_generator_test.exs +++ b/test/nimble_generator_test.exs @@ -107,6 +107,11 @@ defmodule NimbleGeneratorTest do assert times(string("foo"), min: 2, gen_times: 3) |> generate() == "foofoofoofoofoo" end + test "bytes" do + parsec = bytes(3) + assert byte_size(generate(parsec)) === 3 + end + defparsec :string_foo, string("foo"), export_metadata: true defparsec :string_choice, choice([parsec(:string_foo), string("bar")]), export_metadata: true diff --git a/test/nimble_parsec_test.exs b/test/nimble_parsec_test.exs index 4fdbec5..9adb87e 100644 --- a/test/nimble_parsec_test.exs +++ b/test/nimble_parsec_test.exs @@ -1467,6 +1467,18 @@ defmodule NimbleParsecTest do end end + describe "bytes/2 combinator" do + defparsec :parse_bytes, bytes(3) + + test "succeeds if input has sufficient bytes" do + assert parse_bytes("abc") == {:ok, ["abc"], "", %{}, {1, 0}, 3} + end + + test "fails if input has insufficent bytes" do + assert parse_bytes("ab") == {:error, "expected 3 bytes", "ab", %{}, {1, 0}, 0} + end + end + describe "continuing parser" do defparsecp :digits, [?0..?9] |> ascii_char() |> times(min: 1) |> label("digits") defparsecp :chars, [?a..?z] |> ascii_char() |> times(min: 1) |> label("chars")