From 890a6500690039b798f6b62005a3b3006db642d5 Mon Sep 17 00:00:00 2001 From: Ron Klein Date: Mon, 21 Sep 2026 08:34:23 +0300 Subject: [PATCH] Replace IPv4 parser internals with regex-free scanners (#1) * Replace IPv4 parser internals with regex-free scanners Port the strict four-part digit scanner and loose inet_aton digit/base scanner in-tree so IPv4.isValid / parse / parseCIDR / isValidFourPartDecimal no longer use RegExp. Keep the public API and IPv6 transitional ipv4Part pattern unchanged for a small upstreamable diff. * Harden parseIntAutoDigits against empty digit spans Throw when start >= end, matching @kleinron/ipv4, so a caller bug cannot silently return 0 from an empty range. --------- Co-authored-by: Ron Klein <1203923+kleinron@users.noreply.github.com> --- lib/ipaddr.js | 363 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 257 insertions(+), 106 deletions(-) diff --git a/lib/ipaddr.js b/lib/ipaddr.js index 4e210ed..1feb26b 100644 --- a/lib/ipaddr.js +++ b/lib/ipaddr.js @@ -1,17 +1,7 @@ -// A list of regular expressions that match arbitrary IPv4 addresses, -// for which a number of weird notations exist. -// Note that an address like 0010.0xa5.1.1 is considered legal. +// IPv4 part pattern reused by IPv6 transitional regexes only. +// Loose / inet_aton IPv4 parsing is regex-free (see parseIPv4Aton below). +// Note that an address like 0010.0xa5.1.1 is considered legal for loose IPv4. const ipv4Part = '(0?\\d+|0x[a-f0-9]+)'; -const ipv4Regexes = { - fourOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}$`, 'i'), - threeOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}$`, 'i'), - twoOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}$`, 'i'), - longValue: new RegExp(`^${ipv4Part}$`, 'i') -}; - -// Regular Expression for checking Octal numbers -const octalRegex = new RegExp(`^0[0-7]+$`, 'i'); -const hexRegex = new RegExp(`^0x[a-f0-9]+$`, 'i'); const zoneIndex = '%[0-9a-z]{1,}'; @@ -138,22 +128,254 @@ function matchCIDR (first, second, partSize, cidrBits) { return true; } -function parseIntAuto (string) { - // Hexadecimal base 16 (0x#) - if (hexRegex.test(string)) { - return parseInt(string, 16); +// Strict four-part decimal scanner (no leading zeros, octets 0-255). +// Equivalent accept set to isValid + /^(0|[1-9]\d*)(\.(0|[1-9]\d*)){3}$/. +function scanIPv4FourPart (string) { + if (typeof string !== 'string') { + return null; + } + + const n = string.length; + if (n < 7 || n > 15) { + return null; + } + + const octets = new Array(4); + let p = 0; + + for (let i = 0; i < 4; i++) { + if (p >= n) { + return null; + } + + const c0 = string.charCodeAt(p); + if (c0 < 0x30 || c0 > 0x39) { + return null; + } + + let v = c0 - 0x30; + p++; + let ndigits = 1; + + // Leading zero ban: if first digit is 0, field must end + if (v === 0) { + if (i < 3) { + if (p >= n || string.charCodeAt(p) !== 0x2e) { + return null; + } + } else if (p !== n) { + return null; + } + octets[i] = 0; + if (i < 3) { + p++; // consume '.' + } + continue; + } + + // Accumulate up to 2 more digits; reject a 4th digit before overflow math + while (p < n) { + const c = string.charCodeAt(p); + if (c < 0x30 || c > 0x39) { + break; + } + if (ndigits >= 3) { + return null; + } + v = v * 10 + (c - 0x30); + ndigits++; + p++; + } + + if (v > 255) { + return null; + } + octets[i] = v; + + if (i < 3) { + if (p >= n || string.charCodeAt(p) !== 0x2e) { + return null; + } + p++; // consume '.' + } else if (p !== n) { + return null; + } + } + + return octets; +} + +// parseIntAuto on a verified digit slice [start, end). +// Octal when leading 0 + at least one more digit; else decimal. +function parseIntAutoDigits (string, start, end) { + if (start >= end) { + throw new Error('ipaddr: empty digit span'); + } + const len = end - start; + // leading 0 and a following digit → octal path (legacy parseIntAuto) + if (string.charCodeAt(start) === 0x30 && len >= 2) { + let v = 0; + for (let k = start; k < end; k++) { + const c = string.charCodeAt(k); + if (c > 0x37) { + // 8 or 9 — same throw as legacy octal parse + throw new Error(`ipaddr: cannot parse ${string.slice(start, end)} as octal`); + } + v = v * 8 + (c - 0x30); + } + return v; + } + + let v = 0; + for (let k = start; k < end; k++) { + v = v * 10 + (string.charCodeAt(k) - 0x30); + } + return v; +} + +// Same 1/2/3/4-field expansion + range checks as the legacy IPv4.parser. +function packIPv4Fields (vals) { + const len = vals.length; + if (len === 4) { + return vals; + } + if (len === 1) { + const value = vals[0]; + if (value > 0xffffffff || value < 0) { + throw new Error('ipaddr: address outside defined range'); + } + return [ + (value >>> 24) & 0xff, + (value >>> 16) & 0xff, + (value >>> 8) & 0xff, + value & 0xff + ]; + } + if (len === 2) { + const value = vals[1]; + if (value > 0xffffff || value < 0) { + throw new Error('ipaddr: address outside defined range'); + } + return [ + vals[0], + (value >>> 16) & 0xff, + (value >>> 8) & 0xff, + value & 0xff + ]; + } + if (len === 3) { + const value = vals[2]; + if (value > 0xffff || value < 0) { + throw new Error('ipaddr: address outside defined range'); + } + return [vals[0], vals[1], (value >>> 8) & 0xff, value & 0xff]; + } + return null; +} + +// Regex-free loose / inet_aton scanner — identical accept set to the +// former ipv4Regexes + parseIntAuto IPv4.parser path. +function parseIPv4Aton (string) { + const n = string.length; + if (n === 0) { + return null; + } + + const vals = []; + let i = 0; + + while (i < n) { + if (vals.length >= 4) { + return null; + } + + const c0 = string.charCodeAt(i); + + // hex: 0x / 0X + one or more hex digits + if ( + c0 === 0x30 && + i + 1 < n && + (string.charCodeAt(i + 1) === 0x78 || string.charCodeAt(i + 1) === 0x58) + ) { + i += 2; + let v = 0; + let digits = 0; + while (i < n) { + const c = string.charCodeAt(i); + let d; + if (c >= 0x30 && c <= 0x39) { + d = c - 0x30; + } else if (c >= 0x61 && c <= 0x66) { + d = c - 0x61 + 10; + } else if (c >= 0x41 && c <= 0x46) { + d = c - 0x41 + 10; + } else { + break; + } + v = v * 16 + d; + digits++; + i++; + } + if (digits === 0) { + return null; + } + vals.push(v); + } else { + // decimal / octal digit run — must start with 0-9 + if (c0 < 0x30 || c0 > 0x39) { + return null; + } + const start = i; + i++; + while (i < n) { + const c = string.charCodeAt(i); + if (c < 0x30 || c > 0x39) { + break; + } + i++; + } + vals.push(parseIntAutoDigits(string, start, i)); + } + + if (i === n) { + break; + } + // separator must be '.' and must not be trailing + if (string.charCodeAt(i) !== 0x2e) { + return null; + } + i++; + if (i === n) { + return null; + } + } + + return packIPv4Fields(vals); +} + +// Regex-free CIDR split: last '/', non-empty addr, non-empty decimal mask digits. +function splitIPv4CIDR (string) { + const n = string.length; + let slash = -1; + for (let i = n - 1; i >= 0; i--) { + if (string.charCodeAt(i) === 0x2f) { + slash = i; + break; + } } - // While octal representation is discouraged by ECMAScript 3 - // and forbidden by ECMAScript 5, we silently allow it to - // work only if the rest of the string has numbers less than 8. - if (string[0] === '0' && !isNaN(parseInt(string[1], 10))) { - if (octalRegex.test(string)) { - return parseInt(string, 8); + if (slash <= 0 || slash >= n - 1) { + return null; } - throw new Error(`ipaddr: cannot parse ${string} as octal`); + + let mask = 0; + for (let i = slash + 1; i < n; i++) { + const c = string.charCodeAt(i); + if (c < 0x30 || c > 0x39) { + return null; + } + mask = mask * 10 + (c - 0x30); } - // Always include the base 10 radix! - return parseInt(string, 10); + return { addr: string.slice(0, slash), mask: mask }; } function padPart (part, length) { @@ -364,22 +586,18 @@ export class IPv4 { // Checks if a given string is a full four-part IPv4 Address. static isValidFourPartDecimal(string) { - if (IPv4.isValid(string) && string.match(/^(0|[1-9]\d*)(\.(0|[1-9]\d*)){3}$/)) { - return true; - } else { - return false; - } + return scanIPv4FourPart(string) !== null; }; // Checks if a given string is a full four-part IPv4 Address with CIDR prefix. static isValidCIDRFourPartDecimal(string) { - const match = string.match(/^(.+)\/(\d+)$/); + const parts = splitIPv4CIDR(string); - if (!IPv4.isValidCIDR(string) || !match) { + if (!IPv4.isValidCIDR(string) || !parts) { return false; } - return IPv4.isValidFourPartDecimal(match[1]); + return IPv4.isValidFourPartDecimal(parts.addr); }; // A utility function to return network address given the IPv4 interface and prefix length in CIDR notation @@ -418,12 +636,12 @@ export class IPv4 { // Parses the string as an IPv4 Address with CIDR Notation. static parseCIDR(string) { - let match; + const parts = splitIPv4CIDR(string); - if ((match = string.match(/^(.+)\/(\d+)$/))) { - const maskLength = parseInt(match[2]); + if (parts) { + const maskLength = parts.mask; if (maskLength >= 0 && maskLength <= 32) { - const parsed = [this.parse(match[1]), maskLength]; + const parsed = [this.parse(parts.addr), maskLength]; Object.defineProperty(parsed, 'toString', { value: function () { return this.join('/'); @@ -440,74 +658,7 @@ export class IPv4 { // value representing last three octets; this corresponds to a class C // address) are omitted due to classless nature of modern Internet. static parser(string) { - let match, part, value; - - // parseInt recognizes all that octal & hexadecimal weirdness for us - if ((match = string.match(ipv4Regexes.fourOctet))) { - return (function () { - const ref = match.slice(1, 6); - const results = []; - - for (let i = 0; i < ref.length; i++) { - part = ref[i]; - results.push(parseIntAuto(part)); - } - - return results; - })(); - } else if ((match = string.match(ipv4Regexes.longValue))) { - value = parseIntAuto(match[1]); - if (value > 0xffffffff || value < 0) { - throw new Error('ipaddr: address outside defined range'); - } - - return ((function () { - const results = []; - let shift; - - for (shift = 0; shift <= 24; shift += 8) { - results.push((value >> shift) & 0xff); - } - - return results; - })()).reverse(); - } else if ((match = string.match(ipv4Regexes.twoOctet))) { - return (function () { - const ref = match.slice(1, 4); - const results = []; - - value = parseIntAuto(ref[1]); - if (value > 0xffffff || value < 0) { - throw new Error('ipaddr: address outside defined range'); - } - - results.push(parseIntAuto(ref[0])); - results.push((value >> 16) & 0xff); - results.push((value >> 8) & 0xff); - results.push( value & 0xff); - - return results; - })(); - } else if ((match = string.match(ipv4Regexes.threeOctet))) { - return (function () { - const ref = match.slice(1, 5); - const results = []; - - value = parseIntAuto(ref[2]); - if (value > 0xffff || value < 0) { - throw new Error('ipaddr: address outside defined range'); - } - - results.push(parseIntAuto(ref[0])); - results.push(parseIntAuto(ref[1])); - results.push((value >> 8) & 0xff); - results.push( value & 0xff); - - return results; - })(); - } else { - return null; - } + return parseIPv4Aton(string); }; // A utility function to return subnet mask in IPv4 format given the prefix length