Skip to content

WirefilterParser doesn't round-trip keyed header/cookie/query conditions (any(...)/has_key(...) syntax) #277

Description

@gfargo

What's wrong

As of #269 (and #263 before it), ExpressionBuilder.fromUnifiedCondition compiles a keyed query/header/cookie condition to any(field["key"][*] <op> value) (value comparisons) or has_key(field, "key") (exists/not_exists) — the type-valid Cloudflare wirefilter construct, verified against the Ruleset Engine docs.

WirefilterParser.ts (the inverse — Cloudflare wirefilter → UnifiedCondition[], used by cloudflareToUnified) was deliberately not extended to understand this syntax when #263/#269 landed. Its tokenizer/grammar only handles the plain field["key"] <op> value / field["key"] exists shape it previously produced — no function-call syntax (any(, has_key(), no [*] wildcard indexing, and no comma tokenization (needed for has_key(map, "key")'s two arguments).

Concretely: parseWirefilterExpression('any(http.request.headers["x-custom"][*] eq "value")') returns null today, and cloudflareToUnified falls back to its generic "expression could not be parsed back into structured conditions" warning + empty conditions: [] for any rule with a keyed header/cookie/query condition. This is a safe degradation (matches the parser's own documented contract: unsupported constructs return null rather than being guessed at, and the caller already has a warning path for exactly this) but a real capability loss — doorman status/diff/backup/download against a Cloudflare zone with such a rule won't show its actual conditions, just a warning.

Suggested fix

Extend WirefilterParser.ts:

  • Tokenizer (tokenize()): add a COMMA token type (currently , gets silently absorbed into the "maximal run" WORD-reading branch, corrupting anything after it — needed for has_key(map, "key")'s argument separator).
  • Grammar: add function-call parsing for any( and has_key( before parseFieldRef's bare-field-reference path is tried (both start with a WORD token that's actually a function name, not a field path).
    • any(field["key"][*] <op> value) → a comparison node with key set (the [*] wildcard only appears inside any(...), never standalone).
    • has_key(field, "key") → an exists node with key set (mirrors the existing bracket-field["key"] exists handling, just via a function call instead).
  • Reverse the field-name mapping for the keyed map fields specifically — http.request.cookies (keyed cookie) needs to map back to cookie even though it's a different Cloudflare field name than the bare cookie case (http.cookie); same idea for http.request.uri.args (keyed query) vs. http.request.uri.query (bare). CLOUDFLARE_FIELD_TO_UNIFIED as it stands only has entries for the bare field names.
  • Add round-trip tests for all three keyed types (header/cookie/query), covering value comparison, exists/not_exists, and negation — mirroring the round-trip test style already in WirefilterParser.test.ts's "round-trip fidelity against ExpressionBuilder" describe block.
  • Confirm this doesn't affect CelParser.ts — it has its own separate tokenizer/grammar (only TokenStream itself is shared, per its docstring), so this should be a WirefilterParser.ts-only change.

Not urgent (the current behavior is safe, just less complete), but real follow-up work — tracked here rather than left implicit.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions