Repository: nodejs/node
Pull request: net: update net.blocklist to allow file save and file management

Fixes: https://github.com/nodejs/node/issues/56252
<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.
all build test work 
If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: doc/api/net.md
patch: @@ -181,6 +181,38 @@ added:
 * `value` {any} Any JS value
 * Returns `true` if the `value` is a `net.BlockList`.
 
+### `blockList.fromJSON(value)`
+
+> Stability: 1 - Experimental
+
+ <!-- YAML
+added: REPLACEME
+-->
+
+```js
+const blockList = new net.BlockList();
+const data = [
+  'Subnet: IPv4 192.168.1.0/24',
+  'Address: IPv4 10.0.0.5',
+  'Range: IPv4 192.168.2.1-192.168.2.10',
+  'Range: IPv4 10.0.0.1-10.0.0.10',
+];
+blockList.fromJSON(data);
+blockList.fromJSON(JSON.stringify(data));
+```
+
+* `value` Blocklist.rules
+
+### `blockList.toJSON()`
+
+> Stability: 1 - Experimental
+
+ <!-- YAML
+added: REPLACEME
+-->
+
+* Returns Blocklist.rules
+
 ## Class: `net.SocketAddress`
 
 <!-- YAML

filename: lib/internal/blocklist.js
patch: @@ -1,7 +1,10 @@
 'use strict';
 
 const {
+  ArrayIsArray,
   Boolean,
+  JSONParse,
+  NumberParseInt,
   ObjectSetPrototypeOf,
   Symbol,
 } = primordials;
@@ -32,6 +35,7 @@ const { owner_symbol } = internalBinding('symbols');
 
 const {
   ERR_INVALID_ARG_VALUE,
+  ERR_INVALID_ARG_TYPE,
 } = require('internal/errors').codes;
 
 const { validateInt32, validateString } = require('internal/validators');
@@ -139,10 +143,130 @@ class BlockList {
     return Boolean(this[kHandle].check(address[kSocketAddressHandle]));
   }
 
+  /*
+  * @param {string[]} data
+  * @example
+  * const data = [
+  *   // IPv4 examples
+  *   'Subnet: IPv4 192.168.1.0/24',
+  *   'Address: IPv4 10.0.0.5',
+  *   'Range: IPv4 192.168.2.1-192.168.2.10',
+  *   'Range: IPv4 10.0.0.1-10.0.0.10',
+  *
+  *   // IPv6 examples
+  *   'Subnet: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334/64',
+  *   'Address: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
+  *   'Range: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335',
+  *   'Subnet: IPv6 2001:db8:1234::/48',
+  *   'Address: IPv6 2001:db8:1234::1',
+  *   'Range: IPv6 2001:db8:1234::1-2001:db8:1234::10'
+  * ];
+  */
+  #parseIPInfo(data) {
+    for (const item of data) {
+      if (item.includes('IPv4')) {
+        const subnetMatch = item.match(
+          /Subnet: IPv4 (\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})/,
+        );
+        if (subnetMatch) {
+          const { 1: network, 2: prefix } = subnetMatch;
+          this.addSubnet(network, NumberParseInt(prefix));
+          continue;
+        }
+        const addressMatch = item.match(/Address: IPv4 (\d{1,3}(?:\.\d{1,3}){3})/);
+        if (addressMatch) {
+          const { 1: address } = addressMatch;
+          this.addAddress(address);
+          continue;
+        }
+
+        const rangeMatch = item.match(
+          /Range: IPv4 (\d{1,3}(?:\.\d{1,3}){3})-(\d{1,3}(?:\.\d{1,3}){3})/,
+        );
+        if (rangeMatch) {
+          const { 1: start, 2: end } = rangeMatch;
+          this.addRange(start, end);
+          continue;
+        }
+      }
+      // IPv6 parsing with support for compressed addresses
+      if (item.includes('IPv6')) {
+        // IPv6 subnet pattern: supports both full and compressed formats
+        // Examples:
+        // - 2001:0db8:85a3:0000:0000:8a2e:0370:7334/64 (full)
+        // - 2001:db8:85a3::8a2e:370:7334/64 (compressed)
+        // - 2001:db8:85a3::192.0.2.128/64 (mixed)
+        const ipv6SubnetMatch = item.match(
+          /Subnet: IPv6 ([0-9a-fA-F:]{1,39})\/([0-9]{1,3})/i,
+        );
+        if (ipv6SubnetMatch) {
+          const { 1: network, 2: prefix } = ipv6SubnetMatch;
+          this.addSubnet(network, NumberParseInt(prefix), 'ipv6');
+          continue;
+        }
+
+        // IPv6 address pattern: supports both full and compressed formats
+        // Examples:
+        // - 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (full)
+        // - 2001:db8:85a3::8a2e:370:7334 (compressed)
+        // - 2001:db8:85a3::192.0.2.128 (mixed)
+        const ipv6AddressMatch = item.match(/Address: IPv6 ([0-9a-fA-F:]{1,39})/i);
+        if (ipv6AddressMatch) {
+          const { 1: address } = ipv6AddressMatch;
+          this.addAddress(address, 'ipv6');
+          continue;
+        }
+
+        // IPv6 range pattern: supports both full and compressed formats
+        // Examples:
+        // - 2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335 (full)
+        // - 2001:db8:85a3::8a2e:370:7334-2001:db8:85a3::8a2e:370:7335 (compressed)
+        // - 2001:db8:85a3::192.0.2.128-2001:db8:85a3::192.0.2.129 (mixed)
+        const ipv6RangeMatch = item.match(/Range: IPv6 ([0-9a-fA-F:]{1,39})-([0-9a-fA-F:]{1,39})/i);
+        if (ipv6RangeMatch) {
+          const { 1: start, 2: end } = ipv6RangeMatch;
+          this.addRange(start, end, 'ipv6');
+          continue;
+        }
+      }
+    }
+  }
+
+
+  toJSON() {
+    return this.rules;
+  }
+
+  fromJSON(data) {
+    // The data argument must be a string, or an array of strings that
+    // is JSON parseable.
+    if (ArrayIsArray(data)) {
+      for (const n of data) {
+        if (typeof n !== 'string') {
+          throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
+        }
+      }
+    } else if (typeof data !== 'string') {
+      throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
+    } else {
+      data = JSONParse(data);
+      if (!ArrayIsArray(data)) {
+        throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
+      }
+      for (const n of data) {
+        if (typeof n !== 'string') {
+          throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
+        }
+      }
+    }
+
+    this.#parseIPInfo(data);
+  }
+
+
   get rules() {
     return this[kHandle].getRules();
   }
-
   [kClone]() {
     const handle = this[kHandle];
     return {

filename: test/parallel/test-blocklist.js
patch: @@ -287,3 +287,75 @@ const util = require('util');
   assert(BlockList.isBlockList(new BlockList()));
   assert(!BlockList.isBlockList({}));
 }
+
+// Test exporting and importing the rule list to/from JSON
+{
+  const ruleList = [
+    'Address: IPv4 10.0.0.5',
+    'Address: IPv6 ::',
+    'Subnet: IPv4 192.168.1.0/24',
+    'Subnet: IPv6 8592:757c:efae:4e45::/64',
+  ];
+
+  const test2 = new BlockList();
+  const test3 = new BlockList();
+  const test4 = new BlockList();
+  const test5 = new BlockList();
+
+  const bl = new BlockList();
+  bl.addAddress('10.0.0.5');
+  bl.addAddress('::', 'ipv6');
+  bl.addSubnet('192.168.1.0', 24);
+  bl.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6');
+
+  // Test invalid inputs (input to fromJSON must be an array of
+  // string rules or a serialized json string of an array of
+  // string rules.
+  [
+    1, null, Symbol(), [1, 2, 3], '123', [Symbol()], new Map(),
+  ].forEach((i) => {
+    assert.throws(() => test2.fromJSON(i), {
+      code: 'ERR_INVALID_ARG_TYPE',
+    });
+  });
+
+  // Invalid rules are ignored.
+  test2.fromJSON(['1', '2', '3']);
+  assert.deepStrictEqual(test2.rules, []);
+
+  // Direct output from toJSON method works
+  test2.fromJSON(bl.toJSON());
+  assert.deepStrictEqual(test2.rules.sort(), ruleList);
+
+  // JSON stringified output works
+  test3.fromJSON(JSON.stringify(bl));
+  assert.deepStrictEqual(test3.rules.sort(), ruleList);
+
+  // A raw array works
+  test4.fromJSON(ruleList);
+  assert.deepStrictEqual(test4.rules.sort(), ruleList);
+
+  // Individual rules work
+  ruleList.forEach((item) => {
+    test5.fromJSON([item]);
+  });
+  assert.deepStrictEqual(test5.rules.sort(), ruleList);
+
+  // Each of the created blocklists should handle the checks identically.
+  [
+    ['10.0.0.5', 'ipv4', true],
+    ['10.0.0.6', 'ipv4', false],
+    ['::', 'ipv6', true],
+    ['::1', 'ipv6', false],
+    ['192.168.1.0', 'ipv4', true],
+    ['193.168.1.0', 'ipv4', false],
+    ['8592:757c:efae:4e45::', 'ipv6', true],
+    ['1111:1111:1111:1111::', 'ipv6', false],
+  ].forEach((i) => {
+    assert.strictEqual(bl.check(i[0], i[1]), i[2]);
+    assert.strictEqual(test2.check(i[0], i[1]), i[2]);
+    assert.strictEqual(test3.check(i[0], i[1]), i[2]);
+    assert.strictEqual(test4.check(i[0], i[1]), i[2]);
+    assert.strictEqual(test5.check(i[0], i[1]), i[2]);
+  });
+}

filename: test/parallel/test-net-blocklist.js
patch: @@ -3,7 +3,6 @@
 const common = require('../common');
 const net = require('net');
 const assert = require('assert');
-
 const blockList = new net.BlockList();
 blockList.addAddress('127.0.0.1');
 blockList.addAddress('127.0.0.2');