diff --git a/lib/dasl/car/stream_decoder.ex b/lib/dasl/car/stream_decoder.ex --- a/lib/dasl/car/stream_decoder.ex +++ b/lib/dasl/car/stream_decoder.ex @@ -115,7 +115,6 @@ defp finish({phase, leftover, _verify}) do # TODO: when loading my repo with `File.stream!()` without any byte chunking, this fails here. # But specifying any bytes makes it work. Is there something seen in it that looks like newline which gets consumed? - IO.inspect(leftover, label: "leftovers") raise "CAR stream ended with #{byte_size(leftover)} unprocessed bytes in phase #{phase}" end diff --git a/lib/dasl/drisl/decoder.ex b/lib/dasl/drisl/decoder.ex --- a/lib/dasl/drisl/decoder.ex +++ b/lib/dasl/drisl/decoder.ex @@ -48,152 +48,270 @@ """ @spec decode(binary()) :: {:ok, any(), binary()} | {:error, atom()} def decode(binary) when is_binary(binary) do - with :ok <- scan_float_precision(binary), + with {:ok, _rest} <- scan_item(binary), {:ok, value, rest} <- cbor_decode(binary), {:ok, value} <- validate_and_remap(value) do {:ok, value, rest} end end - # Structurally scans the CBOR binary, following CBOR item boundaries so that - # payload bytes inside bytestrings and text strings are never mistaken for - # CBOR initial bytes. Rejects half-precision (0xf9) and single-precision - # (0xfa) float initial bytes wherever they appear as CBOR items. - @spec scan_float_precision(binary()) :: :ok | {:error, atom()} - defp scan_float_precision(binary) do - case scan_item(binary) do - {:ok, _rest} -> :ok - {:error, _} = err -> err - end - end + # --------------------------------------------------------------------------- + # Structural scanner + # + # Walks the CBOR binary item by item, validating every structural constraint + # that can be checked at the byte level before handing off to the CBOR + # library for full decoding. Returns {:ok, remaining_bytes} or an error. + # + # Constraints enforced here: + # - Half-precision (0xf9) and single-precision (0xfa) floats — forbidden + # - CBOR `undefined` (0xf7) — forbidden (only true/false/null allowed) + # - All other simple values (0xf8 + any, 0xe0..0xf3) — forbidden + # - Minimal integer encoding — value must use the shortest additional bytes + # - Minimal byte/text string length encoding + # - Minimal array/map count encoding + # - Tag 42 must use the short 2-byte form (0xd8 0x2a); all other tags + # and all non-minimal tag encodings are forbidden + # - Indefinite-length items (additional = 31, break = 0xff) — forbidden + # - UTF-8 validity for text strings + # - Map keys must be CBOR text strings, in bytewise-lexicographic order, + # with no duplicates + # --------------------------------------------------------------------------- - # Scans one CBOR item from `bin`, returns `{:ok, remaining}` or an error. @spec scan_item(binary()) :: {:ok, binary()} | {:error, atom()} + + # Empty input: valid (no item to scan) defp scan_item(<<>>), do: {:ok, <<>>} - # Major type 7, additional 25 = half-precision float — forbidden - defp scan_item(<<0b111_11001, _::binary>>), - do: {:error, :half_precision_float} + # --- Major type 7 (floats and simples) --- + + # 0xf9 — half-precision float: forbidden + defp scan_item(<<0xF9, _::binary>>), do: {:error, :half_precision_float} + + # 0xfa — single-precision float: forbidden + defp scan_item(<<0xFA, _::binary>>), do: {:error, :single_precision_float} + + # 0xfb — 64-bit float: allowed + defp scan_item(<<0xFB, _::binary-size(8), rest::binary>>), do: {:ok, rest} + + # 0xf4 false, 0xf5 true, 0xf6 null — allowed simple values + defp scan_item(<>) when add in [0xF4, 0xF5, 0xF6], do: {:ok, rest} + + # 0xf7 undefined — forbidden + defp scan_item(<<0xF7, _::binary>>), do: {:error, :forbidden_simple} + + # 0xf8 — one-byte extended simple value: all forbidden in DRISL + defp scan_item(<<0xF8, _::8, _::binary>>), do: {:error, :forbidden_simple} + + # 0xe0..0xf3 — unassigned simple values (additional 0..19): forbidden + defp scan_item(<<0b111::3, add::5, _::binary>>) when add < 20, + do: {:error, :forbidden_simple} + + # --- Major type 0 (unsigned integer) — minimal encoding --- + + # additional < 24: value is the additional bits itself (always minimal) + defp scan_item(<<0b000::3, add::5, rest::binary>>) when add < 24, do: {:ok, rest} - # Major type 7, additional 26 = single-precision float — forbidden - defp scan_item(<<0b111_11010, _::binary>>), - do: {:error, :single_precision_float} + # additional = 24: one-byte value; must be >= 24 + defp scan_item(<<0b000_11000, val::8, rest::binary>>) do + if val >= 24, do: {:ok, rest}, else: {:error, :non_minimal_encoding} + end - # Major type 7, additional 27 = 64-bit float — allowed, advance 8 bytes - defp scan_item(<<0b111_11011, _::binary-size(8), rest::binary>>), - do: {:ok, rest} + # additional = 25: two-byte value; must be > 0xFF + defp scan_item(<<0b000_11001, val::16, rest::binary>>) do + if val > 0xFF, do: {:ok, rest}, else: {:error, :non_minimal_encoding} + end + + # additional = 26: four-byte value; must be > 0xFFFF + defp scan_item(<<0b000_11010, val::32, rest::binary>>) do + if val > 0xFFFF, do: {:ok, rest}, else: {:error, :non_minimal_encoding} + end + + # additional = 27: eight-byte value; must be > 0xFFFFFFFF + defp scan_item(<<0b000_11011, val::64, rest::binary>>) do + if val > 0xFFFFFFFF, do: {:ok, rest}, else: {:error, :non_minimal_encoding} + end + + # --- Major type 1 (negative integer) — same minimality rules as type 0 --- + + defp scan_item(<<0b001::3, add::5, rest::binary>>) when add < 24, do: {:ok, rest} + + defp scan_item(<<0b001_11000, val::8, rest::binary>>) do + if val >= 24, do: {:ok, rest}, else: {:error, :non_minimal_encoding} + end + + defp scan_item(<<0b001_11001, val::16, rest::binary>>) do + if val > 0xFF, do: {:ok, rest}, else: {:error, :non_minimal_encoding} + end - # Major type 7, additional < 24 = simple value or float16/32 handled above - # true (0xf5), false (0xf4), null (0xf6), undefined (0xf7) are 1-byte items - defp scan_item(<<0b111::3, add::5, rest::binary>>) when add < 24, - do: {:ok, rest} + defp scan_item(<<0b001_11010, val::32, rest::binary>>) do + if val > 0xFFFF, do: {:ok, rest}, else: {:error, :non_minimal_encoding} + end - # Major type 7, additional 24 = one-byte simple value - defp scan_item(<<0b111_11000, _::8, rest::binary>>), - do: {:ok, rest} + defp scan_item(<<0b001_11011, val::64, rest::binary>>) do + if val > 0xFFFFFFFF, do: {:ok, rest}, else: {:error, :non_minimal_encoding} + end - # Major type 0 (uint) or 1 (nint): value encoded in additional bytes - defp scan_item(<>) when mt in [0, 1] and add < 24, - do: {:ok, rest} + # --- Major type 2 (byte string) — minimal length encoding + skip payload --- - defp scan_item(<>) when mt in [0, 1], - do: {:ok, rest} + defp scan_item(<<0b010::3, len::5, rest::binary>>) when len < 24, + do: skip_bytes(len, rest) - defp scan_item(<>) when mt in [0, 1], - do: {:ok, rest} + defp scan_item(<<0b010_11000, len::8, rest::binary>>) do + if len >= 24, do: skip_bytes(len, rest), else: {:error, :non_minimal_encoding} + end - defp scan_item(<>) when mt in [0, 1], - do: {:ok, rest} + defp scan_item(<<0b010_11001, len::16, rest::binary>>) do + if len > 0xFF, do: skip_bytes(len, rest), else: {:error, :non_minimal_encoding} + end - defp scan_item(<>) when mt in [0, 1], - do: {:ok, rest} + defp scan_item(<<0b010_11010, len::32, rest::binary>>) do + if len > 0xFFFF, do: skip_bytes(len, rest), else: {:error, :non_minimal_encoding} + end - # Major type 2 (bytestring) or 3 (text string): skip payload bytes - defp scan_item(<>) when mt in [2, 3] and len < 24 do - case rest do - <<_::binary-size(len), tail::binary>> -> {:ok, tail} - _ -> {:error, :cbor_decode_error} - end + defp scan_item(<<0b010_11011, len::64, rest::binary>>) do + if len > 0xFFFFFFFF, do: skip_bytes(len, rest), else: {:error, :non_minimal_encoding} end - defp scan_item(<>) when mt in [2, 3] do - case rest do - <<_::binary-size(len), tail::binary>> -> {:ok, tail} - _ -> {:error, :cbor_decode_error} - end + # --- Major type 3 (text string) — minimal length encoding + UTF-8 check --- + + defp scan_item(<<0b011::3, len::5, rest::binary>>) when len < 24, + do: skip_text(len, rest) + + defp scan_item(<<0b011_11000, len::8, rest::binary>>) do + if len >= 24, do: skip_text(len, rest), else: {:error, :non_minimal_encoding} end - defp scan_item(<>) when mt in [2, 3] do - case rest do - <<_::binary-size(len), tail::binary>> -> {:ok, tail} - _ -> {:error, :cbor_decode_error} - end + defp scan_item(<<0b011_11001, len::16, rest::binary>>) do + if len > 0xFF, do: skip_text(len, rest), else: {:error, :non_minimal_encoding} end - defp scan_item(<>) when mt in [2, 3] do - case rest do - <<_::binary-size(len), tail::binary>> -> {:ok, tail} - _ -> {:error, :cbor_decode_error} - end + defp scan_item(<<0b011_11010, len::32, rest::binary>>) do + if len > 0xFFFF, do: skip_text(len, rest), else: {:error, :non_minimal_encoding} end - defp scan_item(<>) when mt in [2, 3] do - case rest do - <<_::binary-size(len), tail::binary>> -> {:ok, tail} - _ -> {:error, :cbor_decode_error} - end + defp scan_item(<<0b011_11011, len::64, rest::binary>>) do + if len > 0xFFFFFFFF, do: skip_text(len, rest), else: {:error, :non_minimal_encoding} end - # Major type 4 (array): scan `count` sub-items + # --- Major type 4 (array) — minimal count encoding, then scan sub-items --- + defp scan_item(<<0b100::3, count::5, rest::binary>>) when count < 24, do: scan_items(count, rest) - defp scan_item(<<0b100_11000, count::8, rest::binary>>), - do: scan_items(count, rest) + defp scan_item(<<0b100_11000, count::8, rest::binary>>) do + if count >= 24, do: scan_items(count, rest), else: {:error, :non_minimal_encoding} + end - defp scan_item(<<0b100_11001, count::16, rest::binary>>), - do: scan_items(count, rest) + defp scan_item(<<0b100_11001, count::16, rest::binary>>) do + if count > 0xFF, do: scan_items(count, rest), else: {:error, :non_minimal_encoding} + end - defp scan_item(<<0b100_11010, count::32, rest::binary>>), - do: scan_items(count, rest) + defp scan_item(<<0b100_11010, count::32, rest::binary>>) do + if count > 0xFFFF, do: scan_items(count, rest), else: {:error, :non_minimal_encoding} + end - defp scan_item(<<0b100_11011, count::64, rest::binary>>), - do: scan_items(count, rest) + defp scan_item(<<0b100_11011, count::64, rest::binary>>) do + if count > 0xFFFFFFFF, do: scan_items(count, rest), else: {:error, :non_minimal_encoding} + end - # Major type 5 (map): scan `count * 2` sub-items (key + value pairs) + # --- Major type 5 (map) — minimal count encoding, ordered string keys --- + defp scan_item(<<0b101::3, count::5, rest::binary>>) when count < 24, - do: scan_items(count * 2, rest) + do: scan_map_pairs(count, rest, nil) - defp scan_item(<<0b101_11000, count::8, rest::binary>>), - do: scan_items(count * 2, rest) + defp scan_item(<<0b101_11000, count::8, rest::binary>>) do + if count >= 24, + do: scan_map_pairs(count, rest, nil), + else: {:error, :non_minimal_encoding} + end - defp scan_item(<<0b101_11001, count::16, rest::binary>>), - do: scan_items(count * 2, rest) + defp scan_item(<<0b101_11001, count::16, rest::binary>>) do + if count > 0xFF, + do: scan_map_pairs(count, rest, nil), + else: {:error, :non_minimal_encoding} + end - defp scan_item(<<0b101_11010, count::32, rest::binary>>), - do: scan_items(count * 2, rest) + defp scan_item(<<0b101_11010, count::32, rest::binary>>) do + if count > 0xFFFF, + do: scan_map_pairs(count, rest, nil), + else: {:error, :non_minimal_encoding} + end - defp scan_item(<<0b101_11011, count::64, rest::binary>>), - do: scan_items(count * 2, rest) + defp scan_item(<<0b101_11011, count::64, rest::binary>>) do + if count > 0xFFFFFFFF, + do: scan_map_pairs(count, rest, nil), + else: {:error, :non_minimal_encoding} + end - # Major type 6 (tag): scan the tag number header then one sub-item - defp scan_item(<<0b110::3, _tag::5, rest::binary>>), - do: scan_item(rest) + # --- Major type 6 (tag) --- - defp scan_item(<<0b110_11000, _::8, rest::binary>>), - do: scan_item(rest) + # Tag 2 (unsigned bignum) and tag 3 (negative bignum) in short 1-byte form: + # allowed by DRISL/c42 only for integers outside [-2^64, 2^64-1]. The + # tagged value must be a bytestring with no leading zero bytes and a value + # that does not fit in a CBOR major type 0/1 integer. + defp scan_item(<<0b110_00010, rest::binary>>), do: scan_bignum(rest) + defp scan_item(<<0b110_00011, rest::binary>>), do: scan_bignum(rest) - defp scan_item(<<0b110_11001, _::16, rest::binary>>), - do: scan_item(rest) + # Tag 42 in short 2-byte form (0xd8 0x2a) is the only CID tag form permitted. + defp scan_item(<<0xD8, 0x2A, rest::binary>>), do: scan_item(rest) - defp scan_item(<<0b110_11010, _::32, rest::binary>>), - do: scan_item(rest) + # Any other tag (including non-minimal encodings of tag 42, tag 2, tag 3, + # or any other tag number) is forbidden. + defp scan_item(<<0b110::3, _::5, _::binary>>), do: {:error, :forbidden_tag} - defp scan_item(<<0b110_11011, _::64, rest::binary>>), - do: scan_item(rest) + # --- Catch-all: invalid / indefinite / reserved --- defp scan_item(_), do: {:error, :cbor_decode_error} + # --------------------------------------------------------------------------- + # Helpers + # --------------------------------------------------------------------------- + + # Validates that the next item in `bin` is a bytestring representing a bignum + # value that requires tag 2/3 — i.e. > 0xFFFFFFFFFFFFFFFF (2^64 - 1). + # Leading zero bytes are forbidden (non-minimal). Returns {:ok, rest}. + @uint64_max 0xFFFFFFFFFFFFFFFF + + @spec scan_bignum(binary()) :: {:ok, binary()} | {:error, atom()} + + defp scan_bignum(<<0b010::3, len::5, rest::binary>>) when len < 24 and len > 0 do + case rest do + <<0x00, _::binary>> -> + {:error, :non_minimal_encoding} + + <> -> + if :binary.decode_unsigned(bytes) > @uint64_max, + do: {:ok, tail}, + else: {:error, :non_minimal_encoding} + + _ -> + {:error, :cbor_decode_error} + end + end + + defp scan_bignum(_), do: {:error, :non_minimal_encoding} + + @spec skip_bytes(non_neg_integer(), binary()) :: {:ok, binary()} | {:error, atom()} + defp skip_bytes(len, rest) do + case rest do + <<_::binary-size(len), tail::binary>> -> {:ok, tail} + _ -> {:error, :cbor_decode_error} + end + end + + @spec skip_text(non_neg_integer(), binary()) :: {:ok, binary()} | {:error, atom()} + defp skip_text(len, rest) do + case rest do + <> -> + if String.valid?(text), + do: {:ok, tail}, + else: {:error, :invalid_utf8} + + _ -> + {:error, :cbor_decode_error} + end + end + @spec scan_items(non_neg_integer(), binary()) :: {:ok, binary()} | {:error, atom()} defp scan_items(0, rest), do: {:ok, rest} @@ -204,6 +322,87 @@ {:error, _} = err -> err end end + # Scans `count` map key/value pairs. Keys must be CBOR text strings and must + # be in strictly increasing bytewise-lexicographic order of their encoded + # form (length-first, then content). `prev_key_enc` is the encoded bytes of + # the previous key for ordering comparison; nil on the first pair. + @spec scan_map_pairs(non_neg_integer(), binary(), binary() | nil) :: + {:ok, binary()} | {:error, atom()} + defp scan_map_pairs(0, rest, _prev), do: {:ok, rest} + + defp scan_map_pairs(n, bin, prev_key_enc) do + with {:ok, key_enc, after_key} <- extract_text_key(bin), + :ok <- check_key_order(key_enc, prev_key_enc), + {:ok, after_value} <- scan_item(after_key) do + scan_map_pairs(n - 1, after_value, key_enc) + end + end + + # Extracts one CBOR text string key from the binary, returning its full + # encoded bytes (including the initial byte) and the remaining binary. + # Rejects non-text-string keys and non-minimal length encodings. + @spec extract_text_key(binary()) :: {:ok, binary(), binary()} | {:error, atom()} + + defp extract_text_key(<<0b011::3, len::5, rest::binary>>) when len < 24 do + head = <<0b011::3, len::5>> + + case rest do + <> -> + if String.valid?(text), + do: {:ok, head <> text, tail}, + else: {:error, :invalid_utf8} + + _ -> + {:error, :cbor_decode_error} + end + end + + defp extract_text_key(<<0b011_11000, len::8, rest::binary>>) when len >= 24, + do: extract_text_key_payload(<<0b011_11000, len::8>>, len, rest) + + defp extract_text_key(<<0b011_11000, _::8, _::binary>>), + do: {:error, :non_minimal_encoding} + + defp extract_text_key(<<0b011_11001, len::16, rest::binary>>) when len > 0xFF, + do: extract_text_key_payload(<<0b011_11001, len::16>>, len, rest) + + defp extract_text_key(<<0b011_11001, _::16, _::binary>>), + do: {:error, :non_minimal_encoding} + + # Any other initial byte is not a text string key + defp extract_text_key(_), do: {:error, :non_string_map_key} + + @spec extract_text_key_payload(binary(), non_neg_integer(), binary()) :: + {:ok, binary(), binary()} | {:error, atom()} + defp extract_text_key_payload(head, len, rest) do + case rest do + <> -> + if String.valid?(text), + do: {:ok, head <> text, tail}, + else: {:error, :invalid_utf8} + + _ -> + {:error, :cbor_decode_error} + end + end + + @spec check_key_order(binary(), binary() | nil) :: :ok | {:error, atom()} + defp check_key_order(_key_enc, nil), do: :ok + + defp check_key_order(key_enc, prev_enc) do + # Bytewise-lexicographic ordering by encoded form: first by length, then + # by content. Since CBOR minimal encoding encodes length in the head bytes, + # comparing the full encoded binary is equivalent to length-first ordering. + if byte_size(key_enc) > byte_size(prev_enc) or + (byte_size(key_enc) == byte_size(prev_enc) and key_enc > prev_enc), + do: :ok, + else: {:error, :map_key_order} + end + + # --------------------------------------------------------------------------- + # CBOR library decode pass + # --------------------------------------------------------------------------- + @spec cbor_decode(binary()) :: {:ok, any(), binary()} | {:error, atom()} defp cbor_decode(binary) do case CBOR.decode(binary) do @@ -212,10 +411,19 @@ {:error, _} -> {:error, :cbor_decode_error} end end - # Recursively validate and remap a decoded CBOR value. + # --------------------------------------------------------------------------- + # Post-decode validation and remapping + # # The CBOR library auto-decodes certain tags to Elixir structs (tag 0 → # DateTime/Date/Time, tag 32 → URI). These are forbidden in DRISL. + # Tag 42 → CID struct. Tag 2/3 (bignum) → Elixir integer if in range, + # or left as %CBOR.Tag{tag: 2|3} for very large values; both are allowed + # in DRISL/c42 as long as they are minimally encoded (enforced above in + # the structural scanner: bignum tag is still tag 42 short-form only). + # --------------------------------------------------------------------------- + @spec validate_and_remap(any()) :: {:ok, any()} | {:error, atom()} + defp validate_and_remap(%CBOR.Tag{tag: 42} = tag) do case DASL.CID.from_cbor(tag) do {:ok, cid} -> {:ok, cid} diff --git a/lib/dasl/drisl/encoder.ex b/lib/dasl/drisl/encoder.ex --- a/lib/dasl/drisl/encoder.ex +++ b/lib/dasl/drisl/encoder.ex @@ -89,6 +89,10 @@ defp encode_value(s) when is_binary(s) do {:ok, encode_string(3, s)} end + defp encode_value(%CBOR.Tag{tag: :bytes, value: data}) when is_binary(data) do + {:ok, encode_string(2, data)} + end + defp encode_value(%DASL.CID{} = cid) do %CBOR.Tag{tag: 42, value: %CBOR.Tag{tag: :bytes, value: cid_bytes}} = DASL.CID.to_cbor(cid) {:ok, encode_head(6, 42) <> encode_string(2, cid_bytes)} diff --git a/test/dasl/drisl_fixtures_test.exs b/test/dasl/drisl_fixtures_test.exs new file mode 100644 --- /dev/null +++ b/test/dasl/drisl_fixtures_test.exs @@ -0,0 +1,124 @@ +defmodule DASL.DRISL.FixturesTest do + @moduledoc """ + Runs the hyphacoop/dasl-testing CBOR fixture suite against DASL.DRISL. + + Fixture source: https://github.com/hyphacoop/dasl-testing/tree/main/fixtures/cbor + + Test types: + - `roundtrip` — decode must succeed with no leftover bytes, then re-encode + must produce exactly the original bytes. + - `invalid_in` — decode must fail (error tuple or leftover bytes). + - `invalid_out` — decode may succeed; re-encode must fail. + + Only test cases whose `tags` list intersects with `@applicable_tags` are run. + Add a test's `"id"` field to `@skipped_ids` to skip it explicitly. + """ + + use ExUnit.Case, async: true + + @fixtures_dir Path.join([__DIR__, "..", "fixtures", "cbor"]) + + # Tags this implementation claims conformance with. DRISL implements the c42 + # profile, which differs from dag-cbor in some areas (e.g. bignum support), + # so "dag-cbor" is intentionally excluded. + @applicable_tags MapSet.new(["c42", "dasl-cid", "basic"]) + + # Explicit per-ID skips (use the "id" field from the fixture JSON). + @skipped_ids MapSet.new([]) + + # Explicit per-name skips as {name, type} pairs, for fixtures without an + # "id" field. Add entries here to skip by name + test type. + # + # TODO: "Big DASL CID" — BLAKE3 (hash type 0x1e) CIDs are not yet supported; + # the CID implementation currently only handles SHA-256 (0x12). Needs a + # configurable hash registry or explicit BLAKE3 support to pass. + @skipped_names MapSet.new([{"Big DASL CID", "roundtrip"}]) + + # --------------------------------------------------------------------------- + # Fixture loading + # --------------------------------------------------------------------------- + + @fixture_files File.ls!(@fixtures_dir) + |> Enum.filter(&String.ends_with?(&1, ".json")) + |> Enum.sort() + + @fixtures Enum.flat_map(@fixture_files, fn filename -> + path = Path.join(@fixtures_dir, filename) + cases = path |> File.read!() |> JSON.decode!() + Enum.map(cases, fn tc -> Map.put(tc, "file", filename) end) + end) + + for fixture <- @fixtures do + file = fixture["file"] + name = fixture["name"] + type = fixture["type"] + data = fixture["data"] + tags = MapSet.new(fixture["tags"]) + id = fixture["id"] + + applicable? = not MapSet.disjoint?(tags, @applicable_tags) + + skipped? = + (id != nil and MapSet.member?(@skipped_ids, id)) or + MapSet.member?(@skipped_names, {name, type}) + + test_name = "[#{file}] #{name} (#{type})" + + cond do + skipped? -> + @tag :skip + test test_name do + :ok + end + + not applicable? -> + :ok + + type == "roundtrip" -> + @tag fixture_type: :roundtrip + test test_name do + bytes = Base.decode16!(unquote(data), case: :lower) + + assert {:ok, term, rest} = DASL.DRISL.decode(bytes), + "decode failed for: #{unquote(data)}" + + assert rest == "", + "decode left unconsumed bytes (#{byte_size(rest)} bytes) for: #{unquote(data)}" + + assert {:ok, ^bytes} = DASL.DRISL.encode(term), + "re-encode did not reproduce original bytes for: #{unquote(data)}" + end + + type == "invalid_in" -> + @tag fixture_type: :invalid_in + test test_name do + bytes = Base.decode16!(unquote(data), case: :lower) + + result = DASL.DRISL.decode(bytes) + + refute match?({:ok, _, ""}, result), + "expected decode to fail or leave leftover bytes, but got clean {:ok, _, \"\"} for: #{unquote(data)}" + end + + type == "invalid_out" -> + @tag fixture_type: :invalid_out + test test_name do + bytes = Base.decode16!(unquote(data), case: :lower) + + case DASL.DRISL.decode(bytes) do + {:ok, term, _rest} -> + assert {:error, _} = DASL.DRISL.encode(term), + "expected encode to fail for: #{unquote(data)}" + + {:error, _} -> + # If decode already fails, the invalid_out condition is trivially + # satisfied — the data cannot be produced by a compliant encoder. + :ok + end + end + + true -> + :ok + end + end +end diff --git a/test/fixtures/cbor/cid.json b/test/fixtures/cbor/cid.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/cid.json @@ -0,0 +1,86 @@ +[ + { + "type": "invalid_in", + "data": "d9002a582500015512205891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + "name": "long CID tag", + "tags": ["dag-cbor"], + "desc": "a longer than necessary CBOR representation of the CID tag is used, which is invalid in dag-cbor" + }, + { + "type": "roundtrip", + "data": "d82a582500015512205891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + "name": "valid CID with short tag", + "tags": ["dag-cbor", "dasl-cid", "c42"], + "desc": "a valid CID (raw, SHA-256) using the correct short CBOR representation of the CID tag" + }, + { + "type": "invalid_in", + "data": "d82a4100", + "name": "invalid CID", + "tags": ["basic"], + "desc": "this is tagged as a CID but is not valid, only containing a single zero byte" + }, + { + "type": "invalid_in", + "data": "d82a582300122022ad631c69ee983095b5b8acd029ff94aff1dc6c48837878589a92b90dfea317", + "name": "CIDv0", + "tags": ["dasl-cid"], + "desc": "a valid CIDv0, which is invalid as a DASL CID" + }, + { + "type": "invalid_in", + "data": "d82a582400015512205891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be", + "name": "invalid hash size", + "tags": ["basic"], + "desc": "a CIDv1 with a stated hash size of 32 bytes, but only 31 bytes provided" + }, + { + "type": "roundtrip", + "data": "d82a58250001551e208e4c7c1b99dbfd50e7a95185fead5ee1448fa904a2fdd778eaf5f2dbfd629a99", + "name": "Big DASL CID", + "tags": ["dasl-cid"], + "desc": "a valid BLAKE3 CID, also known as Big DASL" + }, + { + "type": "invalid_in", + "data": "d82a450001551200", + "name": "empty CID", + "tags": ["dasl-cid"], + "desc": "a valid CIDv1 with no hash data (hash length of zero)" + }, + { + "type": "invalid_in", + "data": "d82a5824000155121f5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be", + "name": "short hash digest", + "tags": ["dasl-cid"], + "desc": "a valid (IPFS) CID that has a hash digest < 32 bytes" + }, + { + "type": "invalid_in", + "data": "d82a582600015512215891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be0300", + "name": "long hash digest", + "tags": ["dasl-cid"], + "desc": "a valid (IPFS) CID that has a hash digest > 32 bytes" + }, + { + "type": "invalid_in", + "data": "d82a58250001701220e9822efc7c48027a5429fdbd988d02b2b8e4eaee8f62c32bd1021dcf922e05de", + "name": "CIDv1 that isn't raw or cbor", + "tags": ["dasl-cid"], + "desc": "a valid CIDv1 that uses dag-pb, invalid in the DASL CID spec" + }, + { + "type": "invalid_in", + "data": "d82a58190001551114f572d396fae9206628714fb2ce00f72e94f2258f", + "name": "disallowed hash type (SHA-1)", + "tags": ["dasl-cid"], + "desc": "a valid CIDv1 that uses the SHA-1 hash, invalid in the DASL CID spec" + }, + { + "type": "invalid_in", + "data": "d82a5824015512205891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + "name": "CID with no null byte", + "tags": ["basic"], + "desc": "valid CIDs in CBOR must start with 0x00, this one misses that but is otherwise valid" + } +] diff --git a/test/fixtures/cbor/concat.json b/test/fixtures/cbor/concat.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/concat.json @@ -0,0 +1,9 @@ +[ + { + "type": "invalid_in", + "data": "0000", + "name": "concatenated objects / CBOR sequence", + "tags": ["dag-cbor"], + "desc": "a CBOR sequence of two zeros, invalid in the DAG-CBOR spec" + } +] diff --git a/test/fixtures/cbor/floats.json b/test/fixtures/cbor/floats.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/floats.json @@ -0,0 +1,156 @@ +[ + { + "type": "roundtrip", + "data": "f93e00", + "name": "float reduction", + "tags": ["rfc8949"], + "desc": "a 16-bit float, a valid representation according to RFC 8949" + }, + { + "type": "roundtrip", + "data": "fb3ff8000000000000", + "name": "64-bit floats only", + "tags": ["dag-cbor", "c42"], + "desc": "a 64-bit float, the only valid float representation for dag-cbor" + }, + { + "type": "invalid_in", + "data": "f93e00", + "name": "64-bit floats only", + "tags": ["dag-cbor", "c42"], + "desc": "a 16-bit float, an invalid input for dag-cbor" + }, + { + "type": "invalid_in", + "data": "fb3ff8000000000000", + "name": "float reduction", + "tags": ["rfc8949"], + "desc": "a 64-bit float that could be reduced, invalid for RFC 8949" + }, + { + "type": "roundtrip", + "data": "f97e00", + "name": "short NaN", + "tags": ["rfc8949", "CBOR-Core"], + "desc": "the only valid NaN representation for RFC 8949" + }, + { + "type": "invalid_in", + "data": "fb7ff8000000000000", + "name": "long NaN", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an invalid NaN representation, too long (RFC 8949) or because NaNs are invalid (dag-cbor)" + }, + { + "type": "roundtrip", + "data": "fb8000000000000000", + "name": "negative zero float64", + "tags": ["dag-cbor", "c42"], + "desc": "testing that negative zero can be roundtripped without becoming positive or an integer" + }, + { + "type": "roundtrip", + "data": "fb0000000000000000", + "name": "zero float64", + "tags": ["dag-cbor", "c42"], + "desc": "a standard float like 0.0 shouldn't become an integer" + }, + { + "type": "roundtrip", + "data": "f90000", + "name": "zero float16", + "tags": ["rfc8949", "CBOR-Core"], + "desc": "0.0 as a 16-bit float is valid and required for RFC 8949" + }, + { + "type": "roundtrip", + "data": "fb0000000000000001", + "name": "64-bit subnormal number", + "tags": ["basic"], + "desc": "subnormal numbers are reduced to zero in some floating point implementations, but should not be here" + }, + { + "type": "invalid_in", + "data": "f97e00", + "name": "short NaN", + "tags": ["dag-cbor", "c42"], + "desc": "even short representations of NaN are invalid for dag-cbor" + }, + { + "type": "invalid_in", + "data": "f97c00", + "name": "short Inf", + "tags": ["dag-cbor", "c42"], + "desc": "infinity is invalid in dag-cbor" + }, + { + "type": "invalid_in", + "data": "fa7f800000", + "name": "32-bit Inf", + "tags": ["dag-cbor", "c42"], + "desc": "infinity is invalid in dag-cbor" + }, + { + "type": "invalid_in", + "data": "fb7ff0000000000000", + "name": "64-bit Inf", + "tags": ["dag-cbor", "c42"], + "desc": "infinity is invalid in dag-cbor" + }, + { + "type": "invalid_in", + "data": "f9fc00", + "name": "short -Inf", + "tags": ["dag-cbor", "c42"], + "desc": "negative infinity is invalid in dag-cbor" + }, + { + "type": "invalid_in", + "data": "faff800000", + "name": "32-bit -Inf", + "tags": ["dag-cbor", "c42"], + "desc": "negative infinity is invalid in dag-cbor" + }, + { + "type": "invalid_in", + "data": "fbfff0000000000000", + "name": "64-bit -Inf", + "tags": ["dag-cbor", "c42"], + "desc": "negative infinity is invalid in dag-cbor" + }, + { + "type": "invalid_out", + "data": "f97e00", + "name": "NaN", + "tags": ["dag-cbor", "c42"], + "desc": "NaN should not be encoded for dag-cbor" + }, + { + "type": "invalid_out", + "data": "f97c00", + "name": "Inf", + "tags": ["dag-cbor", "c42"], + "desc": "infinity should not be encoded for dag-cbor" + }, + { + "type": "invalid_out", + "data": "f9fc00", + "name": "-Inf", + "tags": ["dag-cbor", "c42"], + "desc": "negative infinity should not be encoded for dag-cbor" + }, + { + "type": "invalid_in", + "data": "f97e01", + "name": "NaN with payload", + "tags": ["CBOR-Core"], + "desc": "NaNs with payloads (set bits) are invalid in CBOR::Core" + }, + { + "type": "invalid_in", + "data": "f97d00", + "name": "signaling NaN", + "tags": ["dag-cbor", "c42"], + "desc": "signaling NaNs are invalid in dag-cbor (like other NaNs)" + } +] diff --git a/test/fixtures/cbor/indefinite.json b/test/fixtures/cbor/indefinite.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/indefinite.json @@ -0,0 +1,30 @@ +[ + { + "type": "invalid_in", + "data": "bfff", + "name": "indefinite map", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "maps with undefined length are banned" + }, + { + "type": "invalid_in", + "data": "9fff", + "name": "indefinite array", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "arrays with undefined length are banned" + }, + { + "type": "invalid_in", + "data": "5fff", + "name": "indefinite byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "byte strings with undefined length are banned" + }, + { + "type": "invalid_in", + "data": "7fff", + "name": "indefinite string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "strings with undefined length are banned" + } +] diff --git a/test/fixtures/cbor/integer_range.json b/test/fixtures/cbor/integer_range.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/integer_range.json @@ -0,0 +1,80 @@ +[ + { + "type": "roundtrip", + "data": "1bffffffffffffffff", + "name": "largest CBOR integer, 2^64-1", + "tags": ["basic"], + "desc": "the largest possible value for CBOR's positive integer type" + }, + { + "type": "roundtrip", + "data": "3bffffffffffffffff", + "name": "smallest CBOR integer, -(2^64)", + "tags": ["basic"], + "desc": "the smallest possible value for CBOR's negative integer type" + }, + { + "type": "roundtrip", + "data": "1b001fffffffffffff", + "name": "max safe JS integer, 2^53-1", + "tags": ["basic"], + "desc": "the largest integer considered safe for use in JavaScript" + }, + { + "type": "roundtrip", + "data": "1b0020000000000001", + "name": "2^53+1, unrepresentable in JS", + "tags": ["basic"], + "desc": "an integer that can't be represented in JavaScript (without BigInt)" + }, + { + "type": "roundtrip", + "data": "3b001ffffffffffffe", + "name": "min safe JS integer, -(2^53-1)", + "tags": ["basic"], + "desc": "the smallest integer considered safe for use in JavaScript" + }, + { + "type": "roundtrip", + "data": "3b0020000000000000", + "name": "-(2^53+1), unrepresentable in JS", + "tags": ["basic"], + "desc": "a negative integer that can't be represented in JavaScript (without BigInt)" + }, + { + "type": "roundtrip", + "data": "c249010000000000000000", + "name": "smallest unsigned bigint", + "tags": ["CBOR-Core", "c42"], + "desc": "the smallest positive CBOR big integer value, valid in specs that support big integers" + }, + { + "type": "roundtrip", + "data": "c349010000000000000000", + "name": "highest negative bigint", + "tags": ["CBOR-Core", "c42"], + "desc": "the largest negative CBOR big integer value, valid in specs that support big integers" + }, + { + "type": "invalid_out", + "data": "c249010000000000000000", + "name": "bignum", + "tags": ["dag-cbor"], + "desc": "big integers are banned for encoding by dag-cbor", + "id": "bignum_invalid_out" + }, + { + "type": "invalid_in", + "data": "c249010000000000000000", + "name": "bignum", + "tags": ["dag-cbor"], + "desc": "big integers are banned for decoding by dag-cbor" + }, + { + "type": "invalid_in", + "data": "c24101", + "name": "bignum with small value", + "tags": ["dag-cbor", "rfc8949"], + "desc": "an bignum with a value in the valid range is still banned in dag-cbor, and improper in RFC 8949 preferred serialization" + } +] diff --git a/test/fixtures/cbor/map_keys.json b/test/fixtures/cbor/map_keys.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/map_keys.json @@ -0,0 +1,44 @@ +[ + { + "type": "roundtrip", + "data": "a1616100", + "name": "map with string key", + "tags": ["basic"], + "desc": "a simple map valid by any spec" + }, + { + "type": "invalid_in", + "data": "a10000", + "name": "map with int key", + "tags": ["dag-cbor", "c42"], + "desc": "a map with an integer key, invalid to decode dag-cbor" + }, + { + "type": "invalid_out", + "data": "a10000", + "name": "map with int key", + "tags": ["dag-cbor", "c42"], + "desc": "a map with an integer key, invalid to encode dag-cbor" + }, + { + "type": "roundtrip", + "data": "a361610161620262616103", + "name": "map keys in correct order", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a map with string keys, sorted by their bytes, which also sorts them by length in CBOR" + }, + { + "type": "invalid_in", + "data": "a2616201616100", + "name": "map keys in incorrect order", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a map with string keys, not sorted by bytes/length" + }, + { + "type": "invalid_in", + "data": "a2616100616101", + "name": "duplicate map keys", + "tags": ["basic", "dag-cbor", "c42", "dCBOR"], + "desc": "a map with duplicate string keys which is invalid by the CBOR spec" + } +] diff --git a/test/fixtures/cbor/numeric_reduction.json b/test/fixtures/cbor/numeric_reduction.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/numeric_reduction.json @@ -0,0 +1,30 @@ +[ + { + "type": "invalid_in", + "data": "f90000", + "name": "0.0 could be reduced", + "tags": ["dCBOR"], + "desc": "0.0 could be reduced to an integer according to dCBOR" + }, + { + "type": "invalid_in", + "data": "f98000", + "name": "-0.0 could be reduced", + "tags": ["dCBOR"], + "desc": "-0.0 could be reduced to integer zero according to dCBOR" + }, + { + "type": "roundtrip", + "data": "a20a00f9490000", + "name": "map with equal int and float keys", + "tags": ["dCBOR", "CBOR-Core"], + "desc": "a map with integer and float keys that have equal value, and so the float should not be reduced to an integer" + }, + { + "type": "roundtrip", + "data": "fbc3e0000000000001", + "name": "too small for int reduction, <-(2^63)", + "tags": ["dCBOR"], + "desc": "a floating point value too small to be stored in a CBOR integer should not be reduced according to dCBOR" + } +] diff --git a/test/fixtures/cbor/recursion.json b/test/fixtures/cbor/recursion.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/recursion.json @@ -0,0 +1,9 @@ +[ + { + "type": "roundtrip", + "data": "a260818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818181818180652474797065726170702e62736b792e666565642e70736f74", + "name": "deeply nested object", + "tags": ["basic"], + "desc": "this CBOR object has many nested arrays which can trigger recursion depth issues for some parsers" + } +] diff --git a/test/fixtures/cbor/short_form.json b/test/fixtures/cbor/short_form.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/short_form.json @@ -0,0 +1,226 @@ +[ + { + "type": "roundtrip", + "data": "a1616100", + "name": "one item map", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a valid, minimally encoded map with one item" + }, + { + "type": "invalid_in", + "data": "b801616100", + "name": "unnecessary one-byte map definition", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a map with a length needlessly encoded as separate byte" + }, + { + "type": "invalid_in", + "data": "b90001616100", + "name": "unnecessary two-byte map definition", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a map with a length needlessly encoded as two bytes" + }, + { + "type": "invalid_in", + "data": "ba00000001616100", + "name": "unnecessary four-byte map definition", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a map with a length needlessly encoded as four bytes" + }, + { + "type": "invalid_in", + "data": "bb0000000000000001616100", + "name": "unnecessary eight-byte map definition", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a map with a length needlessly encoded as eight bytes" + }, + { + "type": "roundtrip", + "data": "01", + "name": "small unsigned integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a valid, minimally encoded unsigned integer" + }, + { + "type": "invalid_in", + "data": "1801", + "name": "unnecessary one-byte unsigned integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an unsigned integer with a length needlessly encoded as separate byte" + }, + { + "type": "invalid_in", + "data": "190001", + "name": "unnecessary two-byte unsigned integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an unsigned integer with a length needlessly encoded as two bytes" + }, + { + "type": "invalid_in", + "data": "1a00000001", + "name": "unnecessary four-byte unsigned integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an unsigned integer with a length needlessly encoded as four bytes" + }, + { + "type": "invalid_in", + "data": "1b0000000000000001", + "name": "unnecessary eight-byte unsigned integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an unsigned integer with a length needlessly encoded as eight bytes" + }, + { + "type": "roundtrip", + "data": "20", + "name": "small negative integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a valid, minimally encoded negative integer" + }, + { + "type": "invalid_in", + "data": "3800", + "name": "unnecessary one-byte negative integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a negative integer with a length needlessly encoded as separate byte" + }, + { + "type": "invalid_in", + "data": "390000", + "name": "unnecessary two-byte negative integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a negative integer with a length needlessly encoded as two bytes" + }, + { + "type": "invalid_in", + "data": "3a00000000", + "name": "unnecessary four-byte negative integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a negative integer with a length needlessly encoded as four bytes" + }, + { + "type": "invalid_in", + "data": "3b0000000000000000", + "name": "unnecessary eight-byte negative integer", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a negative integer with a length needlessly encoded as eight bytes" + }, + { + "type": "roundtrip", + "data": "40", + "name": "empty byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a valid, minimally encoded empty byte string" + }, + { + "type": "invalid_in", + "data": "5800", + "name": "unnecessary one-byte byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a byte string with a length needlessly encoded as separate byte" + }, + { + "type": "invalid_in", + "data": "590000", + "name": "unnecessary two-byte byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a byte string with a length needlessly encoded as two bytes" + }, + { + "type": "invalid_in", + "data": "5a00000000", + "name": "unnecessary four-byte byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a byte string with a length needlessly encoded as four bytes" + }, + { + "type": "invalid_in", + "data": "5b0000000000000000", + "name": "unnecessary eight-byte byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a byte string with a length needlessly encoded as eight bytes" + }, + { + "type": "roundtrip", + "data": "60", + "name": "empty string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a valid, minimally encoded empty string" + }, + { + "type": "invalid_in", + "data": "7800", + "name": "unnecessary one-byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a string with a length needlessly encoded as separate byte" + }, + { + "type": "invalid_in", + "data": "790000", + "name": "unnecessary two-byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a string with a length needlessly encoded as two bytes" + }, + { + "type": "invalid_in", + "data": "7a00000000", + "name": "unnecessary four-byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a string with a length needlessly encoded as four bytes" + }, + { + "type": "invalid_in", + "data": "7b0000000000000000", + "name": "unnecessary eight-byte string", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a string with a length needlessly encoded as eight bytes" + }, + { + "type": "roundtrip", + "data": "80", + "name": "empty array", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "a valid, minimally encoded empty map" + }, + { + "type": "invalid_in", + "data": "9800", + "name": "unnecessary one-byte array", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an array with a length needlessly encoded as separate byte" + }, + { + "type": "invalid_in", + "data": "990000", + "name": "unnecessary two-byte array", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an array with a length needlessly encoded as two bytes" + }, + { + "type": "invalid_in", + "data": "9a00000000", + "name": "unnecessary four-byte array", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an array with a length needlessly encoded as four bytes" + }, + { + "type": "invalid_in", + "data": "9b0000000000000000", + "name": "unnecessary eight-byte array", + "tags": ["rfc8949", "dag-cbor", "c42"], + "desc": "an array with a length needlessly encoded as eight bytes" + }, + { + "type": "invalid_in", + "data": "c34a00010000000000000000", + "name": "bigint with leading zero bytes", + "tags": ["rfc8949", "c42"], + "desc": "a big integer encoded with unnecessary leading zeros" + }, + { + "type": "invalid_in", + "data": "c243010000", + "name": "value too small for bigint", + "tags": ["c42"], + "desc": "a big integer encoded with a value that could be a regular integer instead" + } +] diff --git a/test/fixtures/cbor/simple.json b/test/fixtures/cbor/simple.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/simple.json @@ -0,0 +1,39 @@ +[ + { + "type": "invalid_in", + "data": "f7", + "name": "simple value 'undefined'", + "tags": ["dag-cbor", "c42", "dCBOR"], + "desc": "the CBOR value undefined is banned for decoding by some specs" + }, + { + "type": "invalid_out", + "data": "f7", + "name": "simple value 'undefined'", + "tags": ["dag-cbor", "c42", "dCBOR"], + "desc": "the CBOR value undefined is banned for encoding by some specs", + "id": "undefined_invalid_out" + }, + { + "type": "invalid_in", + "data": "e0", + "name": "unassigned simple value", + "tags": ["basic"], + "desc": "this simple value is unassigned by the spec and must be invalid" + }, + { + "type": "invalid_out", + "data": "e0", + "name": "unassigned simple value", + "tags": ["basic"], + "desc": "this simple value is unassigned by the spec and must be invalid", + "id": "unassigned_invalid_out" + }, + { + "type": "roundtrip", + "data": "f6", + "name": "simple value 'null'", + "tags": ["basic"], + "desc": "the null value should be supported by all implementations" + } +] diff --git a/test/fixtures/cbor/tags.json b/test/fixtures/cbor/tags.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/tags.json @@ -0,0 +1,24 @@ +[ + { + "type": "invalid_in", + "data": "d8207368747470733a2f2f6578616d706c652e636f6d", + "name": "tag that isn't 42", + "tags": ["dag-cbor", "c42"], + "desc": "all tags other than 42 (CID) are banned by dag-cbor" + }, + { + "type": "invalid_out", + "data": "c07819323032352d30352d32365431363a31383a31372d30343a3030", + "name": "tagged object (datetime)", + "tags": ["dag-cbor", "c42"], + "desc": "an object that would be encoded as a CBOR tag (a datetime in this case) should be rejected by dag-cbor encoders", + "id": "datetime_invalid_out" + }, + { + "type": "invalid_in", + "data": "d9d9f700", + "name": "self-describing CBOR", + "tags": ["dag-cbor", "c42"], + "desc": "the self-describing CBOR tag, 55799, should also be forbidden" + } +] diff --git a/test/fixtures/cbor/utf8.json b/test/fixtures/cbor/utf8.json new file mode 100644 --- /dev/null +++ b/test/fixtures/cbor/utf8.json @@ -0,0 +1,37 @@ +[ + { + "type": "roundtrip", + "data": "7873d8a7d984d986d8b561f09f94a5f09f918bf09f8fbc5acda7cc91cc93cca4cd9461cc88cc88cc87cd96ccad6ccdaecc92cdab67cc8ccc9acc97cd9a6fcc94cdaecc87cd90cc87cc99f09fa79fe2808de29980efb88fe2808d20f09f8fb3efb88fe2808de29aa7efb88fe794b0e2808b20e2808b", + "name": "complex UTF-8 string", + "tags": ["basic"], + "desc": "this string contains many different kinds of Unicode characters (English, Arabic, Chinese, emoji, etc) to ensure that implementations don't alter data or struggle with this" + }, + { + "type": "roundtrip", + "data": "630d0a09", + "name": "commonly escaped characters", + "tags": ["basic"], + "desc": "commonly escaped characters like newlines or tabs are not escaped in CBOR" + }, + { + "type": "invalid_in", + "data": "62c328", + "name": "invalid UTF-8", + "tags": ["basic"], + "desc": "CBOR strings are required to be valid UTF-8, this invalid string tests whether libraries check for this" + }, + { + "type": "invalid_in", + "data": "6365cc81", + "name": "text not in Unicode Normalization Form C", + "tags": ["dCBOR"], + "desc": "dCBOR requires strings be normalized into NFC form" + }, + { + "type": "roundtrip", + "data": "6365cc81", + "name": "text not in Unicode Normalization Form C", + "tags": ["dag-cbor", "CDE", "CBOR-Core", "c42"], + "desc": "all specs except dCBOR don't mention Unicode normalization, so this test ensures that libraries don't alter strings for the purpose of normalization" + } +]