fix(ext/node): implement BlockList.rules / isBlockList / toJSON / fromJSON#33445
Conversation
…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.
|
Matches Node |
nathanwhitbot
left a comment
There was a problem hiding this comment.
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
unshiftis O(n) per call — negligible for typical blocklist sizes (tens to hundreds of rules).JSONParse,StringPrototypeMatch,NumberParseIntall from primordials — no global tampering.- No new resources held; all rules are plain strings.
Config
- Added
parallel/test-blocklist.jsas plain{}, alphabetically between-blob-createObjectURL.jsand-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.
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.
|
|
|
|
|
|
Summary
Four gaps in Deno's
BlockListsurfaced byparallel/test-blocklist.js:ruleswas stubbed as[]. Node'slib/internal/blocklist.jsreturns 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/fromJSONwere missing. Node exposes the rule list as JSON (toJSONreturnsrules) and can round-trip a string orstring[]back throughfromJSON; invalid entries are silently ignored, non-string entries throwERR_INVALID_ARG_TYPE.Track the rules in JS (the Rust op doesn't surface them yet) and add the three missing APIs, plus a
parseIPInfohelper that maps rule strings back toaddAddress/addRange/addSubnetcalls — same regexes as Node.Drive-by:
addSubnetreadfamilyafter reassigningnetwork, so the SocketAddress path always sawundefined. Swapped the order.Enables
parallel/test-blocklist.jsin the node compat suite.Test plan
cargo test --test node_compat -- test-blocklist.jspassestest-blocklist-clone.jsstill fails as before (pre-existing, not inconfig.jsonc; needs structured-clone support for the native handle)