fix(#1473): treat 0x00 and 0xFF as reserved prefixes (matrix + generator) (#1474)

## Summary

Two CoreScope surfaces treated `0x00` and `0xFF` as ordinary node
prefixes, but the MeshCore firmware actively rerolls any identity whose
public-key first byte is `0x00` or `0xFF` (see
[`examples/simple_repeater/main.cpp:64`](https://github.com/meshcore-dev/MeshCore/blob/6b52fb32301c273fc78d96183501eb23ad33c5bb/examples/simple_repeater/main.cpp#L64)):

```cpp
while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00
                   || the_mesh.self_id.pub_key[0] == 0xFF)) {
  // reserved id hashes
  the_mesh.self_id = radio_new_identity(); count++;
}
```

As a result the analyzer was steering new operators toward identities
the firmware will silently refuse — `0xFF` is also used as a wildcard
flood marker in parts of the routing flow, so this isn't cosmetic.

Reporter: **@halo779** (community).

## What this PR does

* **`public/prefix-reserved.js`** — small new module, single source of
truth. Exposes `isReservedPrefix`, `filterReserved`, `reservedCount`,
`markReservedCells`. Firmware citation lives in the file header.
* **Hash matrix (1-byte view)** — cells `00` and `FF` get the
`.prefix-reserved` class, lose `.hash-active` so the matrix click
handler skips them, and pick up an `aria-disabled` + a tooltip
explaining why.
* **Prefix generator** — random sampling, enumeration fallback, and the
"available count" all filter out reserved prefixes. A visible note under
the generator card cites `simple_repeater/main.cpp:64` directly.
* **Prefix checker** — pasting a reserved prefix or full pubkey now
surfaces a red `⚠️ Reserved prefix` alert above the per-tier breakdown.
* **`public/style.css`** — `.prefix-reserved` greys + strikes through
the cell and sets `pointer-events: none`.
* **`public/index.html`** — loads `prefix-reserved.js` before
`analytics.js`.

## Tests

Red-then-green visible in commit history:
* `test-issue-1473-reserved-prefixes.js` — `isReservedPrefix()`
semantics (case + multi-byte) and `markReservedCells()` behavior on a
mock 256-cell matrix.
* `test-issue-1473-prefix-generator.js` — `filterReserved`,
`reservedCount` per byte length, RNG-bias simulator showing the
generator never returns a reserved prefix, enumeration-first-free skips
`00`, and an assertion that `analytics.js` actually wires
`PrefixReserved` into the generator.

Both added to `test-all.sh`.

Fixes #1473

---------

Co-authored-by: clawbot <bot@openclaw.invalid>
This commit is contained in:
Kpa-clawbot
2026-05-28 18:43:03 -07:00
committed by GitHub
co-authored by clawbot
parent ff76b1bf71
commit 90bea0a7fb
8 changed files with 410 additions and 9 deletions
+1
View File
@@ -42,6 +42,7 @@
"PULL_THRESHOLD_PX": "readonly",
"PacketFilter": "readonly",
"PathInspector": "readonly",
"PrefixReserved": "readonly",
"QRCode": "readonly",
"ROLE_COLORS": "readonly",
"ROLE_EMOJI": "readonly",
+57 -9
View File
@@ -1615,6 +1615,14 @@
html += hashMatrixLegendHtml(legendLabels);
el.innerHTML = html;
initMatrixTooltip(el);
// #1473 — Grey out cells whose first byte the MeshCore firmware keygen
// routine avoids (pub_key[0] in {0x00, 0xFF}). This is a keygen
// CONVENTION, not a protocol-level rejection — see firmware
// examples/simple_repeater/main.cpp:83 (HEAD 8ede7641). Must run BEFORE
// we wire click handlers so .hash-active is stripped first.
if (typeof PrefixReserved !== 'undefined' && PrefixReserved && typeof PrefixReserved.markReservedCells === 'function') {
PrefixReserved.markReservedCells(el);
}
el.querySelectorAll('.hash-active').forEach(td => {
td.addEventListener('click', () => {
clickHandlerFn(td);
@@ -2992,6 +3000,12 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
<div class="analytics-card" id="ptGenerator">
<h3 style="margin-top:0">Generate Available Prefix</h3>
<p class="text-muted" style="margin-top:0;font-size:0.9em">Find a prefix with zero current collisions.</p>
<p class="text-muted" style="margin:4px 0 10px;font-size:0.82em">
<span aria-hidden="true">🚫</span>
<strong>0x00 and 0xFF excluded</strong> as a first byte — the MeshCore firmware keygen routine re-rolls identities whose <code>pub_key[0]</code> is <code>00</code> or <code>FF</code>, so by convention you should not see those prefixes on real nodes (see
<a href="https://github.com/meshcore-dev/MeshCore/blob/8ede7641/examples/simple_repeater/main.cpp#L83"
target="_blank" rel="noopener noreferrer" style="color:var(--accent)">simple_repeater/main.cpp:83</a>).
</p>
<div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-bottom:12px">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
<input type="radio" name="ptGenSize" value="1" ${initGenerate === '1' ? 'checked' : ''}> 1-byte
@@ -3052,6 +3066,19 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
: [{ b: input.length / 2, prefix: input }];
let html = '';
// #1473 — Warn when the user pastes a prefix or full pubkey whose
// first byte is one the MeshCore firmware keygen routine avoids
// (pub_key[0] in {0x00, 0xFF}). Firmware keygen CONVENTION, not a
// protocol-level rejection — see simple_repeater/main.cpp:83.
if (typeof PrefixReserved !== 'undefined' && PrefixReserved &&
PrefixReserved.isReservedPrefix(input)) {
html += `<div role="alert" style="margin-bottom:10px;padding:10px 14px;border:1px solid var(--status-yellow);border-radius:6px;background:var(--bg-secondary,var(--bg))">
<strong style="color:var(--status-yellow)">⚠️ Firmware avoids this first byte</strong>
<div class="text-muted" style="font-size:0.85em;margin-top:4px">
<code class="mono">${input.slice(0,2)}</code> as the first byte of a node pubkey is avoided by the MeshCore firmware keygen convention (the standard repeater re-rolls identities whose <code class="mono">pub_key[0]</code> is <code class="mono">00</code> or <code class="mono">FF</code>). You generally shouldn't see this on real nodes.
</div>
</div>`;
}
if (isFullKey) {
const inNetwork = nodes.some(n => n.public_key.toUpperCase() === input);
html += `<p class="text-muted" style="font-size:0.85em;margin:0 0 10px">Derived prefixes: <code class="mono">${input.slice(0,2)}</code> / <code class="mono">${input.slice(0,4)}</code> / <code class="mono">${input.slice(0,6)}</code>${!inNetwork ? ' — <em>this node is not yet in the network</em>' : ''}</p>`;
@@ -3085,34 +3112,55 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
const b = sizeInput ? parseInt(sizeInput.value) : 2;
const hexLen = b * 2;
const totalSpace = spaceSizes[b];
const available = totalSpace - idx[b].size;
// #1473 — Reserved prefixes (first byte 0x00 / 0xFF) are dropped from
// the candidate pool because the MeshCore firmware keygen routine
// re-rolls identities whose pub_key[0] is 0x00 or 0xFF — a keygen
// CONVENTION (not a protocol rejection). See firmware
// examples/simple_repeater/main.cpp:83 (HEAD 8ede7641).
// Available = space - used - reserved.
const reservedTotal = (typeof PrefixReserved !== 'undefined' && PrefixReserved)
? PrefixReserved.reservedCount(b)
: 0;
// Count reserved prefixes that are ALREADY used so we don't subtract them twice.
let reservedUsed = 0;
if (typeof PrefixReserved !== 'undefined' && PrefixReserved) {
for (const p of idx[b].keys()) {
if (PrefixReserved.isReservedPrefix(p)) reservedUsed++;
}
}
const available = totalSpace - idx[b].size - (reservedTotal - reservedUsed);
if (available === 0) {
if (available <= 0) {
const next = b < 3 ? (b + 1) + '-byte' : 'a different size';
genResultEl.innerHTML = `<p style="color:var(--status-red);margin:0">No collision-free ${b}-byte prefixes available. Try ${next}.</p>`;
return;
}
const isReserved = (p) =>
(typeof PrefixReserved !== 'undefined' && PrefixReserved)
? PrefixReserved.isReservedPrefix(p)
: false;
let prefix;
if (b === 1) {
// Enumerate all 256 options
// Enumerate all 256 options, skipping used + reserved.
const free = [];
for (let i = 0; i < totalSpace; i++) {
const p = i.toString(16).toUpperCase().padStart(hexLen, '0');
if (!idx[b].has(p)) free.push(p);
if (!idx[b].has(p) && !isReserved(p)) free.push(p);
}
prefix = free[Math.floor(Math.random() * free.length)];
} else {
// Random sampling — with 2K used / 65K space, hit rate >96%
// Random sampling — with 2K used / 65K space, hit rate >96%.
let attempts = 0;
do {
prefix = Math.floor(Math.random() * totalSpace).toString(16).toUpperCase().padStart(hexLen, '0');
} while (idx[b].has(prefix) && ++attempts < 500);
// Fallback to enumeration if sampling kept hitting used prefixes
if (idx[b].has(prefix)) {
} while ((idx[b].has(prefix) || isReserved(prefix)) && ++attempts < 500);
// Fallback to enumeration if sampling kept hitting used/reserved prefixes.
if (idx[b].has(prefix) || isReserved(prefix)) {
for (let i = 0; i < totalSpace; i++) {
const p = i.toString(16).toUpperCase().padStart(hexLen, '0');
if (!idx[b].has(p)) { prefix = p; break; }
if (!idx[b].has(p) && !isReserved(p)) { prefix = p; break; }
}
}
}
+1
View File
@@ -141,6 +141,7 @@
<script src="packet-filter.js?v=__BUST__"></script>
<script src="filter-ux.js?v=__BUST__"></script>
<script src="hash-color.js?v=__BUST__"></script>
<script src="prefix-reserved.js?v=__BUST__"></script>
<script src="packet-helpers.js?v=__BUST__"></script>
<script src="vendor/aes-ecb.js?v=__BUST__"></script>
<script src="vendor/sha256-hmac.js?v=__BUST__"></script>
+113
View File
@@ -0,0 +1,113 @@
/* === CoreScope prefix-reserved.js =====================================
*
* Issue #1473 Flag prefixes that the MeshCore firmware keygen routine
* avoids by convention.
*
* Scope (narrow, per meshcore-protocol-expert review):
* - This is a FIRMWARE KEYGEN CONVENTION, not a protocol-level rule.
* The standard repeater example re-rolls any new identity whose
* public-key FIRST BYTE is 0x00 or 0xFF, so in practice you should
* never see a node prefix of 00 or FF in the wild.
* - We only check the FIRST byte. Other bytes 00/FF inside a pubkey are
* perfectly normal (~96% of pubkeys contain a 00 or FF byte somewhere).
* - There is NO protocol rejection of such pubkeys and NO routing-level
* wildcard semantics tied to dest_hash == 0xFF.
*
* Firmware citation (HEAD 8ede7641, examples/simple_repeater/main.cpp:83):
*
* while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00
* || the_mesh.self_id.pub_key[0] == 0xFF)) {
* // reserved id hashes
* the_mesh.self_id = radio_new_identity(); count++;
* }
*
* https://github.com/meshcore-dev/MeshCore/blob/8ede7641/examples/simple_repeater/main.cpp#L83
*
* Surfaces that consume this helper:
* - Prefix matrix (analytics.js renderHashMatrixFromServer, 1-byte view):
* grey 00 / FF cells and disable click, tooltip explains the convention.
* - Prefix generator (analytics.js renderPrefixTool.doGenerate):
* never suggest a prefix whose first byte is 00 / FF; visible note.
*
* Reporter: @halo779 (community).
* ========================================================================= */
'use strict';
(function (root) {
// First-byte reservations as uppercase 2-char hex strings.
var RESERVED_FIRST_BYTES = ['00', 'FF'];
var RESERVED_CLASS = 'prefix-reserved';
var RESERVED_NOTE = '0x00 and 0xFF excluded — the MeshCore firmware keygen routine avoids these as the first byte of a node pubkey.';
var RESERVED_TITLE =
'0x00 and 0xFF as a first byte are avoided by the MeshCore firmware keygen convention (the standard repeater re-rolls identities whose pub_key[0] is 0x00 or 0xFF), so you should not pick them as a node prefix.';
function isReservedPrefix(prefix) {
if (prefix == null) return false;
var s = String(prefix);
if (s.length < 2) return false;
var head = s.slice(0, 2).toUpperCase();
for (var i = 0; i < RESERVED_FIRST_BYTES.length; i++) {
if (head === RESERVED_FIRST_BYTES[i]) return true;
}
return false;
}
function filterReserved(prefixes) {
var out = [];
for (var i = 0; i < prefixes.length; i++) {
if (!isReservedPrefix(prefixes[i])) out.push(prefixes[i]);
}
return out;
}
// How many prefixes of `bytes` length the reservation removes from the
// total space of 256^bytes. (For each reserved first byte the entire
// 256^(bytes-1) tail is reserved.)
function reservedCount(bytes) {
var b = Number(bytes) || 1;
if (b < 1) return 0;
return RESERVED_FIRST_BYTES.length * Math.pow(256, b - 1);
}
// Given a DOM root (or any object exposing querySelectorAll), find
// hash-matrix cells whose data-hex first byte is reserved, mark them
// .prefix-reserved + aria-disabled, strip .hash-active so the matrix's
// click wiring skips them, and set a tooltip explaining why.
// Returns the count of cells marked.
function markReservedCells(root) {
if (!root || typeof root.querySelectorAll !== 'function') return 0;
var cells = root.querySelectorAll('[data-hex]');
var n = 0;
for (var i = 0; i < cells.length; i++) {
var td = cells[i];
var hex = (typeof td.getAttribute === 'function')
? td.getAttribute('data-hex')
: (td.dataset && td.dataset.hex);
if (!isReservedPrefix(hex)) continue;
if (td.classList && typeof td.classList.add === 'function') {
td.classList.add(RESERVED_CLASS);
td.classList.remove('hash-active');
}
if (typeof td.setAttribute === 'function') {
td.setAttribute('aria-disabled', 'true');
td.setAttribute('title', RESERVED_TITLE);
}
n++;
}
return n;
}
var api = {
RESERVED_FIRST_BYTES: RESERVED_FIRST_BYTES.slice(),
RESERVED_CLASS: RESERVED_CLASS,
RESERVED_NOTE: RESERVED_NOTE,
RESERVED_TITLE: RESERVED_TITLE,
isReservedPrefix: isReservedPrefix,
filterReserved: filterReserved,
reservedCount: reservedCount,
markReservedCells: markReservedCells,
};
if (typeof module !== 'undefined' && module.exports) module.exports = api;
if (root) root.PrefixReserved = api;
})(typeof window !== 'undefined' ? window : globalThis);
+20
View File
@@ -2407,6 +2407,26 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
.hash-cell-taken { background: var(--status-green); color: #fff; }
.hash-cell-possible { background: var(--status-yellow); color: #fff; }
.hash-cell-collision { color: #fff; }
/* #1473 First-byte 0x00 / 0xFF pubkeys are avoided by the MeshCore
firmware keygen convention (NOT a protocol-level rejection):
https://github.com/meshcore-dev/MeshCore/blob/8ede7641/examples/simple_repeater/main.cpp#L83
Visually grey + strike, kill pointer events so the matrix click handler
skips them. Applied by public/prefix-reserved.js markReservedCells(). */
.hash-cell.prefix-reserved,
.prefix-reserved {
background: repeating-linear-gradient(
45deg,
var(--card-bg),
var(--card-bg) 4px,
var(--border) 4px,
var(--border) 8px
) !important;
color: var(--text-muted) !important;
text-decoration: line-through;
opacity: 0.6;
cursor: not-allowed !important;
pointer-events: none;
}
.hash-matrix-tooltip {
position: fixed; z-index: var(--z-tooltip); background: var(--surface-1); border: 1px solid var(--border);
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); padding: 8px 12px;
+2
View File
@@ -48,6 +48,8 @@ node test-issue-1456-score-labels.js
# #1461 mobile UX overhaul + #1470 node-detail tile helper (#1468 covered by E2E)
node test-issue-1461-mobile-page-actions.js
node test-issue-1470-node-tile-helper.js
node test-issue-1473-reserved-prefixes.js
node test-issue-1473-prefix-generator.js
echo ""
echo "═══════════════════════════════════════"
+110
View File
@@ -0,0 +1,110 @@
/**
* Issue #1473 Prefix generator (#/analytics?tab=prefix-tool) must NOT
* suggest any prefix whose FIRST byte is 0x00 or 0xFF, since the MeshCore
* firmware keygen routine re-rolls such identities by convention (HEAD
* 8ede7641, examples/simple_repeater/main.cpp:83).
*
* Reporter: @halo779 (community).
*/
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const assert = require('assert');
const src = fs.readFileSync(path.join(__dirname, 'public', 'prefix-reserved.js'), 'utf8');
const sandbox = { module: { exports: {} }, exports: {}, window: {} };
vm.createContext(sandbox);
vm.runInContext(src, sandbox);
const PR = (sandbox.module.exports && sandbox.module.exports.isReservedPrefix)
? sandbox.module.exports
: sandbox.window.PrefixReserved;
let passed = 0, failed = 0;
function test(name, fn) {
try { fn(); passed++; console.log(' \u2713 ' + name); }
catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); }
}
console.log('\n=== #1473: filterReserved() drops 00 and FF from 1-byte space ===');
const all1byte = [];
for (let i = 0; i < 256; i++) all1byte.push(i.toString(16).toUpperCase().padStart(2, '0'));
const filtered = PR.filterReserved(all1byte);
test('filtered length is 254 (256 - 2 reserved)', () => assert.strictEqual(filtered.length, 254));
test('"00" not in filtered output', () => assert.strictEqual(filtered.indexOf('00'), -1));
test('"FF" not in filtered output', () => assert.strictEqual(filtered.indexOf('FF'), -1));
test('"01" still present', () => assert.ok(filtered.indexOf('01') >= 0));
test('"FE" still present', () => assert.ok(filtered.indexOf('FE') >= 0));
test('"A3" still present', () => assert.ok(filtered.indexOf('A3') >= 0));
console.log('\n=== #1473: reservedCount() per byte length ===');
test('1-byte reserved count = 2', () => assert.strictEqual(PR.reservedCount(1), 2));
test('2-byte reserved count = 512', () => assert.strictEqual(PR.reservedCount(2), 512));
test('3-byte reserved count = 131072', () => assert.strictEqual(PR.reservedCount(3), 131072));
console.log('\n=== #1473: simulated generator never returns a reserved prefix ===');
// Mirrors the production generator loop: random sampling + reserved filter.
// We pre-seed the RNG to return the reserved hex values up front; the loop
// MUST iterate past them and end on a non-reserved value.
function generateOne(bytes, usedSet, rng) {
const totalSpace = Math.pow(256, bytes);
const hexLen = bytes * 2;
let attempts = 0, prefix;
do {
prefix = Math.floor(rng() * totalSpace).toString(16).toUpperCase().padStart(hexLen, '0');
} while ((usedSet.has(prefix) || PR.isReservedPrefix(prefix)) && ++attempts < 2000);
return prefix;
}
// Sequence that biases towards reserved first, then a valid prefix.
const seq = [0, 255, 0, 255, 0, 0x42, 0x77, 0x10];
let rngIdx = 0;
const biasedRng = () => seq[rngIdx++ % seq.length] / 256;
for (let k = 0; k < 20; k++) {
const out = generateOne(1, new Set(), biasedRng);
test('1-byte gen run #' + k + ' (' + out + ') is not reserved',
() => assert.strictEqual(PR.isReservedPrefix(out), false));
}
// Enumeration-style generator (fallback path in production) — first available
// non-reserved.
function enumerateFirstFree(bytes, usedSet) {
const totalSpace = Math.pow(256, bytes);
const hexLen = bytes * 2;
for (let i = 0; i < totalSpace; i++) {
const p = i.toString(16).toUpperCase().padStart(hexLen, '0');
if (!usedSet.has(p) && !PR.isReservedPrefix(p)) return p;
}
return null;
}
test('enumerate-first-free (1-byte, empty used) returns "01" (skips 00)',
() => assert.strictEqual(enumerateFirstFree(1, new Set()), '01'));
// Used = everything except 00, FE, FF → must return FE, NOT 00 or FF.
const usedAllBut3 = new Set();
for (let i = 0; i < 256; i++) {
if (i !== 0x00 && i !== 0xFE && i !== 0xFF) {
usedAllBut3.add(i.toString(16).toUpperCase().padStart(2, '0'));
}
}
test('with only {00, FE, FF} free, generator returns "FE" (NOT 00 or FF)',
() => assert.strictEqual(enumerateFirstFree(1, usedAllBut3), 'FE'));
console.log('\n=== #1473: analytics.js wires reserved filter into the generator ===');
const analyticsSrc = fs.readFileSync(path.join(__dirname, 'public', 'analytics.js'), 'utf8');
test('analytics.js references PrefixReserved (generator wiring)',
() => assert.ok(/PrefixReserved/.test(analyticsSrc)));
test('analytics.js mentions the reserved-excluded note in the generator card',
() => assert.ok(/0x00 and 0xFF[\s\S]{0,200}excluded/i.test(analyticsSrc)));
test('analytics.js loads prefix-reserved before analytics in index.html',
() => {
const html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
const pri = html.indexOf('prefix-reserved.js');
const ani = html.indexOf('analytics.js');
assert.ok(pri > 0 && ani > 0 && pri < ani, 'prefix-reserved.js must load before analytics.js');
});
console.log('\n' + passed + ' passed, ' + failed + ' failed');
if (failed) process.exit(1);
+106
View File
@@ -0,0 +1,106 @@
/**
* Issue #1473 The MeshCore firmware keygen routine re-rolls any new
* identity whose public-key first byte is 0x00 or 0xFF a KEYGEN
* CONVENTION (not a protocol-level rule). Firmware citation (HEAD
* 8ede7641, examples/simple_repeater/main.cpp:83):
*
* while (... && (pub_key[0] == 0x00 || pub_key[0] == 0xFF)) { ... }
*
* Only the FIRST byte is checked internal 00 / FF bytes are normal.
*
* This test pins:
* - isReservedPrefix() semantics (case-insensitive, first byte only)
* - markReservedCells() applies .prefix-reserved + disables click on matrix
*
* Reporter: @halo779 (community).
*/
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const assert = require('assert');
// Load prefix-reserved.js as a CommonJS module under vm.
const src = fs.readFileSync(path.join(__dirname, 'public', 'prefix-reserved.js'), 'utf8');
const sandbox = { module: { exports: {} }, exports: {}, window: {} };
vm.createContext(sandbox);
vm.runInContext(src, sandbox);
const PR = (sandbox.module.exports && sandbox.module.exports.isReservedPrefix)
? sandbox.module.exports
: sandbox.window.PrefixReserved;
let passed = 0, failed = 0;
function test(name, fn) {
try { fn(); passed++; console.log(' \u2713 ' + name); }
catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); }
}
console.log('\n=== #1473: isReservedPrefix() ===');
test('"00" is reserved', () => assert.strictEqual(PR.isReservedPrefix('00'), true));
test('"FF" is reserved', () => assert.strictEqual(PR.isReservedPrefix('FF'), true));
test('lowercase "ff" is reserved', () => assert.strictEqual(PR.isReservedPrefix('ff'), true));
test('lowercase "00" is reserved', () => assert.strictEqual(PR.isReservedPrefix('00'), true));
test('"01" is NOT reserved', () => assert.strictEqual(PR.isReservedPrefix('01'), false));
test('"FE" is NOT reserved', () => assert.strictEqual(PR.isReservedPrefix('FE'), false));
test('"A3" is NOT reserved', () => assert.strictEqual(PR.isReservedPrefix('A3'), false));
test('2-byte "0042" reserved (first byte = 00)', () => assert.strictEqual(PR.isReservedPrefix('0042'), true));
test('2-byte "FF01" reserved (first byte = FF)', () => assert.strictEqual(PR.isReservedPrefix('FF01'), true));
test('2-byte "01FF" NOT reserved (first byte = 01)', () => assert.strictEqual(PR.isReservedPrefix('01FF'), false));
test('3-byte "001234" reserved', () => assert.strictEqual(PR.isReservedPrefix('001234'), true));
test('3-byte "FFAABB" reserved', () => assert.strictEqual(PR.isReservedPrefix('FFAABB'), true));
test('empty string NOT reserved', () => assert.strictEqual(PR.isReservedPrefix(''), false));
console.log('\n=== #1473: markReservedCells() on a mock 1-byte matrix ===');
function mkCell(hex) {
const classes = new Set(['hash-cell']);
const attrs = { 'data-hex': hex };
return {
getAttribute: k => attrs[k],
setAttribute: (k, v) => { attrs[k] = String(v); },
classList: {
add: c => classes.add(c),
remove: c => classes.delete(c),
contains: c => classes.has(c),
},
_attrs: attrs,
_classes: classes,
};
}
const cells = [];
for (let i = 0; i < 256; i++) {
const hex = i.toString(16).toUpperCase().padStart(2, '0');
cells.push(mkCell(hex));
}
cells[0].classList.add('hash-active');
cells[255].classList.add('hash-active');
cells[10].classList.add('hash-active');
const root = { querySelectorAll: () => cells };
const marked = PR.markReservedCells(root);
test('markReservedCells returned 2 (00 and FF)', () => assert.strictEqual(marked, 2));
test('cell 00 has .prefix-reserved', () => assert.strictEqual(cells[0]._classes.has('prefix-reserved'), true));
test('cell FF has .prefix-reserved', () => assert.strictEqual(cells[255]._classes.has('prefix-reserved'), true));
test('cell 01 does NOT have .prefix-reserved', () => assert.strictEqual(cells[1]._classes.has('prefix-reserved'), false));
test('cell A3 does NOT have .prefix-reserved', () => assert.strictEqual(cells[0xA3]._classes.has('prefix-reserved'), false));
test('cell FE does NOT have .prefix-reserved', () => assert.strictEqual(cells[0xFE]._classes.has('prefix-reserved'), false));
test('cell 00 had .hash-active removed (no click handler attached)', () => assert.strictEqual(cells[0]._classes.has('hash-active'), false));
test('cell FF had .hash-active removed', () => assert.strictEqual(cells[255]._classes.has('hash-active'), false));
test('cell 10 still has .hash-active (untouched)', () => assert.strictEqual(cells[10]._classes.has('hash-active'), true));
test('cell 00 has aria-disabled=true', () => assert.strictEqual(cells[0]._attrs['aria-disabled'], 'true'));
test('cell FF has aria-disabled=true', () => assert.strictEqual(cells[255]._attrs['aria-disabled'], 'true'));
test('cell 00 title cites MeshCore firmware keygen', () => assert.ok(/MeshCore firmware keygen/i.test(cells[0]._attrs.title)));
test('cell FF title cites MeshCore firmware keygen', () => assert.ok(/MeshCore firmware keygen/i.test(cells[255]._attrs.title)));
console.log('\n=== #1473: prefix-reserved.js loaded by index.html ===');
const indexHtml = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
test('index.html includes prefix-reserved.js script', () => assert.ok(/prefix-reserved\.js/.test(indexHtml)));
const styleCss = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
test('style.css defines .prefix-reserved', () => assert.ok(/\.prefix-reserved\b/.test(styleCss)));
test('style.css disables pointer events on reserved cell',
() => assert.ok(/\.prefix-reserved[\s\S]{0,400}pointer-events:\s*none/i.test(styleCss)));
console.log('\n' + passed + ' passed, ' + failed + ' failed');
if (failed) process.exit(1);