Skip to content

fix(ext/node): implement BlockList.rules / isBlockList / toJSON / fromJSON#33445

Merged
bartlomieju merged 4 commits into
denoland:mainfrom
nathanwhitbot:fix/node-compat-iter22
Apr 25, 2026
Merged

fix(ext/node): implement BlockList.rules / isBlockList / toJSON / fromJSON#33445
bartlomieju merged 4 commits into
denoland:mainfrom
nathanwhitbot:fix/node-compat-iter22

Conversation

@nathanwhitbot

Copy link
Copy Markdown
Contributor

Summary

Four gaps in Deno's BlockList surfaced by parallel/test-blocklist.js:

  • rules was stubbed as []. Node's lib/internal/blocklist.js returns every added rule as a formatted string (Address: IPv4 1.1.1.1, Range: IPv4 10.0.0.1-10.0.0.10, Subnet: IPv6 .../64) in reverse insertion order.
  • BlockList.isBlockList(value) was missing; Node returns true for BlockList instances.
  • toJSON / fromJSON were missing. Node exposes the rule list as JSON (toJSON returns rules) and can round-trip a string or string[] back through fromJSON; invalid entries are silently ignored, non-string entries throw ERR_INVALID_ARG_TYPE.

Track the rules in JS (the Rust op doesn't surface them yet) and add the three missing APIs, plus a parseIPInfo helper that maps rule strings back to addAddress / addRange / addSubnet calls — same regexes as Node.

Drive-by: addSubnet read family after reassigning network, so the SocketAddress path always saw undefined. Swapped the order.

Enables parallel/test-blocklist.js in the node compat suite.

Test plan

  • cargo test --test node_compat -- test-blocklist.js passes
  • test-blocklist-clone.js still fails as before (pre-existing, not in config.jsonc; needs structured-clone support for the native handle)

…mJSON

BlockList in lib/internal/blocklist.js:

- `rules` returns every rule that's been added, formatted as
  `Address: IPv4 1.1.1.1` / `Range: IPv4 10.0.0.1-10.0.0.10` /
  `Subnet: IPv6 .../64`, in reverse insertion order.
- `BlockList.isBlockList(value)` is true for BlockList instances.
- `toJSON` returns those rules; `fromJSON` accepts the same shape
  (an array of strings, or a JSON-serialized array of strings).
  Invalid entries are silently ignored; non-string entries throw
  `ERR_INVALID_ARG_TYPE`.

Deno's polyfill stubbed `rules` as `[]` and had no `isBlockList` /
`toJSON` / `fromJSON`. Track the rules in JS (the native op doesn't
surface them), and add the three missing APIs along with the rule
string <-> API conversion used by `fromJSON`. Also fixed a latent
bug in `addSubnet` where the SocketAddress-path read `family` after
reassigning `network`, so the case-branch always saw `undefined`.

Enables parallel/test-blocklist.js in the node compat suite.
@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

Matches Node lib/internal/blocklist.js: isBlockList checks internal handle presence, rule strings use Address/Range/Subnet: IPv4|IPv6 ... format with reverse-insertion order, parseIPInfo regexes are the same, fromJSON accepts string|string[] with ERR_INVALID_ARG_TYPE on non-string items. addSubnet family-before-reassign drive-by fix is a real bug fix. Clean port.

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Verified against Node source and parallel/test-blocklist.js.

Correctness — direct port of Node's BlockList APIs

isBlockList at lib/internal/blocklist.js:55-57:

static isBlockList(value) {
  return value?.[kHandle] !== undefined;
}

PR uses value?.[internalBlockList] !== undefined — semantically identical (Deno's symbol vs Node's kHandle). Test at L287-288 covered: isBlockList(new BlockList()) → true, isBlockList({}) → false. ✅

toJSON / fromJSON at L236-264:

toJSON() { return this.rules; }
fromJSON(data) { /* string | string[] validation */ this.#parseIPInfo(data); }

PR's validation block (string vs array vs JSONParse) matches Node line-by-line. The ERR_INVALID_ARG_TYPE check order — array-iteration-first, else typeof !== 'string', else JSONParse-and-validate — is byte-for-byte. ✅

Rule format strings at src/node_sockaddr.cc GetRules:

  • Address: IPv4 1.1.1.1
  • Range: IPv4 1.1.1.1-1.1.1.2
  • Subnet: IPv6 .../64
  • Reverse insertion order via ArrayPrototypeUnshift

Test at L180-185 verifies all three formats + reverse order:

const rulesCheck = [
  'Subnet: IPv6 8592:757c:efae:4e45::/64',  // last added, first listed
  'Range: IPv4 10.0.0.1-10.0.0.10',
  'Address: IPv4 1.1.1.1',                   // first added, last listed
];

PR's unshift produces this order. ✅

parseIPInfo regex set matches Node's at L168-228 — same \d{1,3}(?:\.\d{1,3}){3} for IPv4, same [0-9a-fA-F:]{1,39} with /i flag for IPv6, same continue-on-match flow. ✅

Drive-by fix is correct

Old addSubnet:

network = network.address;  // network now the address string
family = network.family;     // BUG: undefined (string has no .family)

Fix swaps the order. The addAddress and addRange SocketAddress branches had the same latent bug — also fixed in this PR (family = address.family; / family = start.family; set before reassignment). Genuine quiet-bug-rescue.

Test trace — all 6 added APIs ✅

Test line Case PR path
L180-185 rules → reverse order, formatted unshift of formatFamily-prefixed strings ✅
L287-288 isBlockList static method check on internalBlockList symbol ✅
L317 fromJSON invalid input → ERR_INVALID_ARG_TYPE type-check matches Node ✅
L323-324 fromJSON(['1','2','3']) → invalid rules ignored regex no-match → continue, rules stays []
L327-328 fromJSON(bl.toJSON()) round-trip regex re-parses formatted strings ✅
L331-336 round-trip via JSON.stringify and direct array both branches in fromJSON

Concerns / non-blocking

1. Case-insensitive family. Test at L178 uses 'IpV6'. PR handles via StringPrototypeToLowerCase(family) before formatFamily. ✅ But the lowercase happens after the SocketAddress branch's family = address.family assignment in addAddress and addRange — the lowercase is only applied during the formatFamily call. So if a user passes a SocketAddress with family 'IPv6' (preserves case from constructor), the rule string still gets formatted correctly because formatFamily lowercases internally. Fine. In addSubnet the lowercase is applied earlier (before the switch), which is needed for the validateInt32 dispatch. Inconsistency between the three add-methods, but functionally correct in all three.

2. JS-side rules tracking diverges from native handle. PR description acknowledges: "the Rust op doesn't surface them yet". If/when the op gains getRules, this JS-side kRules array will need to be retired or kept in sync. Worth a TODO comment near this[kRules] = [] so it's not forgotten.

3. parseIPInfo will throw on regex-matching-but-semantically-invalid entries. A rule like "Address: IPv4 999.999.999.999" would pass the IPv4 regex (\d{1,3}), then addAddress('999.999.999.999') would throw ERR_INVALID_ARG_VALUE from the SocketAddress constructor. Node has the same behavior — the "invalid entries are silently ignored" claim only covers entries that don't match any regex. This isn't a divergence, just a sharp edge to be aware of.

4. Range with mixed families. addRange(start: SocketAddress(ipv4), end: SocketAddress(ipv6)) — PR captures family = start.family but never validates end.family. Rule string says "Range: IPv4 …" even if end is IPv6. The underlying op_blocklist_add_range almost certainly rejects mismatched families before this matters, so this is a no-op concern, but the rule string is potentially misleading if it ever reaches that path. Pre-existing in the validation flow; not introduced here.

Safety / performance

  • unshift is O(n) per call — negligible for typical blocklist sizes (tens to hundreds of rules).
  • JSONParse, StringPrototypeMatch, NumberParseInt all from primordials — no global tampering.
  • No new resources held; all rules are plain strings.

Config

  • Added parallel/test-blocklist.js as plain {}, alphabetically between -blob-createObjectURL.js and -btoa-atob.js. Clean.

LGTM. Recommend a TODO marker near this[kRules] = [] flagging the JS-side tracking as a temporary substitute for op_blocklist_get_rules, but that's editorial.

nathanwhit and others added 2 commits April 24, 2026 22:59
prefer-primordials lint flagged three raw iterator protocol uses in
the new fromJSON / parseIPInfo paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
prefer-primordials lint flagged 8 remaining errors (StringPrototypeIncludes
calls and 6 RegExp literals) after the SafeArrayIterator fix. Replace .includes
with StringPrototypeIncludes, hoist the 6 IPv4/IPv6 regexes to module-level
SafeRegExp constants, and switch to RegExpPrototypeExec.
@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

test unit debug windows-x86_64 failure is just the actions/upload-artifact step — 100 unit tests all passed. GitHub artifact upload infra flake. Rerun needed. cc @nathanwhit

@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

wpt release linux-x86_64 failure is also infra — the Autobahn testsuite step (job 72968978451) failed with error: Import 'https://deno.land/x/[email protected]/mod.ts' failed: 500 Internal Server Error while loading ext/websocket/autobahn/fuzzingclient.js. deno.land/x returned a 500 for the dax module download. Unrelated to BlockList. cc @nathanwhit

@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

wpt release linux-x86_64 failure is a wpt expectations drift (fetch.json/schema.json deltas), unrelated to this PR's BlockList changes. Same kind of flake hit earlier on iter12/iter20. Rerun needed.

Comment thread ext/node/polyfills/internal/blocklist.mjs Outdated
Comment thread ext/node/polyfills/internal/blocklist.mjs Outdated
Comment thread ext/node/polyfills/internal/blocklist.mjs Outdated
Comment thread ext/node/polyfills/internal/blocklist.mjs Outdated
Comment thread ext/node/polyfills/internal/blocklist.mjs Outdated
@bartlomieju
bartlomieju enabled auto-merge (squash) April 25, 2026 05:47
@bartlomieju
bartlomieju merged commit 7c9e1f6 into denoland:main Apr 25, 2026
112 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants