mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Return null for invalid options or limits instead of throwing during BigInt conversion. Preserve zero, default, and safe integer limits. Co-Authored-By: GPT-6 <noreply@openai.com>
379 lines
15 KiB
JavaScript
379 lines
15 KiB
JavaScript
// IP address arithmetic shared by the IP Calculator (front-end) and rdap.js's
|
||
// CIDR containment (back-end). Both families are BigInt: 32-bit `<<` goes
|
||
// negative on 240.0.0.0, and IPv6 counts exceed Number anyway.
|
||
//
|
||
// Parsed = { family: 4 | 6, value: bigint }
|
||
// Cidr = { family, prefix, address, network, aligned }
|
||
//
|
||
// Every export is pure, never throws, and returns null (false for predicates)
|
||
// on unusable input. Parsers are strict — `01.2.3.4` is rejected because
|
||
// inet_aton would read the octet as octal; ip-calc.js labels such forms
|
||
// instead. No imports, so valid-ip.js could adopt this file without a cycle.
|
||
|
||
const BITS = { 4: 32, 6: 128 };
|
||
const MAX = { 4: (1n << 32n) - 1n, 6: (1n << 128n) - 1n };
|
||
|
||
const V4_OCTET = /^(0|[1-9]\d{0,2})$/;
|
||
const HEX_GROUP = /^[0-9a-fA-F]{1,4}$/;
|
||
const V6_CHARS = /^[0-9a-fA-F:.]+$/;
|
||
|
||
const isFamily = (family) => family === 4 || family === 6;
|
||
const isBig = (v) => typeof v === 'bigint';
|
||
const inFamily = (value, family) => isBig(value) && value >= 0n && value <= MAX[family];
|
||
const isPrefix = (prefix, family) =>
|
||
Number.isInteger(prefix) && prefix >= 0 && prefix <= BITS[family];
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Parsing */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
export const parseIPv4 = (str) => {
|
||
if (typeof str !== 'string') return null;
|
||
const parts = str.split('.');
|
||
if (parts.length !== 4) return null;
|
||
let value = 0n;
|
||
for (const part of parts) {
|
||
if (!V4_OCTET.test(part)) return null;
|
||
const n = Number(part);
|
||
if (n > 255) return null;
|
||
value = (value << 8n) | BigInt(n);
|
||
}
|
||
return { family: 4, value };
|
||
};
|
||
|
||
// One `::` at most, 1–4 hex per group, and a dotted-quad tail only as the
|
||
// final token (`::ffff:192.0.2.1`), which then stands in for two groups.
|
||
export const parseIPv6 = (str) => {
|
||
if (typeof str !== 'string' || str === '' || !V6_CHARS.test(str)) return null;
|
||
|
||
let text = str;
|
||
let embeddedV4 = false;
|
||
if (str.includes('.')) {
|
||
const lastColon = str.lastIndexOf(':');
|
||
const tail = str.slice(lastColon + 1);
|
||
if (lastColon < 0 || !tail.includes('.')) return null;
|
||
const quad = parseIPv4(tail);
|
||
if (!quad) return null;
|
||
const hi = (quad.value >> 16n).toString(16);
|
||
const lo = (quad.value & 0xffffn).toString(16);
|
||
text = `${str.slice(0, lastColon + 1)}${hi}:${lo}`;
|
||
embeddedV4 = true;
|
||
}
|
||
|
||
const halves = text.split('::');
|
||
if (halves.length > 2) return null;
|
||
const shorthand = halves.length === 2;
|
||
const left = halves[0] === '' ? [] : halves[0].split(':');
|
||
const right = shorthand && halves[1] !== '' ? halves[1].split(':') : [];
|
||
const explicit = left.length + right.length;
|
||
if (shorthand ? explicit > 7 : explicit !== 8) return null;
|
||
|
||
const groups = [...left, ...Array(shorthand ? 8 - explicit : 0).fill('0'), ...right];
|
||
let value = 0n;
|
||
for (const group of groups) {
|
||
if (!HEX_GROUP.test(group)) return null;
|
||
value = (value << 16n) | BigInt(parseInt(group, 16));
|
||
}
|
||
return { family: 6, value, embeddedV4 };
|
||
};
|
||
|
||
export const parseIp = (str) => parseIPv4(str) || parseIPv6(str);
|
||
|
||
export const ipToBigInt = (str) => parseIp(str)?.value ?? null;
|
||
|
||
// `a.b.c.d/n`, `x::/n`, or (IPv4 only) `a.b.c.d/m.m.m.m` with a contiguous
|
||
// dotted mask. Host bits are kept on `address`; `network` has them cleared.
|
||
export const parseCidr = (str) => {
|
||
if (typeof str !== 'string') return null;
|
||
const slash = str.indexOf('/');
|
||
if (slash <= 0 || slash !== str.lastIndexOf('/')) return null;
|
||
const parsed = parseIp(str.slice(0, slash));
|
||
if (!parsed) return null;
|
||
const { family, value } = parsed;
|
||
|
||
const rhs = str.slice(slash + 1);
|
||
let prefix = null;
|
||
if (/^\d{1,3}$/.test(rhs)) {
|
||
prefix = Number(rhs);
|
||
} else if (family === 4) {
|
||
const mask = parseIPv4(rhs);
|
||
prefix = mask ? maskToPrefix(mask.value, 4) : null;
|
||
}
|
||
if (prefix === null || !isPrefix(prefix, family)) return null;
|
||
|
||
const network = value & prefixToMask(prefix, family);
|
||
return { family, prefix, address: value, network, aligned: network === value };
|
||
};
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Formatting */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
export const toOctets = (value) => {
|
||
if (!inFamily(value, 4)) return null;
|
||
return [24n, 16n, 8n, 0n].map((shift) => Number((value >> shift) & 0xffn));
|
||
};
|
||
|
||
export const toHextets = (value) => {
|
||
if (!inFamily(value, 6)) return null;
|
||
return Array.from({ length: 8 }, (_, i) => Number((value >> BigInt((7 - i) * 16)) & 0xffffn));
|
||
};
|
||
|
||
export const formatIPv4 = (value) => toOctets(value)?.join('.') ?? null;
|
||
|
||
// RFC 5952: lowercase, no leading zeros, the longest run of two or more zero
|
||
// groups becomes `::` (first run wins a tie), a lone zero group stays.
|
||
export const formatIPv6 = (value, { expanded = false } = {}) => {
|
||
const hextets = toHextets(value);
|
||
if (!hextets) return null;
|
||
if (expanded) return hextets.map((h) => h.toString(16).padStart(4, '0')).join(':');
|
||
|
||
let bestStart = -1;
|
||
let bestLen = 0;
|
||
for (let i = 0; i < 8; i += 1) {
|
||
if (hextets[i] !== 0) continue;
|
||
let j = i;
|
||
while (j < 8 && hextets[j] === 0) j += 1;
|
||
if (j - i > bestLen) {
|
||
bestStart = i;
|
||
bestLen = j - i;
|
||
}
|
||
i = j;
|
||
}
|
||
const hex = hextets.map((h) => h.toString(16));
|
||
if (bestLen < 2) return hex.join(':');
|
||
const head = hex.slice(0, bestStart).join(':');
|
||
const tail = hex.slice(bestStart + bestLen).join(':');
|
||
return `${head}::${tail}`;
|
||
};
|
||
|
||
export const formatIp = (parsed) => {
|
||
if (!parsed || !isFamily(parsed.family)) return null;
|
||
return parsed.family === 4 ? formatIPv4(parsed.value) : formatIPv6(parsed.value);
|
||
};
|
||
|
||
export const formatCidr = (cidr, { network = true } = {}) => {
|
||
if (!cidr || !isFamily(cidr.family)) return null;
|
||
const ip = formatIp({ family: cidr.family, value: network ? cidr.network : cidr.address });
|
||
return ip === null ? null : `${ip}/${cidr.prefix}`;
|
||
};
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Masks & counts */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
export const prefixToMask = (prefix, family) => {
|
||
if (!isFamily(family) || !isPrefix(prefix, family)) return null;
|
||
const bits = BITS[family];
|
||
return ((1n << BigInt(prefix)) - 1n) << BigInt(bits - prefix);
|
||
};
|
||
|
||
// Null unless the mask is ones followed by zeros.
|
||
export const maskToPrefix = (mask, family) => {
|
||
if (!isFamily(family) || !inFamily(mask, family)) return null;
|
||
for (let prefix = 0; prefix <= BITS[family]; prefix += 1) {
|
||
if (prefixToMask(prefix, family) === mask) return prefix;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
export const wildcardMask = (prefix, family) => {
|
||
const mask = prefixToMask(prefix, family);
|
||
return mask === null ? null : MAX[family] ^ mask;
|
||
};
|
||
|
||
export const addressCount = (prefix, family) => {
|
||
if (!isFamily(family) || !isPrefix(prefix, family)) return null;
|
||
return 1n << BigInt(BITS[family] - prefix);
|
||
};
|
||
|
||
// IPv4 loses network + broadcast except on /31 (RFC 3021 point-to-point)
|
||
// and /32; IPv6 has no broadcast, every address is assignable.
|
||
export const usableCount = (prefix, family) => {
|
||
const count = addressCount(prefix, family);
|
||
if (count === null) return null;
|
||
if (family === 6 || prefix >= 31) return count;
|
||
return count - 2n;
|
||
};
|
||
|
||
// Smallest block that holds `count` addresses (number or bigint).
|
||
export const smallestPrefixFor = (count, family) => {
|
||
if (!isFamily(family)) return null;
|
||
let need;
|
||
try {
|
||
need = BigInt(count);
|
||
} catch {
|
||
return null;
|
||
}
|
||
if (need < 1n) return null;
|
||
for (let prefix = BITS[family]; prefix >= 0; prefix -= 1) {
|
||
if (addressCount(prefix, family) >= need) return prefix;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Block info */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
export const cidrInfo = (str) => {
|
||
const cidr = parseCidr(str);
|
||
if (!cidr) return null;
|
||
const { family, prefix, address, network, aligned } = cidr;
|
||
const mask = prefixToMask(prefix, family);
|
||
const wildcard = wildcardMask(prefix, family);
|
||
const lastAddress = network | wildcard;
|
||
const hostRange = family === 4 && prefix < 31;
|
||
return {
|
||
family,
|
||
prefix,
|
||
cidr: formatCidr(cidr),
|
||
address,
|
||
network,
|
||
broadcast: family === 4 ? lastAddress : null,
|
||
first: hostRange ? network + 1n : network,
|
||
last: hostRange ? lastAddress - 1n : lastAddress,
|
||
lastAddress,
|
||
mask,
|
||
wildcard,
|
||
count: addressCount(prefix, family),
|
||
usable: usableCount(prefix, family),
|
||
aligned,
|
||
};
|
||
};
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Containment & ordering */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
export const prefixContains = (network, prefix, family, value) => {
|
||
if (!isFamily(family) || !isPrefix(prefix, family)
|
||
|| !inFamily(network, family) || !inFamily(value, family)) return false;
|
||
const shift = BigInt(BITS[family] - prefix);
|
||
return (value >> shift) === (network >> shift);
|
||
};
|
||
|
||
export const cidrContains = (cidrStr, ipStr) => {
|
||
const cidr = parseCidr(cidrStr);
|
||
const ip = parseIp(ipStr);
|
||
if (!cidr || !ip || cidr.family !== ip.family) return null;
|
||
return prefixContains(cidr.network, cidr.prefix, cidr.family, ip.value);
|
||
};
|
||
|
||
export const cidrOverlaps = (aStr, bStr) => {
|
||
const a = parseCidr(aStr);
|
||
const b = parseCidr(bStr);
|
||
if (!a || !b || a.family !== b.family) return null;
|
||
return prefixContains(a.network, a.prefix, a.family, b.network)
|
||
|| prefixContains(b.network, b.prefix, b.family, a.network);
|
||
};
|
||
|
||
// IPv4 sorts before IPv6; within a family, numerically.
|
||
export const compareIps = (a, b) => {
|
||
if (a.family !== b.family) return a.family < b.family ? -1 : 1;
|
||
if (a.value === b.value) return 0;
|
||
return a.value < b.value ? -1 : 1;
|
||
};
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Set operations */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
// Children of `cidrStr` at `newPrefix`. `total` is exact; `subnets` stops at
|
||
// `limit` so a /8 → /32 request never materialises 16M strings.
|
||
export const splitCidr = (cidrStr, newPrefix, options = {}) => {
|
||
if (!options || typeof options !== 'object' || Array.isArray(options)) return null;
|
||
const { limit = 1024 } = options;
|
||
if (!Number.isSafeInteger(limit) || limit < 0) return null;
|
||
const cidr = parseCidr(cidrStr);
|
||
if (!cidr || !isPrefix(newPrefix, cidr.family) || newPrefix < cidr.prefix) return null;
|
||
const { family, network } = cidr;
|
||
const total = 1n << BigInt(newPrefix - cidr.prefix);
|
||
const size = addressCount(newPrefix, family);
|
||
const emit = total < BigInt(limit) ? Number(total) : limit;
|
||
const subnets = [];
|
||
for (let i = 0n; i < BigInt(emit); i += 1n) {
|
||
subnets.push(`${formatIp({ family, value: network + i * size })}/${newPrefix}`);
|
||
}
|
||
return { family, prefix: newPrefix, total, subnets, truncated: BigInt(emit) < total };
|
||
};
|
||
|
||
const isAligned = (network, prefix, family) =>
|
||
(network & (addressCount(prefix, family) - 1n)) === 0n;
|
||
|
||
// Minimal covering set per family: bare IPs become host routes, contained
|
||
// blocks are absorbed, aligned siblings merge upward (4 × /26 → one /24).
|
||
export const aggregateCidrs = (list) => {
|
||
const out = { v4: [], v6: [], invalid: [] };
|
||
if (!Array.isArray(list)) return out;
|
||
const blocks = { 4: [], 6: [] };
|
||
for (const token of list) {
|
||
const text = typeof token === 'string' ? token.trim() : '';
|
||
const cidr = parseCidr(text);
|
||
if (cidr) {
|
||
blocks[cidr.family].push({ network: cidr.network, prefix: cidr.prefix });
|
||
continue;
|
||
}
|
||
const ip = parseIp(text);
|
||
if (ip) {
|
||
blocks[ip.family].push({ network: ip.value, prefix: BITS[ip.family] });
|
||
continue;
|
||
}
|
||
out.invalid.push(token);
|
||
}
|
||
|
||
for (const family of [4, 6]) {
|
||
const sorted = blocks[family].sort((a, b) => {
|
||
if (a.network !== b.network) return a.network < b.network ? -1 : 1;
|
||
return a.prefix - b.prefix;
|
||
});
|
||
const stack = [];
|
||
for (const block of sorted) {
|
||
const top = stack[stack.length - 1];
|
||
if (top && top.prefix <= block.prefix
|
||
&& prefixContains(top.network, top.prefix, family, block.network)) continue;
|
||
stack.push(block);
|
||
while (stack.length >= 2) {
|
||
const a = stack[stack.length - 2];
|
||
const b = stack[stack.length - 1];
|
||
const size = addressCount(a.prefix, family);
|
||
if (a.prefix === 0 || a.prefix !== b.prefix || b.network !== a.network + size
|
||
|| !isAligned(a.network, a.prefix - 1, family)) break;
|
||
stack.splice(-2, 2, { network: a.network, prefix: a.prefix - 1 });
|
||
}
|
||
}
|
||
out[family === 4 ? 'v4' : 'v6'] = stack.map(
|
||
(b) => `${formatIp({ family, value: b.network })}/${b.prefix}`,
|
||
);
|
||
}
|
||
return out;
|
||
};
|
||
|
||
// Greedy cover of an inclusive range with the fewest CIDRs: at each step
|
||
// take the largest block aligned at `cur` that still fits before `end`.
|
||
export const rangeToCidrs = (startStr, endStr) => {
|
||
const start = parseIp(startStr);
|
||
const end = parseIp(endStr);
|
||
if (!start || !end || start.family !== end.family || start.value > end.value) return null;
|
||
const { family } = start;
|
||
const bits = BITS[family];
|
||
const cidrs = [];
|
||
let cur = start.value;
|
||
while (cur <= end.value) {
|
||
let prefix = 0;
|
||
for (; prefix <= bits; prefix += 1) {
|
||
const size = addressCount(prefix, family);
|
||
if (isAligned(cur, prefix, family) && cur + size - 1n <= end.value) break;
|
||
}
|
||
cidrs.push(`${formatIp({ family, value: cur })}/${prefix}`);
|
||
cur += addressCount(prefix, family);
|
||
}
|
||
return { family, cidrs };
|
||
};
|
||
|
||
export const cidrToRange = (cidrStr) => {
|
||
const info = cidrInfo(cidrStr);
|
||
if (!info) return null;
|
||
return { family: info.family, start: info.network, end: info.lastAddress };
|
||
};
|