// --- CIDR Calculation Core Engine --- /** * Converts IP dotted decimal string to 32-bit unsigned integer. */ function ipToInt(ip) { return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0; } /** * Converts 32-bit unsigned integer to IP dotted decimal string. */ function intToIp(int) { return [ (int >>> 24) & 255, (int >>> 16) & 255, (int >>> 8) & 255, int & 255 ].join('.'); } /** * Checks if the IP is in private ranges: * - 10.0.0.0/8 * - 172.16.0.0/12 * - 192.168.0.0/16 * - 127.0.0.0/8 (Loopback is technically not private in the RFC 1918 sense, but we can treat as private/internal) */ function checkPrivate(ipInt) { const o1 = (ipInt >>> 24) & 255; const o2 = (ipInt >>> 16) & 255; if (o1 === 10) return true; if (o1 === 172 && o2 >= 16 && o2 <= 31) return true; if (o1 === 192 && o2 === 168) return true; if (o1 === 127) return true; return false; } /** * Determines class of IP based on first octet. */ function getIpClass(ipInt) { const o1 = (ipInt >>> 24) & 255; if (o1 >= 0 && o1 <= 127) return 'Class A'; if (o1 >= 128 && o1 <= 191) return 'Class B'; if (o1 >= 192 && o1 <= 223) return 'Class C'; if (o1 >= 224 && o1 <= 239) return 'Class D (Multicast)'; return 'Class E (Experimental)'; } /** * Parses and validates an IPv4 CIDR string. */ function parseCIDR(cidrStr) { const clean = cidrStr.trim(); const parts = clean.split('/'); if (parts.length > 2) return null; const ipPart = parts[0]; const prefixPart = parts[1]; // Validate IP format const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; const match = ipPart.match(ipRegex); if (!match) return null; for (let i = 1; i <= 4; i++) { const val = parseInt(match[i], 10); if (val < 0 || val > 255) return null; // Prevent leading zero issues like 192.168.01.1 if (match[i].length > 1 && match[i].startsWith('0')) return null; } let prefix = 24; if (prefixPart !== undefined) { if (!/^\d+$/.test(prefixPart)) return null; prefix = parseInt(prefixPart, 10); if (prefix < 0 || prefix > 32) return null; } return { ip: ipPart, prefix }; } /** * Runs calculations on valid input. */ function calculateCIDR(ip, prefix) { const ipVal = ipToInt(ip); const maskVal = (prefix === 0 ? 0 : (~0 << (32 - prefix))) >>> 0; const networkVal = (ipVal & maskVal) >>> 0; const broadcastVal = (networkVal | ~maskVal) >>> 0; let usableHosts = 0; let firstHostVal = 0; let lastHostVal = 0; if (prefix === 32) { usableHosts = 1; firstHostVal = networkVal; lastHostVal = networkVal; } else if (prefix === 31) { usableHosts = 2; // RFC 3021 firstHostVal = networkVal; lastHostVal = broadcastVal; } else { usableHosts = Math.max(0, (broadcastVal - networkVal) - 1); firstHostVal = networkVal + 1; lastHostVal = broadcastVal - 1; } const wildcardVal = (~maskVal) >>> 0; return { cidrNotation: `${intToIp(networkVal)}/${prefix}`, networkAddress: intToIp(networkVal), broadcastAddress: intToIp(broadcastVal), subnetMask: intToIp(maskVal), wildcardMask: intToIp(wildcardVal), usableHosts: usableHosts.toLocaleString(), hostRange: usableHosts > 0 ? `${intToIp(firstHostVal)} – ${intToIp(lastHostVal)}` : 'N/A', ipClass: getIpClass(ipVal), isPrivate: checkPrivate(ipVal) ? 'Private (RFC 1918)' : 'Public' }; } // --- UI and Controller Layer --- document.addEventListener('DOMContentLoaded', () => { const cidrInput = document.getElementById('cidr-input'); const prefixDropdown = document.getElementById('prefix-dropdown'); const errorMsg = document.getElementById('error-msg'); const resultsContainer = document.getElementById('results'); // Value DOM fields const outCidr = document.getElementById('out-cidr'); const outNetwork = document.getElementById('out-network'); const outBroadcast = document.getElementById('out-broadcast'); const outMask = document.getElementById('out-mask'); const outRange = document.getElementById('out-range'); const outHosts = document.getElementById('out-hosts'); const outClass = document.getElementById('out-class'); const outType = document.getElementById('out-type'); // Setup dropdown options (0 to 32) for (let i = 0; i <= 32; i++) { const opt = document.createElement('option'); opt.value = i; opt.textContent = `/${i}`; if (i === 24) opt.selected = true; prefixDropdown.appendChild(opt); } // Live calculation function function update() { let rawVal = cidrInput.value.trim(); if (!rawVal) { clearResults(); hideError(); return; } // Auto-sync prefix dropdown if typed/pasted with a slash, then clean the input field to show only the IP if (rawVal.includes('/')) { const parts = rawVal.split('/'); const ipPart = parts[0].trim(); const prefixPart = parts[1].trim(); if (/^\d+$/.test(prefixPart)) { const prefixVal = parseInt(prefixPart, 10); if (prefixVal >= 0 && prefixVal <= 32) { prefixDropdown.value = prefixVal; } } cidrInput.value = ipPart; rawVal = ipPart; } // Validate IP format const ipRegex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; const match = rawVal.match(ipRegex); let isValidIp = true; if (!match) { isValidIp = false; } else { for (let i = 1; i <= 4; i++) { const val = parseInt(match[i], 10); if (val < 0 || val > 255) isValidIp = false; if (match[i].length > 1 && match[i].startsWith('0')) isValidIp = false; } } if (!isValidIp) { showError('Invalid IPv4 address. Use format: e.g. 192.168.0.1'); clearResults(); return; } hideError(); const prefix = parseInt(prefixDropdown.value, 10); const results = calculateCIDR(rawVal, prefix); displayResults(results); } function displayResults(data) { outCidr.textContent = data.cidrNotation; outNetwork.textContent = data.networkAddress; outBroadcast.textContent = data.broadcastAddress; outMask.textContent = data.subnetMask; outRange.textContent = data.hostRange; outHosts.textContent = data.usableHosts; outClass.textContent = data.ipClass; outType.textContent = data.isPrivate; resultsContainer.classList.add('visible'); } function clearResults() { resultsContainer.classList.remove('visible'); } function showError(msg) { cidrInput.classList.add('input-error'); errorMsg.textContent = msg; errorMsg.classList.add('visible'); } function hideError() { cidrInput.classList.remove('input-error'); errorMsg.classList.remove('visible'); } // Listeners cidrInput.addEventListener('input', update); prefixDropdown.addEventListener('change', update); // Copy to Clipboard document.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.getAttribute('data-target'); const targetEl = document.getElementById(targetId); if (!targetEl) return; const text = targetEl.textContent; navigator.clipboard.writeText(text).then(() => { btn.classList.add('copied'); setTimeout(() => { btn.classList.remove('copied'); }, 1500); }); }); }); // --- Theme Toggle FAB Controller --- const themeToggle = document.getElementById('theme-toggle'); const themeIcon = document.getElementById('theme-icon'); const themes = ['system', 'light', 'dark']; let currentThemeIndex = 0; // Default to 'system' function applyTheme(theme) { document.documentElement.removeAttribute('data-theme'); if (theme === 'system') { const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); themeIcon.innerHTML = ``; // Monitor icon } else if (theme === 'light') { document.documentElement.setAttribute('data-theme', 'light'); themeIcon.innerHTML = ``; // Sun icon } else if (theme === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); themeIcon.innerHTML = ``; // Moon icon } } // Handle system preference change dynamically if system theme is selected window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (themes[currentThemeIndex] === 'system') { applyTheme('system'); } }); themeToggle.addEventListener('click', () => { currentThemeIndex = (currentThemeIndex + 1) % themes.length; applyTheme(themes[currentThemeIndex]); }); // Init theme and calculation applyTheme('system'); update(); }); // --- PWA Service Worker Registration --- if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./sw.js') .then(reg => console.log('Service worker registered successfully', reg.scope)) .catch(err => console.error('Service worker registration failed', err)); }); }