if (typeof ip !== 'string') return null;
const octets = ip.split('.');
if (octets.length !== 4) return null;
let accumulator = 0;
for (const octet of octets) {
// Strict regex: digits only, no signs, no whitespace, no exponents.
if (!/^\d+$/.test(octet)) return null;
const value = Number(octet);
if (value < 0 || value > 255) return null;
accumulator = (accumulator * 256) + value;
}
// Coerce to uint32 immediately upon return.
return accumulator >>> 0;
}
**Rationale:** The regex `/^\d+$/` ensures each octet is a pure digit string. This prevents silent acceptance of `+5`, `-1`, or `1.5`. The final `>>> 0` ensures the accumulator is treated as unsigned, though for valid inputs the value will naturally fit within the positive range of a signed int32 until the final octet pushes it higher.
#### 2. Netmask Generation with Shift Safety
The shift modulo-32 trap requires special handling for prefix `0`. Additionally, every bitwise operation must be followed by `>>> 0` to maintain unsigned semantics.
```typescript
/**
* Computes the subnet mask for a given prefix length.
* Handles the /0 edge case where << 32 would otherwise equal << 0.
*/
export function computeNetmask(prefix: number): number {
if (prefix < 0 || prefix > 32) {
throw new Error('Prefix must be between 0 and 32');
}
// Special case: /0 requires a mask of 0x00000000.
// Without this, (0xFFFFFFFF << 32) >>> 0 evaluates to 0xFFFFFFFF due to mod-32 shift.
if (prefix === 0) return 0;
// Standard calculation: shift left by (32 - prefix), then coerce to uint32.
return (0xFFFFFFFF << (32 - prefix)) >>> 0;
}
Rationale: The prefix === 0 check is mandatory. The expression 0xFFFFFFFF << 32 results in 0xFFFFFFFF because the shift amount 32 is reduced to 0. The >>> 0 coercion ensures that even if the top bit is set, the result remains a positive uint32 representation.
3. Deriving Network Details
Network derivation involves calculating the network address, broadcast address, and usable host range. This logic must incorporate RFC 3021 for /31 and handle /32 as a single-host route.
export interface NetworkDetails {
network: number;
broadcast: number;
firstUsable: number;
lastUsable: number;
totalHosts: number;
usableHosts: number;
mask: number;
wildcard: number;
}
/**
* Derives full network details from an IP integer and prefix.
* Implements RFC 3021 for /31 and handles /32 single-host routes.
*/
export function deriveNetworkDetails(ipUint: number, prefix: number): NetworkDetails {
const mask = computeNetmask(prefix);
const network = (ipUint & mask) >>> 0;
// Wildcard mask is the bitwise inverse of the netmask.
// (~mask) produces a signed int32; >>> 0 coerces to uint32.
const wildcard = (~mask) >>> 0;
const broadcast = (network | wildcard) >>> 0;
const totalHosts = Math.pow(2, 32 - prefix);
let firstUsable: number;
let lastUsable: number;
let usableHosts: number;
if (prefix === 32) {
// /32 is a single host route. Network and broadcast are identical.
firstUsable = network;
lastUsable = network;
usableHosts = 1;
} else if (prefix === 31) {
// RFC 3021: /31 is a point-to-point link. Both addresses are usable.
// No network or broadcast reservation.
firstUsable = network;
lastUsable = broadcast;
usableHosts = 2;
} else {
// Standard subnets: reserve network and broadcast addresses.
firstUsable = (network + 1) >>> 0;
lastUsable = (broadcast - 1) >>> 0;
usableHosts = totalHosts - 2;
}
return {
network,
broadcast,
firstUsable,
lastUsable,
totalHosts,
usableHosts,
mask,
wildcard,
};
}
Rationale:
- Non-aligned Inputs: The
network calculation (ipUint & mask) automatically masks off host bits. If a user provides 10.0.5.7/24, the engine resolves it to 10.0.5.0/24. This is superior to rejecting the input, as it aligns with user intent.
- RFC 3021: The
prefix === 31 branch ensures compliance. Legacy math would calculate usableHosts = 2 - 2 = 0, which is incorrect for modern point-to-point links.
- Coercion: Every arithmetic result involving bitwise ops or boundary adjustments uses
>>> 0 to prevent negative values.
4. Safe Subdivision
Subdividing large networks can generate millions of subnets. A production engine must cap output to prevent memory exhaustion and DOM rendering failures.
export interface SubnetEntry {
ip: number;
prefix: number;
}
export interface SubdivisionResult {
subnets: SubnetEntry[];
total: number;
truncated: boolean;
}
/**
* Generates subnets by increasing the prefix length.
* Includes a limit to prevent memory/DOM explosion.
*/
export function generateSubnets(
ipUint: number,
prefix: number,
targetPrefix: number,
limit: number = 256
): SubdivisionResult {
if (targetPrefix <= prefix || targetPrefix > 32) {
throw new Error('Target prefix must be greater than current prefix and β€ 32');
}
const mask = computeNetmask(prefix);
const network = (ipUint & mask) >>> 0;
// Calculate step size. Safe because targetPrefix > prefix >= 0,
// so (32 - targetPrefix) < 32. No mod-32 trap here.
const step = 1 << (32 - targetPrefix);
const count = 1 << (targetPrefix - prefix);
const take = Math.min(count, limit);
const subnets: SubnetEntry[] = [];
for (let i = 0; i < take; i++) {
subnets.push({
ip: (network + i * step) >>> 0,
prefix: targetPrefix,
});
}
return {
subnets,
total: count,
truncated: count > take,
};
}
Rationale: The limit parameter defaults to 256, which is sufficient for most UI displays. The truncated flag informs the consumer that the list is incomplete. The step calculation is safe from the shift trap because targetPrefix is guaranteed to be at least prefix + 1, ensuring the shift amount is strictly less than 32.
Pitfall Guide
| Pitfall | Explanation | Fix |
|---|
| Signed Int32 Leakage | Bitwise ops return signed int32. 0xFFFFFFFF becomes -1. Displaying -1 as an IP breaks UI and parsers. | Append >>> 0 after every bitwise operation and arithmetic result that might touch the sign bit. |
| Shift Modulo-32 Trap | x << 32 is equivalent to x << 0. Calculating a /0 mask as 0xFFFFFFFF << 32 yields all ones instead of all zeros. | Explicitly check if (prefix === 0) return 0; before performing the shift. |
| RFC 3021 Violation | Standard math reserves network/broadcast, yielding 0 usable hosts for /31. RFC 3021 designates /31 for point-to-point links with 2 usable hosts. | Implement a specific branch for prefix === 31 that returns 2 usable hosts and no reservation. |
| Naive Number Parsing | Number(" 10") returns 10. Number("+5") returns 5. These are invalid octets but pass loose validation. | Use regex /^\d+$/ to validate octets strictly. Reject any string with whitespace, signs, or non-digits. |
| DOM Explosion | Subdividing /8 to /30 produces 4,194,304 subnets. Rendering this array crashes the browser. | Implement a limit parameter in subdivision logic. Return a truncated flag and only render the first N items. |
| Non-aligned Input Rejection | Rejecting 10.0.5.7/24 frustrates users. The intent is usually the containing network. | Apply the mask to the input IP: network = ip & mask. This normalizes non-aligned inputs automatically. |
| Wildcard Mask Sign Flip | ~mask inverts bits but produces a signed int32. For /24, ~0xFFFFFF00 becomes 0x000000FF as signed, but for masks with high bits, it flips sign. | Coerce wildcard: wildcard = (~mask) >>> 0. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Browser Dashboard | Custom Engine (Client-side) | Zero latency; no server dependency; full control over RFC compliance. | Low dev cost; high reliability. |
| Node.js CLI Tool | Custom Engine or ip-address lib | Custom engine avoids dependency bloat; lib may have legacy /31 bugs. | Custom: Low cost. Lib: Verify RFC compliance. |
| High-Volume API | Custom Engine (Optimized) | Avoids library overhead; allows caching of mask calculations. | Minimal CPU overhead; scales linearly. |
| Legacy System Integration | Custom Engine with Compatibility Mode | Force /31 to 0 usable hosts if downstream systems reject RFC 3021. | Adds configuration complexity. |
| Educational Tool | Custom Engine with Debug Flags | Expose bitwise steps and coercion for teaching purposes. | Higher dev cost for debug UI. |
Configuration Template
The following template provides a complete, copy-pasteable module for a robust CIDR engine.
// cidr-engine.ts
export type IpUint = number;
export interface NetworkDetails {
network: IpUint;
broadcast: IpUint;
firstUsable: IpUint;
lastUsable: IpUint;
totalHosts: number;
usableHosts: number;
mask: IpUint;
wildcard: IpUint;
}
export interface SubnetEntry {
ip: IpUint;
prefix: number;
}
export interface SubdivisionResult {
subnets: SubnetEntry[];
total: number;
truncated: boolean;
}
export function parseQuadToUint(ip: string): IpUint | null {
if (typeof ip !== 'string') return null;
const octets = ip.split('.');
if (octets.length !== 4) return null;
let acc = 0;
for (const octet of octets) {
if (!/^\d+$/.test(octet)) return null;
const val = Number(octet);
if (val < 0 || val > 255) return null;
acc = (acc * 256) + val;
}
return acc >>> 0;
}
export function computeNetmask(prefix: number): IpUint {
if (prefix < 0 || prefix > 32) throw new Error('Invalid prefix');
if (prefix === 0) return 0;
return (0xFFFFFFFF << (32 - prefix)) >>> 0;
}
export function deriveNetworkDetails(ipUint: IpUint, prefix: number): NetworkDetails {
const mask = computeNetmask(prefix);
const network = (ipUint & mask) >>> 0;
const wildcard = (~mask) >>> 0;
const broadcast = (network | wildcard) >>> 0;
const totalHosts = Math.pow(2, 32 - prefix);
let firstUsable: IpUint;
let lastUsable: IpUint;
let usableHosts: number;
if (prefix === 32) {
firstUsable = network;
lastUsable = network;
usableHosts = 1;
} else if (prefix === 31) {
firstUsable = network;
lastUsable = broadcast;
usableHosts = 2;
} else {
firstUsable = (network + 1) >>> 0;
lastUsable = (broadcast - 1) >>> 0;
usableHosts = totalHosts - 2;
}
return { network, broadcast, firstUsable, lastUsable, totalHosts, usableHosts, mask, wildcard };
}
export function generateSubnets(
ipUint: IpUint,
prefix: number,
targetPrefix: number,
limit: number = 256
): SubdivisionResult {
if (targetPrefix <= prefix || targetPrefix > 32) {
throw new Error('Invalid target prefix');
}
const mask = computeNetmask(prefix);
const network = (ipUint & mask) >>> 0;
const step = 1 << (32 - targetPrefix);
const count = 1 << (targetPrefix - prefix);
const take = Math.min(count, limit);
const subnets: SubnetEntry[] = [];
for (let i = 0; i < take; i++) {
subnets.push({ ip: (network + i * step) >>> 0, prefix: targetPrefix });
}
return { subnets, total: count, truncated: count > take };
}
Quick Start Guide
- Import the Module: Copy
cidr-engine.ts into your project or install via your package manager if published.
- Parse Input: Call
parseQuadToUint("192.168.1.10") to get the integer representation. Handle null for invalid inputs.
- Derive Details: Pass the integer and prefix to
deriveNetworkDetails(ipUint, 24) to get network, broadcast, and usable ranges.
- Subdivide Safely: Use
generateSubnets(ipUint, 16, 24) to get subnets. Check result.truncated to handle large outputs gracefully.
- Render: Convert integers back to dotted-quad strings using
(ip >> 24 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >> 8 & 255) + '.' + (ip & 255) for display. Ensure all intermediate values remain coerced with >>> 0.