From 45a0b3b55fb602e4230c5ac28db4c6b2fdfb3532 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 12 May 2026 15:21:31 +1000 Subject: [PATCH 1/6] docs: add JSON Schema for EQL v2.2 payload format Captures the current on-the-wire payload as the baseline for v2.3 schema work. Two mutually exclusive top-level forms (`EncryptedPayload` for scalars, `SteVecPayload` for jsonb / containment), discriminated by `k`. Index-term fields and STE-vector element shape are factored into shared $defs. --- .../schema/eql-payload-v2.2.schema.json | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 docs/reference/schema/eql-payload-v2.2.schema.json diff --git a/docs/reference/schema/eql-payload-v2.2.schema.json b/docs/reference/schema/eql-payload-v2.2.schema.json new file mode 100644 index 00000000..9908ce31 --- /dev/null +++ b/docs/reference/schema/eql-payload-v2.2.schema.json @@ -0,0 +1,186 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cipherstash.com/schemas/eql/v2.2/payload.schema.json", + "title": "EQL v2.2 payload", + "description": "JSON Schema describing the on-the-wire EQL payload format as of release v2.2 — the baseline before v2.3 schema changes. Two top-level shapes are accepted, mutually exclusive: an `EncryptedPayload` (a single encrypted scalar value, with any combination of index terms) and an `SteVecPayload` (an STE vector for jsonb / containment queries). The encrypted root form is what `eql_v2.check_encrypted` validates and what `eql_v2_encrypted` columns hold. STE-vector elements (inside `sv`) follow a distinct sub-shape and never carry `hm` or a top-level `i`.", + + "oneOf": [ + { "$ref": "#/$defs/EncryptedPayload" }, + { "$ref": "#/$defs/SteVecPayload" } + ], + + "$defs": { + + "Version": { + "title": "Payload version (v)", + "description": "Configuration version that produced the payload. Hard-coded to 2 in EQL v2; `eql_v2._encrypted_check_v` raises if any other value is present.", + "type": "integer", + "const": 2 + }, + + "Kind": { + "title": "Payload kind (k)", + "description": "Disambiguates payload shape. `ct` = ciphertext scalar (the EncryptedPayload form), `sv` = STE vector (the SteVecPayload form). Not currently enforced by `eql_v2.check_encrypted`, but always present in payloads emitted by Proxy / Protect.js and used as the discriminator between the two top-level variants.", + "type": "string", + "enum": ["ct", "sv"] + }, + + "Ident": { + "title": "Column ident (i)", + "description": "Identifies the table and column the payload belongs to. Required on every encrypted payload; both `t` and `c` must be present (enforced by `eql_v2._encrypted_check_i_ct`).", + "type": "object", + "properties": { + "t": { "type": "string", "description": "Table name." }, + "c": { "type": "string", "description": "Column name." } + }, + "required": ["t", "c"], + "additionalProperties": false + }, + + "Ciphertext": { + "title": "Ciphertext (c)", + "description": "Opaque, non-deterministic encrypted blob produced by Proxy. Required on every encrypted payload (enforced by `eql_v2._encrypted_check_c`). Excluded from GIN containment comparison via `eql_v2.jsonb_array`, which keeps only deterministic index-term fields.", + "type": "string", + "minLength": 1 + }, + + "EncryptedPayload": { + "title": "Encrypted payload (scalar, database side)", + "description": "Shape stored in `eql_v2_encrypted` columns for a single encrypted scalar value. `v`, `c`, and `i.{t,c}` are required by `eql_v2.check_encrypted`. Any subset of the index-term fields may be present, depending on the configured indexes for the column. Mutually exclusive with `SteVecPayload`: must not carry `sv`.", + "type": "object", + "properties": { + "v": { "$ref": "#/$defs/Version" }, + "k": { "type": "string", "const": "ct", "description": "Discriminator: identifies this as the scalar ciphertext form." }, + "c": { "$ref": "#/$defs/Ciphertext" }, + "i": { "$ref": "#/$defs/Ident" }, + + "hm": { "$ref": "#/$defs/IndexTerms/properties/hm" }, + "b3": { "$ref": "#/$defs/IndexTerms/properties/b3" }, + "bf": { "$ref": "#/$defs/IndexTerms/properties/bf" }, + "ob": { "$ref": "#/$defs/IndexTerms/properties/ob" }, + "ocf": { "$ref": "#/$defs/IndexTerms/properties/ocf" }, + "ocv": { "$ref": "#/$defs/IndexTerms/properties/ocv" }, + "opf": { "$ref": "#/$defs/IndexTerms/properties/opf" }, + "opv": { "$ref": "#/$defs/IndexTerms/properties/opv" } + }, + "required": ["v", "c", "i"], + "not": { "required": ["sv"] }, + "additionalProperties": false + }, + + "SteVecPayload": { + "title": "STE vector payload (database side)", + "description": "Shape stored in `eql_v2_encrypted` columns for jsonb / structured values. Carries only the discriminator metadata (`v`, `k`, `i`) and the STE vector array (`sv`). Mutually exclusive with `EncryptedPayload`: must not carry a top-level `c` or any of the scalar index terms — those live on individual `sv` elements instead.", + "type": "object", + "properties": { + "v": { "$ref": "#/$defs/Version" }, + "k": { "type": "string", "const": "sv", "description": "Discriminator: identifies this as the STE vector form." }, + "i": { "$ref": "#/$defs/Ident" }, + "sv": { + "type": "array", + "description": "STE vector: array of per-selector encrypted terms used for jsonb containment (`@>`, `<@`) and path queries.", + "items": { "$ref": "#/$defs/SteVecElement" } + } + }, + "required": ["v", "k", "i", "sv"], + "additionalProperties": false + }, + + "SteVecElement": { + "title": "STE vector element", + "description": "One entry inside `sv`. Distinct from the root payload: carries a per-element selector `s` and an array flag `a`, but never an `i`, `v`, or `hm`. Equality on `sv` elements is matched by `s` plus `b3` (see `eql_v2.ste_vec_contains`).", + "type": "object", + "properties": { + "s": { + "type": "string", + "description": "Selector — deterministic per (path, key) within the document. Required: equality matching of STE vector elements compares selectors first." + }, + "a": { + "type": "boolean", + "description": "Array marker. True when the selector points at a JSON array context." + }, + "c": { "$ref": "#/$defs/Ciphertext" }, + "b3": { "$ref": "#/$defs/IndexTerms/properties/b3" }, + "ocf": { "$ref": "#/$defs/IndexTerms/properties/ocf" }, + "ocv": { "$ref": "#/$defs/IndexTerms/properties/ocv" }, + "opf": { "$ref": "#/$defs/IndexTerms/properties/opf" }, + "opv": { "$ref": "#/$defs/IndexTerms/properties/opv" }, + "ob": { "$ref": "#/$defs/IndexTerms/properties/ob" } + }, + "required": ["s", "c"], + "not": { + "anyOf": [ + { "required": ["hm"] }, + { "required": ["i"] }, + { "required": ["v"] }, + { "required": ["sv"] } + ] + }, + "additionalProperties": false + }, + + "IndexTerms": { + "title": "Index term field catalogue", + "description": "Catalogue of every deterministic / order-preserving index term currently emitted by Proxy. Defined here so both the root payload and STE vector elements can $ref individual entries without duplicating descriptions. Field names match the strings hard-coded in `eql_v2.jsonb_array` for GIN containment.", + "type": "object", + "properties": { + + "hm": { + "title": "HMAC-SHA-256 (hm)", + "description": "Deterministic hex-encoded HMAC used for `=` / `<>` equality. Top-level only — never appears in STE vector elements (see `ste_vec_contains` notes). Required for the bare-form equality operator path post #193.", + "type": "string", + "pattern": "^[0-9a-f]+$" + }, + + "b3": { + "title": "Blake3 (b3)", + "description": "Deterministic hex-encoded Blake3 digest. Used inside STE vector elements as the equality term (selector + b3 = match). Also the index term for `eql_v2.compare_blake3`.", + "type": "string", + "pattern": "^[0-9a-f]+$" + }, + + "bf": { + "title": "Bloom filter (bf)", + "description": "Bloom filter representation as an array of set bit positions. Used by `LIKE` / `ILIKE` (`~~`, `~~*`) via `eql_v2.bloom_filter` and the corresponding GIN index.", + "type": "array", + "items": { "type": "integer", "minimum": 0 } + }, + + "ob": { + "title": "ORE block u64_8_256 (ob)", + "description": "Order-Revealing Encryption term used for ordered comparisons (`<`, `<=`, `>`, `>=`) via the custom `eql_v2.ore_block_u64_8_256` comparator. Stored as an array of hex-encoded ORE blocks.", + "type": "array", + "items": { "type": "string", "pattern": "^[0-9a-f]+$" } + }, + + "ocf": { + "title": "ORE CLLW u64_8 fixed (ocf)", + "description": "CLLW Order-Revealing Encryption, fixed-width (numeric). Hex-encoded.", + "type": "string", + "pattern": "^[0-9a-f]+$" + }, + + "ocv": { + "title": "ORE CLLW var_8 (ocv)", + "description": "CLLW Order-Revealing Encryption, variable-width (text). Hex-encoded.", + "type": "string", + "pattern": "^[0-9a-f]+$" + }, + + "opf": { + "title": "OPE CLLW u64_65 fixed (opf)", + "description": "CLLW Order-Preserving Encryption, fixed-width (numeric). Sortable with native bytea ordering — used in environments without custom comparators.", + "type": "string", + "pattern": "^[0-9a-f]+$" + }, + + "opv": { + "title": "OPE CLLW var_8 (opv)", + "description": "CLLW Order-Preserving Encryption, variable-width (text). Sortable with native bytea ordering.", + "type": "string", + "pattern": "^[0-9a-f]+$" + } + } + } + } +} From 84ce15e5c1e2d781c6d338291a8e1f57688e033f Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 12 May 2026 15:44:25 +1000 Subject: [PATCH 2/6] docs: add JSON Schema for EQL v2.3 payload format Captures the target shape for v2.3 alongside the v2.2 baseline. Three breaking changes from v2.2: `b3` is removed (root and `sv` element); `sv` elements now carry `hm`; `opf` and `opv` collapse into a single `op` field; OPE (`op`) and ORE (`ob`/`ocf`/`ocv`) are mutually exclusive within a payload or element, encoded as a shared `OreOpeExclusion` $def. --- CHANGELOG.md | 4 + .../schema/eql-payload-v2.3.schema.json | 193 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 docs/reference/schema/eql-payload-v2.3.schema.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d5c8c4..b46f6038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ Each entry that ships in a published release links to the PR that introduced it. Targeting `2.3.0`. See [`docs/upgrading/v2.3.md`](docs/upgrading/v2.3.md) for the consolidated upgrade notes. +### Added + +- **Formal JSON Schema for the EQL payload format.** Two files under `docs/reference/schema/`: `eql-payload-v2.2.schema.json` captures the on-the-wire shape as of release 2.2 (baseline), and `eql-payload-v2.3.schema.json` captures the target shape for 2.3. The 2.3 schema makes three breaking changes to the payload format: the root-level `b3` term is gone (already reflected in the equality / hashing entries below) and is also dropped from `sv` elements, which now carry `hm` instead; `opf` (fixed-width OPE) and `opv` (variable-width OPE) collapse into a single `op` field with the width carried in the value; OPE (`op`) and ORE (`ob`, `ocf`, `ocv`) are now mutually exclusive within a single payload or `sv` element. Both files use JSON Schema 2020-12 and share a single `IndexTerms` catalogue between root and `sv`-element shapes so that field semantics are described in exactly one place. + ### Changed - **`=`, `<>`, `~~` (`LIKE`), `~~*` (`ILIKE`) on `eql_v2_encrypted` are now inlinable SQL functions.** The planner can structurally match these operators against the documented functional indexes (`eql_v2.hmac_256(col)` for equality, `eql_v2.bloom_filter(col)` for `LIKE`/`ILIKE`), so bare-form queries (`WHERE col = $1`) engage the index without per-query rewriting. Previously these operators wrapped multi-branch PL/pgSQL bodies that the planner could not inline, forcing seq scans on Supabase / managed Postgres installations that lack operator-class indexes. ([#193](https://github.com/cipherstash/encrypt-query-language/pull/193), [#196](https://github.com/cipherstash/encrypt-query-language/pull/196)) diff --git a/docs/reference/schema/eql-payload-v2.3.schema.json b/docs/reference/schema/eql-payload-v2.3.schema.json new file mode 100644 index 00000000..ac734e9b --- /dev/null +++ b/docs/reference/schema/eql-payload-v2.3.schema.json @@ -0,0 +1,193 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cipherstash.com/schemas/eql/v2.3/payload.schema.json", + "title": "EQL v2.3 payload", + "description": "JSON Schema describing the on-the-wire EQL payload format as of release v2.3. Two top-level shapes are accepted, mutually exclusive: an `EncryptedPayload` (a single encrypted scalar value, with any combination of index terms) and an `SteVecPayload` (an STE vector for jsonb / containment queries). The encrypted root form is what `eql_v2.check_encrypted` validates and what `eql_v2_encrypted` columns hold.\n\nChanges since v2.2:\n- The `b3` (Blake3) index term is removed everywhere. STE vector element equality now uses `hm` directly.\n- `SteVecElement` may now carry an `hm` field (matches the root `EncryptedPayload`); the previous prohibition is lifted.\n- `opf` and `opv` are collapsed into a single `op` field. Width information that was previously implied by the field name is now carried on the value itself.\n- OPE (`op`) and ORE (`ob`, `ocf`, `ocv`) are now mutually exclusive within a single payload or `sv` element: a column is configured for one ordered scheme or the other, never both.", + + "oneOf": [ + { "$ref": "#/$defs/EncryptedPayload" }, + { "$ref": "#/$defs/SteVecPayload" } + ], + + "$defs": { + + "Version": { + "title": "Payload version (v)", + "description": "Configuration version that produced the payload. Hard-coded to 2 in EQL v2; `eql_v2._encrypted_check_v` raises if any other value is present.", + "type": "integer", + "const": 2 + }, + + "Kind": { + "title": "Payload kind (k)", + "description": "Disambiguates payload shape. `ct` = ciphertext scalar (the EncryptedPayload form), `sv` = STE vector (the SteVecPayload form). Used as the discriminator between the two top-level variants.", + "type": "string", + "enum": ["ct", "sv"] + }, + + "Ident": { + "title": "Column ident (i)", + "description": "Identifies the table and column the payload belongs to. Required on every encrypted payload; both `t` and `c` must be present (enforced by `eql_v2._encrypted_check_i_ct`).", + "type": "object", + "properties": { + "t": { "type": "string", "description": "Table name." }, + "c": { "type": "string", "description": "Column name." } + }, + "required": ["t", "c"], + "additionalProperties": false + }, + + "Ciphertext": { + "title": "Ciphertext (c)", + "description": "Opaque, non-deterministic encrypted blob produced by Proxy. Required on every encrypted payload (enforced by `eql_v2._encrypted_check_c`). Excluded from GIN containment comparison via `eql_v2.jsonb_array`, which keeps only deterministic index-term fields.", + "type": "string", + "minLength": 1 + }, + + "EncryptedPayload": { + "title": "Encrypted payload (scalar, database side)", + "description": "Shape stored in `eql_v2_encrypted` columns for a single encrypted scalar value. `v`, `c`, and `i.{t,c}` are required by `eql_v2.check_encrypted`. Any subset of the index-term fields may be present, depending on the configured indexes for the column. Mutually exclusive with `SteVecPayload`: must not carry `sv`. The OPE term (`op`) and the ORE terms (`ob`, `ocf`, `ocv`) are also mutually exclusive — see `OreOpeExclusion`.", + "type": "object", + "properties": { + "v": { "$ref": "#/$defs/Version" }, + "k": { "type": "string", "const": "ct", "description": "Discriminator: identifies this as the scalar ciphertext form." }, + "c": { "$ref": "#/$defs/Ciphertext" }, + "i": { "$ref": "#/$defs/Ident" }, + + "hm": { "$ref": "#/$defs/IndexTerms/properties/hm" }, + "bf": { "$ref": "#/$defs/IndexTerms/properties/bf" }, + "ob": { "$ref": "#/$defs/IndexTerms/properties/ob" }, + "ocf": { "$ref": "#/$defs/IndexTerms/properties/ocf" }, + "ocv": { "$ref": "#/$defs/IndexTerms/properties/ocv" }, + "op": { "$ref": "#/$defs/IndexTerms/properties/op" } + }, + "required": ["v", "c", "i"], + "allOf": [ + { "not": { "required": ["sv"] } }, + { "$ref": "#/$defs/OreOpeExclusion" } + ], + "additionalProperties": false + }, + + "SteVecPayload": { + "title": "STE vector payload (database side)", + "description": "Shape stored in `eql_v2_encrypted` columns for jsonb / structured values. Carries only the discriminator metadata (`v`, `k`, `i`) and the STE vector array (`sv`). Mutually exclusive with `EncryptedPayload`: must not carry a top-level `c` or any of the scalar index terms — those live on individual `sv` elements instead.", + "type": "object", + "properties": { + "v": { "$ref": "#/$defs/Version" }, + "k": { "type": "string", "const": "sv", "description": "Discriminator: identifies this as the STE vector form." }, + "i": { "$ref": "#/$defs/Ident" }, + "sv": { + "type": "array", + "description": "STE vector: array of per-selector encrypted terms used for jsonb containment (`@>`, `<@`) and path queries.", + "items": { "$ref": "#/$defs/SteVecElement" } + } + }, + "required": ["v", "k", "i", "sv"], + "additionalProperties": false + }, + + "SteVecElement": { + "title": "STE vector element", + "description": "One entry inside `sv`. Distinct from the root payload: carries a per-element selector `s` and an array flag `a`, but never an `i`, `v`, or nested `sv`. As of v2.3 element equality is matched by `s` plus `hm` (Blake3 / `b3` is removed); `hm` is now permitted at this level. The OPE term (`op`) and the ORE terms (`ob`, `ocf`, `ocv`) are mutually exclusive within an element — see `OreOpeExclusion`.", + "type": "object", + "properties": { + "s": { + "type": "string", + "description": "Selector — deterministic per (path, key) within the document. Required: equality matching of STE vector elements compares selectors first." + }, + "a": { + "type": "boolean", + "description": "Array marker. True when the selector points at a JSON array context." + }, + "c": { "$ref": "#/$defs/Ciphertext" }, + "hm": { "$ref": "#/$defs/IndexTerms/properties/hm" }, + "ocf": { "$ref": "#/$defs/IndexTerms/properties/ocf" }, + "ocv": { "$ref": "#/$defs/IndexTerms/properties/ocv" }, + "op": { "$ref": "#/$defs/IndexTerms/properties/op" }, + "ob": { "$ref": "#/$defs/IndexTerms/properties/ob" } + }, + "required": ["s", "c"], + "allOf": [ + { + "not": { + "anyOf": [ + { "required": ["i"] }, + { "required": ["v"] }, + { "required": ["sv"] } + ] + } + }, + { "$ref": "#/$defs/OreOpeExclusion" } + ], + "additionalProperties": false + }, + + "OreOpeExclusion": { + "title": "OPE / ORE mutual exclusion", + "description": "A payload (or `sv` element) configured for ordered comparison must use OPE (`op`) or ORE (`ob` / `ocf` / `ocv`), never both. Encoded as `not (op AND any-ORE)`.", + "not": { + "allOf": [ + { "required": ["op"] }, + { + "anyOf": [ + { "required": ["ob"] }, + { "required": ["ocf"] }, + { "required": ["ocv"] } + ] + } + ] + } + }, + + "IndexTerms": { + "title": "Index term field catalogue", + "description": "Catalogue of every deterministic / order-preserving index term emitted by Proxy in v2.3. Defined here so both the root payload and STE vector elements can $ref individual entries without duplicating descriptions.", + "type": "object", + "properties": { + + "hm": { + "title": "HMAC-SHA-256 (hm)", + "description": "Deterministic hex-encoded HMAC used for `=` / `<>` equality. As of v2.3 also used for STE vector element equality (selector + hm = match), replacing the previous `b3` term.", + "type": "string", + "pattern": "^[0-9a-f]+$" + }, + + "bf": { + "title": "Bloom filter (bf)", + "description": "Bloom filter representation as an array of set bit positions. Used by `LIKE` / `ILIKE` (`~~`, `~~*`) via `eql_v2.bloom_filter` and the corresponding GIN index.", + "type": "array", + "items": { "type": "integer", "minimum": 0 } + }, + + "ob": { + "title": "ORE block u64_8_256 (ob)", + "description": "Order-Revealing Encryption term used for ordered comparisons (`<`, `<=`, `>`, `>=`) via the custom `eql_v2.ore_block_u64_8_256` comparator. Stored as an array of hex-encoded ORE blocks. Mutually exclusive with `op` (OPE).", + "type": "array", + "items": { "type": "string", "pattern": "^[0-9a-f]+$" } + }, + + "ocf": { + "title": "ORE CLLW u64_8 fixed (ocf)", + "description": "CLLW Order-Revealing Encryption, fixed-width (numeric). Hex-encoded. Mutually exclusive with `op` (OPE).", + "type": "string", + "pattern": "^[0-9a-f]+$" + }, + + "ocv": { + "title": "ORE CLLW var_8 (ocv)", + "description": "CLLW Order-Revealing Encryption, variable-width (text). Hex-encoded. Mutually exclusive with `op` (OPE).", + "type": "string", + "pattern": "^[0-9a-f]+$" + }, + + "op": { + "title": "OPE CLLW (op)", + "description": "CLLW Order-Preserving Encryption term, hex-encoded. Sortable with native bytea ordering — used in environments without custom comparators. As of v2.3 a single field replaces the previous `opf` (fixed-width / numeric) and `opv` (variable-width / text) split; the value itself encodes any width information the comparator needs. Mutually exclusive with the ORE terms (`ob`, `ocf`, `ocv`).", + "type": "string", + "pattern": "^[0-9a-f]+$" + } + } + } + } +} From 55d4144c24233b39c068c7e63872f84f90678cd9 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 12 May 2026 15:48:50 +1000 Subject: [PATCH 3/6] test: add JSON Schema validation tests for v2.2 / v2.3 payload format Adds `tests/sqlx/tests/payload_schema_tests.rs` (20 tests, no database required) that load both schema files and assert against a battery of hand-crafted positive and negative payloads. Coverage: - v2.2 baseline: minimal / fully-populated EncryptedPayload, SteVecPayload with mixed-index elements, root-only fields rejected on `sv` elements, EncryptedPayload <-> SteVecPayload mutual exclusion, additionalProperties. - v2.3 target: `b3` rejected everywhere, legacy `opf`/`opv` rejected, OPE (`op`) and ORE (`ob`/`ocf`/`ocv`) mutually exclusive at both root and element level, `hm` now permitted on `sv` elements, required-field enforcement. Tests live in the existing sqlx test crate as a database-free integration target; jsonschema 0.17 added as a non-dev dependency to keep the dep tree flat. --- tests/sqlx/Cargo.lock | 369 +++++++++++++++++++++- tests/sqlx/Cargo.toml | 1 + tests/sqlx/tests/payload_schema_tests.rs | 382 +++++++++++++++++++++++ 3 files changed, 748 insertions(+), 4 deletions(-) create mode 100644 tests/sqlx/tests/payload_schema_tests.rs diff --git a/tests/sqlx/Cargo.lock b/tests/sqlx/Cargo.lock index 8adf15f5..78b0b6a7 100644 --- a/tests/sqlx/Cargo.lock +++ b/tests/sqlx/Cargo.lock @@ -2,6 +2,29 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -29,6 +52,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -41,6 +70,21 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "2.10.0" @@ -59,6 +103,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "byteorder" version = "1.5.0" @@ -152,6 +208,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -196,6 +261,7 @@ version = "0.1.0" dependencies = [ "anyhow", "hex", + "jsonschema", "serde", "serde_json", "sqlx", @@ -230,6 +296,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "flume" version = "0.11.1" @@ -256,6 +332,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -345,8 +431,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", ] [[package]] @@ -531,12 +631,61 @@ dependencies = [ "hashbrown 0.16.0", ] +[[package]] +name = "iso8601" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" +dependencies = [ + "nom", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonschema" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" +dependencies = [ + "ahash", + "anyhow", + "base64 0.21.7", + "bytecount", + "fancy-regex", + "fraction", + "getrandom 0.2.16", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "serde", + "serde_json", + "time", + "url", + "uuid", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -627,6 +776,39 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -643,6 +825,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-integer" version = "0.1.46" @@ -663,6 +866,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -771,6 +985,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -798,6 +1018,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.6" @@ -825,7 +1051,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] @@ -837,6 +1063,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rsa" version = "0.9.10" @@ -857,6 +1112,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.20" @@ -1028,7 +1289,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "crc", "crossbeam-queue", @@ -1101,7 +1362,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -1143,7 +1404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "crc", @@ -1262,6 +1523,36 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -1409,6 +1700,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -1427,12 +1728,66 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + [[package]] name = "whoami" version = "1.6.1" @@ -1671,6 +2026,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.1" diff --git a/tests/sqlx/Cargo.toml b/tests/sqlx/Cargo.toml index f2681e9c..c419ba8d 100644 --- a/tests/sqlx/Cargo.toml +++ b/tests/sqlx/Cargo.toml @@ -10,6 +10,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" anyhow = "1" hex = "0.4" +jsonschema = { version = "0.17", default-features = false } [dev-dependencies] # None needed - tests live in this crate diff --git a/tests/sqlx/tests/payload_schema_tests.rs b/tests/sqlx/tests/payload_schema_tests.rs new file mode 100644 index 00000000..b71521a1 --- /dev/null +++ b/tests/sqlx/tests/payload_schema_tests.rs @@ -0,0 +1,382 @@ +//! JSON Schema validation tests for the EQL v2.2 baseline and v2.3 target +//! payload formats. These do not touch the database. +//! +//! Goals: +//! - Lock the on-the-wire payload contracts as code so format drift is caught +//! in CI rather than discovered at integration time. +//! - Document the v2.2 -> v2.3 delta as executable assertions: payloads that +//! are valid in 2.2 (e.g. `b3` everywhere, `opf`/`opv` split) must fail +//! under 2.3, and vice versa. + +use std::path::PathBuf; +use std::sync::OnceLock; + +use jsonschema::JSONSchema; +use serde_json::{json, Value}; + +// ---------- helpers ---------- + +fn load_schema(filename: &str) -> Value { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../docs/reference/schema") + .join(filename); + let text = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e)); + serde_json::from_str(&text).unwrap_or_else(|e| panic!("schema is not valid JSON: {e}")) +} + +fn compile(schema: &Value) -> JSONSchema { + // Schema files declare `$schema: draft 2020-12`. The 0.17 jsonschema crate + // doesn't have an explicit Draft 2020-12 variant; let it fall back to its + // default (Draft 7). The constructs we use ($defs, $ref, oneOf, allOf, + // not, const, additionalProperties, pattern, etc.) are all supported. + JSONSchema::compile(schema).expect("schema fails to compile") +} + +fn schema_v2_2() -> &'static JSONSchema { + static S: OnceLock = OnceLock::new(); + S.get_or_init(|| compile(&load_schema("eql-payload-v2.2.schema.json"))) +} + +fn schema_v2_3() -> &'static JSONSchema { + static S: OnceLock = OnceLock::new(); + S.get_or_init(|| compile(&load_schema("eql-payload-v2.3.schema.json"))) +} + +#[track_caller] +fn assert_valid(schema: &JSONSchema, instance: &Value, label: &str) { + if let Err(errors) = schema.validate(instance) { + let msgs: Vec = errors + .map(|e| format!(" - {} (at {})", e, e.instance_path)) + .collect(); + panic!( + "expected `{label}` to validate, but got:\n{}\ninstance:\n{}", + msgs.join("\n"), + serde_json::to_string_pretty(instance).unwrap() + ); + } +} + +#[track_caller] +fn assert_invalid(schema: &JSONSchema, instance: &Value, label: &str) { + if schema.is_valid(instance) { + panic!( + "expected `{label}` to fail validation, but it passed:\n{}", + serde_json::to_string_pretty(instance).unwrap() + ); + } +} + +const CIPHERTEXT: &str = "mBbL@V^%dN?0W$;g)1-JP*cmqX%JhW0ZKZ^G?lNn$CfXJH"; +const HEX: &str = "8067db44a848ab32c3056a3dbe4edf16"; +const HEX_LONG: &str = "fbc7a11fc81f2a321553bc06a91f240bb7d8f3a9c6aec445a5ba6793"; +const SELECTOR: &str = "9493d6010fe7845d52149b697729c745"; + +fn ident() -> Value { + json!({ "t": "users", "c": "email" }) +} + +// =========================================================================== +// v2.2 schema +// =========================================================================== + +#[test] +fn v2_2_minimal_encrypted_payload_is_valid() { + let p = json!({ "v": 2, "c": CIPHERTEXT, "i": ident() }); + assert_valid(schema_v2_2(), &p, "minimal encrypted payload"); +} + +#[test] +fn v2_2_full_encrypted_payload_with_all_index_terms_is_valid() { + // v2.2 still has b3, opf and opv as separate fields. + let p = json!({ + "v": 2, + "k": "ct", + "c": CIPHERTEXT, + "i": ident(), + "hm": HEX, + "b3": HEX, + "bf": [12, 47, 91, 188], + "ob": [HEX, HEX_LONG], + "ocf": HEX, + "ocv": HEX_LONG, + "opf": HEX, + "opv": HEX_LONG + }); + assert_valid(schema_v2_2(), &p, "fully populated encrypted payload"); +} + +#[test] +fn v2_2_ste_vec_payload_is_valid() { + let p = json!({ + "v": 2, + "k": "sv", + "i": ident(), + "sv": [ + { "s": SELECTOR, "a": false, "c": CIPHERTEXT, "b3": HEX }, + { "s": SELECTOR, "a": false, "c": CIPHERTEXT, "ocv": HEX_LONG }, + { "s": SELECTOR, "a": true, "c": CIPHERTEXT, "ocf": HEX, "opv": HEX } + ] + }); + assert_valid(schema_v2_2(), &p, "ste_vec payload"); +} + +#[test] +fn v2_2_encrypted_missing_required_fields_fails() { + let cases = [ + ("missing v", json!({ "c": CIPHERTEXT, "i": ident() })), + ("missing c", json!({ "v": 2, "i": ident() })), + ("missing i", json!({ "v": 2, "c": CIPHERTEXT })), + ( + "ident missing t", + json!({ "v": 2, "c": CIPHERTEXT, "i": { "c": "email" } }), + ), + ( + "ident missing c", + json!({ "v": 2, "c": CIPHERTEXT, "i": { "t": "users" } }), + ), + ]; + for (label, p) in cases { + assert_invalid(schema_v2_2(), &p, label); + } +} + +#[test] +fn v2_2_encrypted_with_wrong_version_fails() { + let p = json!({ "v": 1, "c": CIPHERTEXT, "i": ident() }); + assert_invalid(schema_v2_2(), &p, "wrong version"); +} + +#[test] +fn v2_2_encrypted_payload_with_sv_fails() { + // EncryptedPayload is mutually exclusive with SteVecPayload. + let p = json!({ + "v": 2, "k": "ct", "c": CIPHERTEXT, "i": ident(), + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "b3": HEX }] + }); + assert_invalid(schema_v2_2(), &p, "encrypted payload carrying sv"); +} + +#[test] +fn v2_2_ste_vec_payload_with_top_level_ciphertext_fails() { + // SteVecPayload is metadata + sv only — no top-level c. + let p = json!({ + "v": 2, "k": "sv", "i": ident(), + "c": CIPHERTEXT, + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "b3": HEX }] + }); + assert_invalid(schema_v2_2(), &p, "ste_vec payload with top-level c"); +} + +#[test] +fn v2_2_ste_vec_element_with_hm_fails() { + // v2.2 forbids hm at the sv-element level (root-only term). + let p = json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "hm": HEX }] + }); + assert_invalid(schema_v2_2(), &p, "sv element carrying hm"); +} + +#[test] +fn v2_2_ste_vec_element_with_root_only_fields_fails() { + let cases = [ + ( + "element with i", + json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "b3": HEX, "i": ident() }] + }), + ), + ( + "element with v", + json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "b3": HEX, "v": 2 }] + }), + ), + ( + "element with nested sv", + json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ + "s": SELECTOR, "c": CIPHERTEXT, "b3": HEX, + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "b3": HEX }] + }] + }), + ), + ]; + for (label, p) in cases { + assert_invalid(schema_v2_2(), &p, label); + } +} + +#[test] +fn v2_2_unknown_top_level_field_fails() { + // additionalProperties: false at the root. + let p = json!({ + "v": 2, "c": CIPHERTEXT, "i": ident(), + "x": "not a known field" + }); + assert_invalid(schema_v2_2(), &p, "unknown top-level field"); +} + +// =========================================================================== +// v2.3 schema +// =========================================================================== + +#[test] +fn v2_3_minimal_encrypted_payload_is_valid() { + let p = json!({ "v": 2, "c": CIPHERTEXT, "i": ident() }); + assert_valid(schema_v2_3(), &p, "minimal encrypted payload"); +} + +#[test] +fn v2_3_encrypted_payload_with_op_only_is_valid() { + let p = json!({ + "v": 2, "k": "ct", "c": CIPHERTEXT, "i": ident(), + "hm": HEX, "bf": [1, 2, 3], "op": HEX + }); + assert_valid(schema_v2_3(), &p, "encrypted with OPE only"); +} + +#[test] +fn v2_3_encrypted_payload_with_ore_only_is_valid() { + let p = json!({ + "v": 2, "k": "ct", "c": CIPHERTEXT, "i": ident(), + "hm": HEX, "ob": [HEX, HEX_LONG], "ocf": HEX, "ocv": HEX_LONG + }); + assert_valid(schema_v2_3(), &p, "encrypted with ORE only"); +} + +#[test] +fn v2_3_ste_vec_payload_with_hm_in_elements_is_valid() { + // v2.3 promotes element equality from b3 -> hm. + let p = json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [ + { "s": SELECTOR, "a": false, "c": CIPHERTEXT, "hm": HEX }, + { "s": SELECTOR, "a": true, "c": CIPHERTEXT, "hm": HEX, "op": HEX } + ] + }); + assert_valid(schema_v2_3(), &p, "ste_vec payload with hm-bearing elements"); +} + +#[test] +fn v2_3_b3_field_is_rejected_everywhere() { + let root = json!({ + "v": 2, "c": CIPHERTEXT, "i": ident(), + "b3": HEX + }); + assert_invalid(schema_v2_3(), &root, "encrypted payload carrying b3"); + + let element = json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "b3": HEX }] + }); + assert_invalid(schema_v2_3(), &element, "sv element carrying b3"); +} + +#[test] +fn v2_3_legacy_opf_and_opv_are_rejected() { + let with_opf = json!({ + "v": 2, "c": CIPHERTEXT, "i": ident(), "opf": HEX + }); + assert_invalid(schema_v2_3(), &with_opf, "encrypted payload with legacy opf"); + + let with_opv = json!({ + "v": 2, "c": CIPHERTEXT, "i": ident(), "opv": HEX_LONG + }); + assert_invalid(schema_v2_3(), &with_opv, "encrypted payload with legacy opv"); + + let element_with_opf = json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "hm": HEX, "opf": HEX }] + }); + assert_invalid(schema_v2_3(), &element_with_opf, "sv element with legacy opf"); +} + +#[test] +fn v2_3_ope_and_ore_are_mutually_exclusive_at_root() { + let cases = [ + ( + "op + ob", + json!({ + "v": 2, "c": CIPHERTEXT, "i": ident(), + "op": HEX, "ob": [HEX, HEX_LONG] + }), + ), + ( + "op + ocf", + json!({ + "v": 2, "c": CIPHERTEXT, "i": ident(), + "op": HEX, "ocf": HEX + }), + ), + ( + "op + ocv", + json!({ + "v": 2, "c": CIPHERTEXT, "i": ident(), + "op": HEX, "ocv": HEX_LONG + }), + ), + ]; + for (label, p) in cases { + assert_invalid(schema_v2_3(), &p, label); + } +} + +#[test] +fn v2_3_ope_and_ore_are_mutually_exclusive_in_sv_element() { + let p = json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ + "s": SELECTOR, "c": CIPHERTEXT, + "hm": HEX, "op": HEX, "ocv": HEX_LONG + }] + }); + assert_invalid(schema_v2_3(), &p, "sv element with both op and ocv"); +} + +#[test] +fn v2_3_encrypted_payload_with_sv_is_rejected() { + let p = json!({ + "v": 2, "k": "ct", "c": CIPHERTEXT, "i": ident(), + "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "hm": HEX }] + }); + assert_invalid(schema_v2_3(), &p, "encrypted payload with sv"); +} + +#[test] +fn v2_3_minimum_required_fields_enforced() { + let cases = [ + ("missing v", json!({ "c": CIPHERTEXT, "i": ident() })), + ("missing c (encrypted)", json!({ "v": 2, "i": ident() })), + ("missing i (encrypted)", json!({ "v": 2, "c": CIPHERTEXT })), + ( + "ste_vec missing sv", + json!({ "v": 2, "k": "sv", "i": ident() }), + ), + ( + "ste_vec missing k", + json!({ "v": 2, "i": ident(), "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "hm": HEX }] }), + ), + ( + "sv element missing s", + json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ "c": CIPHERTEXT, "hm": HEX }] + }), + ), + ( + "sv element missing c", + json!({ + "v": 2, "k": "sv", "i": ident(), + "sv": [{ "s": SELECTOR, "hm": HEX }] + }), + ), + ]; + for (label, p) in cases { + assert_invalid(schema_v2_3(), &p, label); + } +} From f48d122cd578be69c51566225a883e24df3de65a Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 12 May 2026 15:49:53 +1000 Subject: [PATCH 4/6] chore: add `mise run test:schema` task for payload schema validation No-op wrapper around `cargo test --test payload_schema_tests` in the sqlx test crate. Database-free, runs in <1s. Useful as a fast pre-flight when editing either schema file. --- mise.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mise.toml b/mise.toml index ff70e4ce..fbf499b4 100644 --- a/mise.toml +++ b/mise.toml @@ -55,3 +55,10 @@ dir = "{{config_root}}/tests/sqlx" run = """ cargo watch -x test """ + +[tasks."test:schema"] +description = "Validate sample payloads against the v2.2 / v2.3 JSON Schemas (no database required)" +dir = "{{config_root}}/tests/sqlx" +run = """ +cargo test --test payload_schema_tests +""" From de75d26e4558a903771ec7b17375e59695a0bed9 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 12 May 2026 16:00:41 +1000 Subject: [PATCH 5/6] ci: gate Postgres matrix on JSON Schema validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `schema` job to `test-eql.yml` that runs `mise run test:schema` on a vanilla ubuntu runner (no Postgres, no matrix). The `test` matrix job now `needs: schema`, so a broken JSON Schema fails the build in seconds instead of after the 4x Postgres matrix has spun up. `splinter` is left independent — schema correctness has no bearing on Supabase compatibility checks. --- .github/workflows/test-eql.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index 89594571..73e5ab37 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -30,9 +30,32 @@ defaults: shell: bash -l {0} jobs: + schema: + name: "JSON Schema validation" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: jdx/mise-action@v4 + with: + version: 2026.4.0 + install: true + cache: true + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: tests/sqlx + shared-key: sqlx-tests + + - name: Validate v2.2 / v2.3 payload schemas + run: | + mise run test:schema + test: name: "Test & Validate EQL (Postgres ${{ matrix.postgres-version }})" runs-on: ubuntu-latest-m + needs: schema strategy: fail-fast: false From beb64edc9a697946774213f28d89ef4ff8004189 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 12 May 2026 18:18:00 +1000 Subject: [PATCH 6/6] test: upgrade jsonschema to 0.46.4 and fix rustfmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the jsonschema crate from 0.17 to 0.46.4 (current stable). The API moved from `JSONSchema::compile` to `validator_for` / `Validator` and `instance_path` is now a method rather than a field — both updated. Side benefit: the new crate has native Draft 2020-12 support, so the schemas are now validated under their declared draft instead of the Draft 7 fallback. Also runs `cargo fmt` to fix the rustfmt failure on `payload_schema_tests.rs` that broke the `test:lint` step on all four Postgres matrix jobs. Appends the PR link to the CHANGELOG entry per repo convention. --- CHANGELOG.md | 2 +- tests/sqlx/Cargo.lock | 244 +++++++++++++---------- tests/sqlx/Cargo.toml | 2 +- tests/sqlx/tests/payload_schema_tests.rs | 56 ++++-- 4 files changed, 174 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b46f6038..63321693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Targeting `2.3.0`. See [`docs/upgrading/v2.3.md`](docs/upgrading/v2.3.md) for th ### Added -- **Formal JSON Schema for the EQL payload format.** Two files under `docs/reference/schema/`: `eql-payload-v2.2.schema.json` captures the on-the-wire shape as of release 2.2 (baseline), and `eql-payload-v2.3.schema.json` captures the target shape for 2.3. The 2.3 schema makes three breaking changes to the payload format: the root-level `b3` term is gone (already reflected in the equality / hashing entries below) and is also dropped from `sv` elements, which now carry `hm` instead; `opf` (fixed-width OPE) and `opv` (variable-width OPE) collapse into a single `op` field with the width carried in the value; OPE (`op`) and ORE (`ob`, `ocf`, `ocv`) are now mutually exclusive within a single payload or `sv` element. Both files use JSON Schema 2020-12 and share a single `IndexTerms` catalogue between root and `sv`-element shapes so that field semantics are described in exactly one place. +- **Formal JSON Schema for the EQL payload format.** Two files under `docs/reference/schema/`: `eql-payload-v2.2.schema.json` captures the on-the-wire shape as of release 2.2 (baseline), and `eql-payload-v2.3.schema.json` captures the target shape for 2.3. The 2.3 schema makes three breaking changes to the payload format: the root-level `b3` term is gone (already reflected in the equality / hashing entries below) and is also dropped from `sv` elements, which now carry `hm` instead; `opf` (fixed-width OPE) and `opv` (variable-width OPE) collapse into a single `op` field with the width carried in the value; OPE (`op`) and ORE (`ob`, `ocf`, `ocv`) are now mutually exclusive within a single payload or `sv` element. Both files use JSON Schema 2020-12 and share a single `IndexTerms` catalogue between root and `sv`-element shapes so that field semantics are described in exactly one place. ([#208](https://github.com/cipherstash/encrypt-query-language/pull/208)) ### Changed diff --git a/tests/sqlx/Cargo.lock b/tests/sqlx/Cargo.lock index 78b0b6a7..db86aeba 100644 --- a/tests/sqlx/Cargo.lock +++ b/tests/sqlx/Cargo.lock @@ -52,12 +52,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -72,18 +66,18 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" @@ -103,6 +97,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bumpalo" version = "3.20.2" @@ -197,6 +197,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + [[package]] name = "der" version = "0.7.10" @@ -208,15 +214,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - [[package]] name = "digest" version = "0.10.7" @@ -255,6 +252,15 @@ dependencies = [ "serde", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "eql_tests" version = "0.1.0" @@ -298,12 +304,24 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.11.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" dependencies = [ "bit-set", - "regex", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", ] [[package]] @@ -323,6 +341,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -334,9 +358,9 @@ dependencies = [ [[package]] name = "fraction" -version = "0.13.1" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" dependencies = [ "lazy_static", "num", @@ -431,10 +455,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -444,9 +466,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -457,7 +481,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -465,6 +489,11 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -631,15 +660,6 @@ dependencies = [ "hashbrown 0.16.0", ] -[[package]] -name = "iso8601" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" -dependencies = [ - "nom", -] - [[package]] name = "itoa" version = "1.0.15" @@ -653,37 +673,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ "cfg-if", - "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "jsonschema" -version = "0.17.1" +version = "0.46.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" +checksum = "fc59d2432e047d6090ba1d83c782d0128bd6203857978218f5614dbd3287281f" dependencies = [ "ahash", - "anyhow", - "base64 0.21.7", "bytecount", + "data-encoding", + "email_address", "fancy-regex", "fraction", - "getrandom 0.2.16", - "iso8601", + "getrandom 0.3.4", + "idna", "itoa", - "memchr", "num-cmp", - "once_cell", - "parking_lot", + "num-traits", "percent-encoding", + "referencing", "regex", + "regex-syntax", "serde", "serde_json", - "time", - "url", - "uuid", + "unicode-general-category", + "uuid-simd", ] [[package]] @@ -765,6 +783,12 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + [[package]] name = "mio" version = "1.1.0" @@ -776,15 +800,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "num" version = "0.4.3" @@ -840,12 +855,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - [[package]] name = "num-integer" version = "0.1.46" @@ -893,6 +902,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking" version = "2.2.1" @@ -985,12 +1000,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1063,6 +1072,43 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "referencing" +version = "0.46.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb674900ca31acd75c4aaf63f48e43e719631c0539ea5a9e64163d1296bcb730" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.16.0", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.12.3" @@ -1289,7 +1335,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "crc", "crossbeam-queue", @@ -1362,7 +1408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "bytes", @@ -1404,7 +1450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "crc", @@ -1523,36 +1569,6 @@ dependencies = [ "syn", ] -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -1661,6 +1677,12 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.20" @@ -1701,13 +1723,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "uuid" -version = "1.23.1" +name = "uuid-simd" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" dependencies = [ - "js-sys", - "wasm-bindgen", + "outref", + "vsimd", ] [[package]] @@ -1722,6 +1744,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/tests/sqlx/Cargo.toml b/tests/sqlx/Cargo.toml index c419ba8d..875383cf 100644 --- a/tests/sqlx/Cargo.toml +++ b/tests/sqlx/Cargo.toml @@ -10,7 +10,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" anyhow = "1" hex = "0.4" -jsonschema = { version = "0.17", default-features = false } +jsonschema = { version = "0.46.4", default-features = false } [dev-dependencies] # None needed - tests live in this crate diff --git a/tests/sqlx/tests/payload_schema_tests.rs b/tests/sqlx/tests/payload_schema_tests.rs index b71521a1..186a75a7 100644 --- a/tests/sqlx/tests/payload_schema_tests.rs +++ b/tests/sqlx/tests/payload_schema_tests.rs @@ -11,7 +11,7 @@ use std::path::PathBuf; use std::sync::OnceLock; -use jsonschema::JSONSchema; +use jsonschema::Validator; use serde_json::{json, Value}; // ---------- helpers ---------- @@ -25,29 +25,29 @@ fn load_schema(filename: &str) -> Value { serde_json::from_str(&text).unwrap_or_else(|e| panic!("schema is not valid JSON: {e}")) } -fn compile(schema: &Value) -> JSONSchema { - // Schema files declare `$schema: draft 2020-12`. The 0.17 jsonschema crate - // doesn't have an explicit Draft 2020-12 variant; let it fall back to its - // default (Draft 7). The constructs we use ($defs, $ref, oneOf, allOf, - // not, const, additionalProperties, pattern, etc.) are all supported. - JSONSchema::compile(schema).expect("schema fails to compile") +fn compile(schema: &Value) -> Validator { + // `validator_for` auto-detects the draft from the schema's `$schema` + // keyword. Both files declare draft 2020-12, which is supported natively + // by jsonschema >= 0.18. + jsonschema::validator_for(schema).expect("schema fails to compile") } -fn schema_v2_2() -> &'static JSONSchema { - static S: OnceLock = OnceLock::new(); +fn schema_v2_2() -> &'static Validator { + static S: OnceLock = OnceLock::new(); S.get_or_init(|| compile(&load_schema("eql-payload-v2.2.schema.json"))) } -fn schema_v2_3() -> &'static JSONSchema { - static S: OnceLock = OnceLock::new(); +fn schema_v2_3() -> &'static Validator { + static S: OnceLock = OnceLock::new(); S.get_or_init(|| compile(&load_schema("eql-payload-v2.3.schema.json"))) } #[track_caller] -fn assert_valid(schema: &JSONSchema, instance: &Value, label: &str) { - if let Err(errors) = schema.validate(instance) { - let msgs: Vec = errors - .map(|e| format!(" - {} (at {})", e, e.instance_path)) +fn assert_valid(schema: &Validator, instance: &Value, label: &str) { + if !schema.is_valid(instance) { + let msgs: Vec = schema + .iter_errors(instance) + .map(|e| format!(" - {} (at {})", e, e.instance_path())) .collect(); panic!( "expected `{label}` to validate, but got:\n{}\ninstance:\n{}", @@ -58,7 +58,7 @@ fn assert_valid(schema: &JSONSchema, instance: &Value, label: &str) { } #[track_caller] -fn assert_invalid(schema: &JSONSchema, instance: &Value, label: &str) { +fn assert_invalid(schema: &Validator, instance: &Value, label: &str) { if schema.is_valid(instance) { panic!( "expected `{label}` to fail validation, but it passed:\n{}", @@ -259,7 +259,11 @@ fn v2_3_ste_vec_payload_with_hm_in_elements_is_valid() { { "s": SELECTOR, "a": true, "c": CIPHERTEXT, "hm": HEX, "op": HEX } ] }); - assert_valid(schema_v2_3(), &p, "ste_vec payload with hm-bearing elements"); + assert_valid( + schema_v2_3(), + &p, + "ste_vec payload with hm-bearing elements", + ); } #[test] @@ -282,18 +286,30 @@ fn v2_3_legacy_opf_and_opv_are_rejected() { let with_opf = json!({ "v": 2, "c": CIPHERTEXT, "i": ident(), "opf": HEX }); - assert_invalid(schema_v2_3(), &with_opf, "encrypted payload with legacy opf"); + assert_invalid( + schema_v2_3(), + &with_opf, + "encrypted payload with legacy opf", + ); let with_opv = json!({ "v": 2, "c": CIPHERTEXT, "i": ident(), "opv": HEX_LONG }); - assert_invalid(schema_v2_3(), &with_opv, "encrypted payload with legacy opv"); + assert_invalid( + schema_v2_3(), + &with_opv, + "encrypted payload with legacy opv", + ); let element_with_opf = json!({ "v": 2, "k": "sv", "i": ident(), "sv": [{ "s": SELECTOR, "c": CIPHERTEXT, "hm": HEX, "opf": HEX }] }); - assert_invalid(schema_v2_3(), &element_with_opf, "sv element with legacy opf"); + assert_invalid( + schema_v2_3(), + &element_with_opf, + "sv element with legacy opf", + ); } #[test]