Skip to content

Commit

Permalink
add bytes/2 combinator
Browse files Browse the repository at this point in the history
This commit adds a new combinator bytes/2. The combinator will
pull the next n bytes from the input. It combiles to a byte-size
pattern match on the input.
  • Loading branch information
foresttoney committed Nov 28, 2023
1 parent 0b19551 commit c37977e
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 0 deletions.
34 changes: 34 additions & 0 deletions lib/nimble_parsec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 ::
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions lib/nimble_parsec/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)) end
offset = add_offset(offset, count)
metadata = %{metadata | counter: counter, offset: offset}
{:ok, [input], [], [var], metadata}
end

defp bound_combinator(_, _) do
:error
end
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions test/nimble_generator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions test/nimble_parsec_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit c37977e

Please sign in to comment.