diff --git a/script.js b/script.js index 1485e9f..26e8293 100644 --- a/script.js +++ b/script.js @@ -2973,52 +2973,100 @@ let ipamData = []; let ipamExpandedIds = new Set(); let ipamDrillStack = []; // [{id, label}] breadcrumb -// --- IP-laskenta-apufunktiot --- -function ipToInt(ip) { - return ip.split('.').reduce((acc, oct) => (acc << 8) + parseInt(oct), 0) >>> 0; +// --- IP-laskenta-apufunktiot (IPv4 + IPv6) --- +function ipv4ToBI(ip) { + return ip.split('.').reduce((acc, oct) => (acc << 8n) + BigInt(parseInt(oct)), 0n); } -function prefixToMask(prefix) { - return prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0; +function ipv6ToBI(ip) { + // Expand :: shorthand + let parts = ip.split('::'); + let left = parts[0] ? parts[0].split(':') : []; + let right = parts.length > 1 && parts[1] ? parts[1].split(':') : []; + const missing = 8 - left.length - right.length; + const full = [...left, ...Array(missing).fill('0'), ...right]; + return full.reduce((acc, hex) => (acc << 16n) + BigInt(parseInt(hex || '0', 16)), 0n); } function parseNetwork(verkko) { if (!verkko) return null; const v = verkko.trim(); + let ip, prefix; if (v.includes('/')) { - const [ip, pfx] = v.split('/'); - const prefix = parseInt(pfx); - if (isNaN(prefix) || prefix < 0 || prefix > 32) return null; - const parts = ip.split('.'); - if (parts.length !== 4) return null; - return { net: ipToInt(ip), prefix }; + const slash = v.lastIndexOf('/'); + ip = v.substring(0, slash); + prefix = parseInt(v.substring(slash + 1)); + if (isNaN(prefix) || prefix < 0) return null; } else { - const parts = v.split('.'); - if (parts.length !== 4) return null; - return { net: ipToInt(v), prefix: 32 }; + ip = v; + prefix = null; // auto-detect } + // IPv6? + if (ip.includes(':')) { + const maxBits = 128; + if (prefix === null) prefix = 128; + if (prefix > maxBits) return null; + try { + return { net: ipv6ToBI(ip), prefix, bits: maxBits, v6: true }; + } catch { return null; } + } + // IPv4 + const parts = ip.split('.'); + if (parts.length !== 4) return null; + const maxBits = 32; + if (prefix === null) prefix = 32; + if (prefix > maxBits) return null; + return { net: ipv4ToBI(ip), prefix, bits: maxBits, v6: false }; } -function isSubnetOf(childNet, childPrefix, parentNet, parentPrefix) { +function isSubnetOf(childNet, childPrefix, childBits, parentNet, parentPrefix, parentBits) { + if (childBits !== parentBits) return false; // eri perhe (v4 vs v6) if (childPrefix <= parentPrefix) return false; - const mask = prefixToMask(parentPrefix); - return (childNet & mask) === (parentNet & mask); + const shift = BigInt(parentBits - parentPrefix); + return (childNet >> shift) === (parentNet >> shift); +} +// Laske subnetin käyttöaste: kuinka monta lasta (direct children) vs kapasiteetti +function subnetUsageHtml(node) { + if (node.entry.tyyppi !== 'subnet' || node.children.length === 0) return ''; + const childCount = node.children.length; + // Laske kuinka monta "slottia" tässä subnetissa on seuraavalla tasolla + // Etsi yleisin lapsi-prefix + const childPrefixes = node.children.filter(c => c.prefix > node.prefix).map(c => c.prefix); + if (childPrefixes.length === 0) return `${childCount}`; + // Käytä pienintä child-prefixiä (isoimpia aliverkkoja) kapasiteetin laskuun + const commonPrefix = Math.min(...childPrefixes); + const bits = node.entry.tyyppi === 'subnet' ? (node.children[0]?.bits || 32) : 32; + const slotBits = commonPrefix - node.prefix; + if (slotBits <= 0 || slotBits > 20) return `${childCount}`; + const totalSlots = 1 << slotBits; // 2^slotBits + const sameLevel = node.children.filter(c => c.prefix === commonPrefix).length; + const freeSlots = totalSlots - sameLevel; + return `${sameLevel}/${totalSlots}`; } // --- Puurakenne --- function buildIpamTree(entries) { - // Parsitaan verkko-osoitteet ja suodatetaan validit - const items = entries.map(e => { + // Parsitaan verkko-osoitteet; ei-parsittavat lisätään juureen sellaisenaan + const unparsed = []; + const items = []; + for (const e of entries) { const parsed = parseNetwork(e.verkko); - return parsed ? { entry: e, net: parsed.net, prefix: parsed.prefix, children: [] } : null; - }).filter(Boolean); - // Järjestetään: pienin prefix (isoin verkko) ensin, sitten IP-osoitteen mukaan - items.sort((a, b) => a.prefix - b.prefix || a.net - b.net); + if (parsed) { + items.push({ entry: e, net: parsed.net, prefix: parsed.prefix, bits: parsed.bits, v6: parsed.v6, children: [] }); + } else { + unparsed.push({ entry: e, net: 0n, prefix: 0, bits: 0, v6: false, children: [] }); + } + } + // Järjestetään: v4 ennen v6, pienin prefix ensin, sitten osoitteen mukaan + items.sort((a, b) => { + if (a.v6 !== b.v6) return a.v6 ? 1 : -1; + if (a.prefix !== b.prefix) return a.prefix - b.prefix; + return a.net < b.net ? -1 : a.net > b.net ? 1 : 0; + }); const roots = []; for (const item of items) { - let placed = false; // Etsi lähin parent (suurin prefix joka sisältää tämän) const findParent = (nodes) => { for (let i = nodes.length - 1; i >= 0; i--) { const node = nodes[i]; - if (isSubnetOf(item.net, item.prefix, node.net, node.prefix)) { + if (isSubnetOf(item.net, item.prefix, item.bits, node.net, node.prefix, node.bits)) { // Tarkista ensin onko jokin lapsi tarkempi parent if (!findParent(node.children)) { node.children.push(item); @@ -3032,6 +3080,8 @@ function buildIpamTree(entries) { roots.push(item); } } + // Lisää ei-parsittavat (esim. virheelliset osoitteet) juureen + roots.push(...unparsed); return roots; } @@ -3057,7 +3107,7 @@ function flattenTree(nodes, depth, drillId) { for (const node of list) { const hasChildren = node.children.length > 0; const expanded = ipamExpandedIds.has(node.entry.id); - rows.push({ entry: node.entry, depth: d, hasChildren, expanded }); + rows.push({ entry: node.entry, depth: d, hasChildren, expanded, node }); if (hasChildren && expanded) { render(node.children, d + 1); } @@ -3140,7 +3190,7 @@ function renderIpam() {
${esc(e.verkko || '-')}${drillBtn}${esc(e.verkko || '-')}${drillBtn} ${subnetUsageHtml(r.node)}