// A live prefix calculator on a slide. Any
// with and gets its // .px-bits, .px-range and .px-facts filled in as the reader types. (function () { const ALL = (1n << 128n) - 1n; const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]); // "2001:db8::1" -> BigInt, or null. function parse(text) { const s = text.trim().toLowerCase(); if (!/^[0-9a-f:]+$/.test(s) || s.split("::").length > 2) return null; const [left, right = ""] = s.split("::"); const l = left ? left.split(":") : [], r = right ? right.split(":") : []; if (s.includes("::") ? l.length + r.length > 7 : l.length !== 8) return null; const groups = [...l, ...Array(8 - l.length - r.length).fill("0"), ...r]; let v = 0n; for (const g of groups) { if (!/^[0-9a-f]{1,4}$/.test(g)) return null; v = (v << 16n) | BigInt(parseInt(g, 16)); } return v; } function groupsOf(v) { const g = []; for (let i = 7; i >= 0; i--) g.push(Number((v >> BigInt(i * 16)) & 0xffffn)); return g; } // Shortest form, RFC 5952 style. function format(v) { const g = groupsOf(v).map((x) => x.toString(16)); let best = -1, bestLen = 0; for (let i = 0; i < 8; i++) { let n = 0; while (i + n < 8 && g[i + n] === "0") n++; if (n > bestLen && n > 1) { best = i; bestLen = n; } } if (best < 0) return g.join(":"); return g.slice(0, best).join(":") + "::" + g.slice(best + bestLen).join(":"); } const sup = (k) => String(k).replace(/\d/g, (d) => "⁰¹²³⁴⁵⁶⁷⁸⁹"[d]); // 2^k as a number people can read: exact up to 2^50, else a power. const pow2 = (k) => (k <= 50 ? Number(1n << BigInt(k)).toLocaleString("en") : `2${sup(k)}`); function render(form) { const input = form.elements.prefix, slider = form.elements.len; const bits = form.querySelector(".px-bits"), range = form.querySelector(".px-range"), facts = form.querySelector(".px-facts"); let [addrText, lenText] = input.value.split("/"); let len = lenText === undefined ? Number(slider.value) : parseInt(lenText, 10); const addr = parse(addrText || ""); if (addr === null || !(len >= 0 && len <= 128)) { bits.innerHTML = 'not an IPv6 prefix: write address/length, e.g. 2001:db8::/32'; range.innerHTML = ""; facts.textContent = ""; return; } slider.value = len; const host = 128 - len; const first = addr & ((ALL << BigInt(host)) & ALL); const last = first | ((1n << BigInt(host)) - 1n); // The 32 hex digits with the fixed part in the accent colour; a nibble // that straddles the boundary is shown in binary. const hex = groupsOf(first).map((x) => x.toString(16).padStart(4, "0")).join(""); const fullNibbles = Math.floor(len / 4), partBits = len % 4; const withColons = (s, offset) => s.replace(/./g, (c, i) => ((offset + i) % 4 === 3 && offset + i < 31 ? c + ":" : c)); let html = `${withColons(hex.slice(0, fullNibbles), 0)}`; let rest = fullNibbles; if (partBits) { const nib = parseInt(hex[fullNibbles], 16).toString(2).padStart(4, "0"); // One hex digit, four bits, with the boundary inside it. html += `${nib.slice(0, partBits)}${"x".repeat(4 - partBits)}`; rest++; } html += `${withColons("x".repeat(32 - rest), rest)}`; bits.innerHTML = html; // Every shorter prefix contains this one; show three of them, at byte // boundaries, with the range's position and share marked in each, so // that no single one looks like "the" parent. Then the range itself. const levels = []; for (let pl = len === 0 ? -1 : 8 * Math.floor((len - 1) / 8); pl >= 0 && levels.length < 3; pl -= 8) { const slots = 1n << BigInt(len - pl); const index = (first >> BigInt(host)) & (slots - 1n); const enclosing = first & ((ALL << BigInt(128 - pl)) & ALL); // Position and width as fractions; the slice is kept visible. const left = Number((index * 100000n) / slots) / 1000, width = Math.max(100 / Number(slots), 0.3); levels.push(`
${esc(format(enclosing))}/${pl} 1 of ${pow2(len - pl)}
`); } range.innerHTML = `
${levels.length ? `
every shorter prefix contains it; three of them:
${levels.join("")}` : '
nothing is shorter than /0: this is every address there is
'}
${esc(format(first))}${pow2(host)} addresses${esc(format(last))}
`; const parts = [`The first ${len} bits are fixed; the other ${host} are for use within the network.`]; if (len < 64) parts.push(`That is ${pow2(64 - len)} /64s, one per LAN.`); else if (len === 64) parts.push(`Exactly one LAN.`); else parts.push(`Smaller than a LAN: ${host === 0 ? "a single host" : `part of one /64`}.`); if (len < 48) parts.push(`Or ${pow2(48 - len)} /48s, one per site.`); facts.innerHTML = parts.join(" "); } document.addEventListener("input", (e) => { const form = e.target.closest && e.target.closest("form.prefix-explorer"); if (!form) return; if (e.target.name === "len") { // The slider rewrites the length in the text field. const addr = form.elements.prefix.value.split("/")[0]; form.elements.prefix.value = `${addr}/${e.target.value}`; } render(form); if (window.deck) window.deck.layout(); }); document.addEventListener("submit", (e) => { if (e.target.closest && e.target.closest("form.prefix-explorer")) e.preventDefault(); }); // Fill in the initial values once the slides exist. const init = () => document.querySelectorAll("form.prefix-explorer").forEach(render); window.addEventListener("deck-ready", init); if (document.readyState === "complete") setTimeout(init, 500); })();