Skip to content

Commit 0ccc43e

Browse files
authored
add bytes/2 combinator (#132)
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.
1 parent 0b19551 commit 0ccc43e

File tree

4 files changed

+64
-0
lines changed

4 files changed

+64
-0
lines changed

lib/nimble_parsec.ex

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ defmodule NimbleParsec do
237237
@typep bound_combinator ::
238238
{:bin_segment, [inclusive_range], [exclusive_range], bin_modifier}
239239
| {:string, binary}
240+
| {:bytes, pos_integer}
240241
| :eos
241242

242243
@typep maybe_bound_combinator ::
@@ -380,6 +381,11 @@ defmodule NimbleParsec do
380381
generate(parsecs, mod, gen_times(t, Enum.random(0..max), mod, acc))
381382
end
382383

384+
defp generate([{:bytes, count} | parsecs], mod, acc) do
385+
bytes = bytes_random(count)
386+
generate(parsecs, mod, [bytes | acc])
387+
end
388+
383389
defp generate([], _mod, acc), do: Enum.reverse(acc)
384390

385391
defp gen_export(mod, fun) do
@@ -437,6 +443,10 @@ defmodule NimbleParsec do
437443
defp weighted_random([_ | list], [weight | weights], chosen),
438444
do: weighted_random(list, weights, chosen - weight)
439445

446+
defp bytes_random(count) when is_integer(count) do
447+
:crypto.strong_rand_bytes(count)
448+
end
449+
440450
@doc ~S"""
441451
Returns an empty combinator.
442452
@@ -1774,6 +1784,30 @@ defmodule NimbleParsec do
17741784
choice(combinator, [optional, empty()])
17751785
end
17761786

1787+
@doc """
1788+
Defines a combinator to consume the next `n` bytes from the input.
1789+
1790+
## Examples
1791+
1792+
defmodule MyParser do
1793+
import NimbleParsec
1794+
1795+
defparsec :three_bytes, bytes(3)
1796+
end
1797+
1798+
MyParser.three_bytes("abc")
1799+
#=> {:ok, ["abc"], "", %{}, {1, 0}, 3}
1800+
1801+
MyParser.three_bytes("ab")
1802+
#=> {:error, "expected 3 bytes", "ab", %{}, {1, 0}, 0}
1803+
"""
1804+
@spec bytes(pos_integer) :: t
1805+
@spec bytes(t, pos_integer) :: t
1806+
def bytes(combinator \\ empty(), count)
1807+
when is_combinator(combinator) and is_integer(count) and count > 0 do
1808+
[{:bytes, count} | combinator]
1809+
end
1810+
17771811
## Helpers
17781812

17791813
defp validate_min_and_max!(count_or_opts, required_min \\ 0)

lib/nimble_parsec/compiler.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,15 @@ defmodule NimbleParsec.Compiler do
900900
end
901901
end
902902

903+
defp bound_combinator({:bytes, count}, metadata) do
904+
%{counter: counter, offset: offset} = metadata
905+
{var, counter} = build_var(counter)
906+
input = quote do: unquote(var) :: binary - size(unquote(count))
907+
offset = add_offset(offset, count)
908+
metadata = %{metadata | counter: counter, offset: offset}
909+
{:ok, [input], [], [var], metadata}
910+
end
911+
903912
defp bound_combinator(_, _) do
904913
:error
905914
end
@@ -1025,6 +1034,10 @@ defmodule NimbleParsec.Compiler do
10251034
Atom.to_string(name)
10261035
end
10271036

1037+
defp label({:bytes, count}) do
1038+
"#{inspect(count)} bytes"
1039+
end
1040+
10281041
## Bin segments
10291042

10301043
defp compile_bin_ranges(var, ors, ands) do

test/nimble_generator_test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ defmodule NimbleGeneratorTest do
107107
assert times(string("foo"), min: 2, gen_times: 3) |> generate() == "foofoofoofoofoo"
108108
end
109109

110+
test "bytes" do
111+
parsec = bytes(3)
112+
assert byte_size(generate(parsec)) === 3
113+
end
114+
110115
defparsec :string_foo, string("foo"), export_metadata: true
111116
defparsec :string_choice, choice([parsec(:string_foo), string("bar")]), export_metadata: true
112117

test/nimble_parsec_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,18 @@ defmodule NimbleParsecTest do
14671467
end
14681468
end
14691469

1470+
describe "bytes/2 combinator" do
1471+
defparsec :parse_bytes, bytes(3)
1472+
1473+
test "succeeds if input has sufficient bytes" do
1474+
assert parse_bytes("abc") == {:ok, ["abc"], "", %{}, {1, 0}, 3}
1475+
end
1476+
1477+
test "fails if input has insufficent bytes" do
1478+
assert parse_bytes("ab") == {:error, "expected 3 bytes", "ab", %{}, {1, 0}, 0}
1479+
end
1480+
end
1481+
14701482
describe "continuing parser" do
14711483
defparsecp :digits, [?0..?9] |> ascii_char() |> times(min: 1) |> label("digits")
14721484
defparsecp :chars, [?a..?z] |> ascii_char() |> times(min: 1) |> label("chars")

0 commit comments

Comments
 (0)