Files
ferrix/slides/prefix-explorer.js
T

124 lines
6.3 KiB
JavaScript

// A live prefix calculator on a slide. Any <form class="prefix-explorer">
// with <input name="prefix"> and <input type="range" name="len"> 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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 = '<span class="px-bad">not an IPv6 prefix: write address/length, e.g. 2001:db8::/32</span>';
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 = `<span class="fixed">${withColons(hex.slice(0, fullNibbles), 0)}</span>`;
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 += `<span class="part" title="this hex digit straddles the boundary, shown as its four bits">${nib.slice(0, partBits)}<i>${"x".repeat(4 - partBits)}</i></span>`;
rest++;
}
html += `<span class="free">${withColons("x".repeat(32 - rest), rest)}</span>`;
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(`<div class="px-level"><span class="px-level-name">${esc(format(enclosing))}/${pl}</span>
<span class="px-track"><i style="left:${left}%;width:${width}%"></i></span>
<span class="px-level-count">1 of ${pow2(len - pl)}</span></div>`);
}
range.innerHTML = `
<div class="px-levels">${levels.length ? `<div class="px-levels-title">every shorter prefix contains it; three of them:</div>${levels.join("")}` : '<div class="px-levels-title">nothing is shorter than /0: this is every address there is</div>'}</div>
<div class="px-bar"><span class="px-end px-first">${esc(format(first))}</span><span class="px-mid">${pow2(host)} addresses</span><span class="px-end px-last">${esc(format(last))}</span></div>`;
const parts = [`The first <b>${len}</b> bits are fixed; the other ${host} are for use within the network.`];
if (len < 64) parts.push(`That is <b>${pow2(64 - len)}</b> /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);
})();