theseus/bundled-addons/aegis/qr.js
Local Dev 2e54bf5e5a Ship Theseus 0.3.28 5d15508b (Aegis update card + DevTools in tab sidebar + real favicons)
Setup    5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643
Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4

Bundled since 0.3.27:

32d4b75 - Aegis (bchwallet) gains its own update card in Settings >
General beside Ariadne. Check for updates hits the same signed OTA
endpoint the boot timer uses; Restart to apply appears when a signed
newer version is staged. Uses the existing addons-check-updates + a
new app-restart IPC. New Aegis versions ship without a Theseus release.

32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to
the right of the tab (mode: 'right') instead of a detached window.
Matches stock Chrome. Users who prefer detached can drag out via the
DevTools own toolbar.

b71c925 - Search-engine favicons in Settings > Search now use Google's
/s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough
hosts (Brave, Bing, Yandex, etc.) that half the list was falling
through to the emoji placeholder.

Deployed. Verified LIVE 0.3.28.
2026-09-08 18:17:25 +02:00

218 lines
9.8 KiB
JavaScript

// Minimal QR encoder for the receive tab: byte mode, versions 1-10, error
// correction M (falls back to L when M won't fit). Returns a boolean matrix.
// Loaded as a plain script in panel.html and as a CommonJS module in tests.
(function (root) {
const EC_LEVELS = { L: 1, M: 0 };
// Per version: [totalCodewords, {L:[ecPerBlock, [[blocks, dataCw], ...]], M:[...]}]
const TABLE = {
1: [26, { L: [7, [[1, 19]]], M: [10, [[1, 16]]] }],
2: [44, { L: [10, [[1, 34]]], M: [16, [[1, 28]]] }],
3: [70, { L: [15, [[1, 55]]], M: [26, [[1, 44]]] }],
4: [100, { L: [20, [[1, 80]]], M: [18, [[2, 32]]] }],
5: [134, { L: [26, [[1, 108]]], M: [24, [[2, 43]]] }],
6: [172, { L: [18, [[2, 68]]], M: [16, [[4, 27]]] }],
7: [196, { L: [20, [[2, 78]]], M: [18, [[4, 31]]] }],
8: [242, { L: [24, [[2, 97]]], M: [22, [[2, 38], [2, 39]]] }],
9: [292, { L: [30, [[2, 116]]], M: [22, [[3, 36], [2, 37]]] }],
10: [346, { L: [18, [[2, 68], [2, 69]]], M: [26, [[4, 43], [1, 44]]] }],
};
const ALIGN = { 1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30], 6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50] };
const REMAINDER = { 1: 0, 2: 7, 3: 7, 4: 7, 5: 7, 6: 7, 7: 0, 8: 0, 9: 0, 10: 0 };
// GF(256) with the QR polynomial 0x11d.
const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
(function () {
let x = 1;
for (let i = 0; i < 255; i++) { EXP[i] = x; LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x11d; }
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
})();
const gmul = (a, b) => (a && b) ? EXP[LOG[a] + LOG[b]] : 0;
function generator(n) {
let g = [1];
for (let i = 0; i < n; i++) {
const next = new Array(g.length + 1).fill(0);
for (let j = 0; j < g.length; j++) { next[j] ^= g[j]; next[j + 1] ^= gmul(g[j], EXP[i]); }
g = next;
}
return g;
}
function rsEncode(data, nEc) {
const g = generator(nEc);
const out = new Uint8Array(data.length + nEc);
out.set(data);
for (let i = 0; i < data.length; i++) {
const c = out[i];
if (c) for (let j = 1; j < g.length; j++) out[i + j] ^= gmul(g[j], c);
}
return out.slice(data.length);
}
function dataCapacity(v, ec) { return TABLE[v][1][ec][1].reduce((a, [b, d]) => a + b * d, 0); }
function pickVersion(len) {
for (const ec of ["M", "L"]) {
for (let v = 1; v <= 10; v++) {
const bitsNeeded = 4 + (v <= 9 ? 8 : 16) + len * 8;
if (bitsNeeded <= dataCapacity(v, ec) * 8) return { v, ec };
}
}
throw new Error("qr: text too long");
}
function encodeData(bytes, v, ec) {
const cap = dataCapacity(v, ec);
const bits = [];
const push = (val, n) => { for (let i = n - 1; i >= 0; i--) bits.push((val >> i) & 1); };
push(0b0100, 4);
push(bytes.length, v <= 9 ? 8 : 16);
for (const b of bytes) push(b, 8);
for (let i = 0; i < 4 && bits.length < cap * 8; i++) bits.push(0);
while (bits.length % 8) bits.push(0);
const data = new Uint8Array(cap);
for (let i = 0; i < bits.length / 8; i++) data[i] = parseInt(bits.slice(i * 8, i * 8 + 8).join(""), 2);
for (let i = bits.length / 8, k = 0; i < cap; i++, k++) data[i] = k % 2 ? 0x11 : 0xec;
return data;
}
function interleave(data, v, ec) {
const [nEc, groups] = TABLE[v][1][ec];
const blocks = []; let o = 0;
for (const [count, size] of groups) for (let i = 0; i < count; i++) { blocks.push(data.slice(o, o + size)); o += size; }
const ecBlocks = blocks.map((b) => rsEncode(b, nEc));
const out = [];
const maxLen = Math.max(...blocks.map((b) => b.length));
for (let i = 0; i < maxLen; i++) for (const b of blocks) if (i < b.length) out.push(b[i]);
for (let i = 0; i < nEc; i++) for (const b of ecBlocks) out.push(b[i]);
return out;
}
// Remainder of value·x^degree modulo the generator poly (BCH error correction
// for the format / version fields).
function bch(value, poly, degree) {
let v = value << degree;
for (let i = 31 - Math.clz32(v); i >= degree; i--) if ((v >> i) & 1) v ^= poly << (i - degree);
return v;
}
const formatBits = (ec, mask) => { const d = (EC_LEVELS[ec] << 3) | mask; return ((d << 10) | bch(d, 0x537, 10)) ^ 0x5412; };
const versionBits = (v) => (v << 12) | bch(v, 0x1f25, 12);
function build(text) {
const bytes = typeof text === "string" ? new TextEncoder().encode(text) : Uint8Array.from(text);
const { v, ec } = pickVersion(bytes.length);
const size = v * 4 + 17;
const codewords = interleave(encodeData(bytes, v, ec), v, ec);
const grid = Array.from({ length: size }, () => new Uint8Array(size)); // 1 dark, 0 light
const fixed = Array.from({ length: size }, () => new Uint8Array(size)); // function pattern / reserved
const set = (r, c, val) => { grid[r][c] = val ? 1 : 0; fixed[r][c] = 1; };
const finder = (r0, c0) => {
for (let r = -1; r <= 7; r++) for (let c = -1; c <= 7; c++) {
const rr = r0 + r, cc = c0 + c;
if (rr < 0 || cc < 0 || rr >= size || cc >= size) continue;
const inner = r >= 0 && r <= 6 && c >= 0 && c <= 6;
const dark = inner && (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4));
set(rr, cc, dark);
}
};
finder(0, 0); finder(0, size - 7); finder(size - 7, 0);
for (let i = 8; i < size - 8; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
const al = ALIGN[v], last = al.length - 1;
al.forEach((r, ri) => al.forEach((c, ci) => {
// The three corners that would sit on a finder pattern are omitted.
if ((ri === 0 && ci === 0) || (ri === 0 && ci === last) || (ri === last && ci === 0)) return;
for (let dr = -2; dr <= 2; dr++) for (let dc = -2; dc <= 2; dc++) {
set(r + dr, c + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1);
}
}));
set(size - 8, 8, 1); // dark module
// Reserve format areas (filled per mask below) and version areas.
for (let i = 0; i < 9; i++) { fixed[8][i] = 1; fixed[i][8] = 1; }
for (let i = 0; i < 8; i++) { fixed[8][size - 1 - i] = 1; fixed[size - 1 - i][8] = 1; }
if (v >= 7) {
const vb = versionBits(v);
for (let i = 0; i < 18; i++) {
const bit = (vb >> i) & 1;
set(Math.floor(i / 3), size - 11 + (i % 3), bit);
set(size - 11 + (i % 3), Math.floor(i / 3), bit);
}
}
// Zigzag data placement.
const bits = [];
for (const cw of codewords) for (let i = 7; i >= 0; i--) bits.push((cw >> i) & 1);
for (let i = 0; i < REMAINDER[v]; i++) bits.push(0);
let bi = 0, up = true;
for (let col = size - 1; col > 0; col -= 2) {
if (col === 6) col--;
for (let k = 0; k < size; k++) {
const r = up ? size - 1 - k : k;
for (const c of [col, col - 1]) {
if (fixed[r][c]) continue;
grid[r][c] = bi < bits.length ? bits[bi] : 0;
bi++;
}
}
up = !up;
}
// Try every mask, keep the lowest penalty.
const MASKS = [
(i, j) => (i + j) % 2 === 0, (i) => i % 2 === 0, (_i, j) => j % 3 === 0, (i, j) => (i + j) % 3 === 0,
(i, j) => (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0, (i, j) => ((i * j) % 2) + ((i * j) % 3) === 0,
(i, j) => (((i * j) % 2) + ((i * j) % 3)) % 2 === 0, (i, j) => (((i + j) % 2) + ((i * j) % 3)) % 2 === 0,
];
let best = null;
for (let m = 0; m < 8; m++) {
const g = grid.map((row) => Uint8Array.from(row));
for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) if (!fixed[r][c] && MASKS[m](r, c)) g[r][c] ^= 1;
const fb = formatBits(ec, m);
for (let i = 0; i < 15; i++) {
const bit = (fb >> i) & 1;
// Copy 1: down column 8 (bits 0-7, skipping the timing row), then
// left along row 8 (bits 8-14). Copy 2: right end of row 8, bottom
// of column 8.
if (i < 6) g[i][8] = bit; else if (i === 6) g[7][8] = bit; else if (i === 7) g[8][8] = bit;
else if (i === 8) g[8][7] = bit; else g[8][14 - i] = bit;
if (i < 8) g[8][size - 1 - i] = bit; else g[size - 15 + i][8] = bit;
}
const p = penalty(g, size);
if (!best || p < best.p) best = { g, p, m };
}
return { size, version: v, ec, mask: best.m, modules: best.g.map((row) => Array.from(row, (x) => !!x)) };
}
function penalty(g, n) {
let p = 0;
const runs = (get) => {
for (let a = 0; a < n; a++) {
let run = 1;
for (let b = 1; b <= n; b++) {
if (b < n && get(a, b) === get(a, b - 1)) run++;
else { if (run >= 5) p += 3 + run - 5; run = 1; }
}
}
};
runs((a, b) => g[a][b]); runs((a, b) => g[b][a]);
for (let r = 0; r < n - 1; r++) for (let c = 0; c < n - 1; c++) {
const s = g[r][c] + g[r][c + 1] + g[r + 1][c] + g[r + 1][c + 1];
if (s === 0 || s === 4) p += 3;
}
const pat = [1, 0, 1, 1, 1, 0, 1];
const finderLike = (get, a) => {
for (let b = 0; b <= n - 7; b++) {
let ok = true;
for (let k = 0; k < 7; k++) if (get(a, b + k) !== pat[k]) { ok = false; break; }
if (!ok) continue;
const before = b >= 4 && [0, 1, 2, 3].every((k) => get(a, b - 1 - k) === 0);
const after = b + 10 < n && [0, 1, 2, 3].every((k) => get(a, b + 7 + k) === 0);
if (before || after) p += 40;
}
};
for (let a = 0; a < n; a++) { finderLike((x, y) => g[x][y], a); finderLike((x, y) => g[y][x], a); }
let dark = 0;
for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) dark += g[r][c];
const pct = (dark * 100) / (n * n);
p += Math.floor(Math.abs(pct - 50) / 5) * 10;
return p;
}
const api = { build };
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.QR = api;
})(typeof globalThis !== "undefined" ? globalThis : this);