Skip to content

Commit 8b50ef9

Browse files
authored
Merge pull request #51 from frstrtr/fix/parse-non-finite
fix(parse): reject non-finite numbers + pin fast-check repro
2 parents 1a69de9 + 8df4324 commit 8b50ef9

2 files changed

Lines changed: 54 additions & 1 deletion

File tree

web-static/sharechain-explorer/src/pplns/parse.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,11 @@ function parseLegacyShape(obj: Record<string, unknown>): PplnsSnapshot {
222222
let amount: number;
223223
let mergedRaw: unknown[] = [];
224224
if (typeof v === 'number') {
225-
amount = v;
225+
// Drop non-finite values (Infinity, -Infinity, NaN); num() clamps
226+
// them to 0 so the !(amount > 0) guard below discards the row. A
227+
// raw Infinity here would otherwise poison totalPrimary. Regression:
228+
// tests/unit/pplns-parse-properties.test.ts non-finite cases.
229+
amount = num(v);
226230
} else if (v !== null && typeof v === 'object') {
227231
const o = v as Record<string, unknown>;
228232
amount = num(o.amount);

web-static/sharechain-explorer/tests/unit/pplns-parse-properties.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,52 @@ test('parseMinerDetail: well-formed input produces typed output', () => {
274274
}
275275
}), { numRuns: 300 });
276276
});
277+
278+
// ── Regression: non-finite numbers (Infinity / -Infinity / NaN) ──────
279+
// Pinned counterexamples for the fast-check flake on PRs #49/#50
280+
// (seed -1679627146, shrunk input [Infinity]). A bare Infinity in the
281+
// legacy flat-map shape was assigned to amount without finite filtering,
282+
// so it survived the amount > 0 guard and poisoned totalPrimary. These
283+
// explicit cases lock the three non-finite shapes regardless of seed.
284+
285+
test('parseSnapshot: non-finite amounts are dropped, total stays finite', () => {
286+
// Array shape: Object.entries([x]) -> ["0", x], hits the legacy
287+
// bare-number branch — the exact path the property test shrank to.
288+
for (const raw of [[Infinity], [-Infinity], [NaN]]) {
289+
const snap = parseSnapshot(raw);
290+
assert.ok(Number.isFinite(snap.totalPrimary),
291+
`totalPrimary not finite for ${String(raw[0])}`);
292+
assert.equal(snap.totalPrimary, 0);
293+
assert.equal(snap.miners.length, 0);
294+
// Required-keys contract (mirrors the property assertions at L107-112).
295+
assert.equal(typeof snap.totalPrimary, 'number');
296+
assert.ok(Array.isArray(snap.mergedChains));
297+
assert.ok(snap.mergedTotals !== null && typeof snap.mergedTotals === 'object');
298+
assert.equal(typeof snap.schemaVersion, 'string');
299+
assert.ok(Array.isArray(snap.miners));
300+
}
301+
});
302+
303+
test('parseSnapshot: non-finite legacy-object amounts are dropped', () => {
304+
// Legacy { addr: { amount } } shape with a non-finite amount field.
305+
for (const bad of [Infinity, -Infinity, NaN]) {
306+
const snap = parseSnapshot({ miner1: { amount: bad } });
307+
assert.ok(Number.isFinite(snap.totalPrimary));
308+
assert.equal(snap.totalPrimary, 0);
309+
assert.equal(snap.miners.length, 0);
310+
}
311+
});
312+
313+
test('parseSnapshot: non-finite new-shape miner amounts are dropped', () => {
314+
// New shape: a miner row whose amount is non-finite must not appear
315+
// and must not poison the totalPrimary fallback sum.
316+
for (const bad of [Infinity, -Infinity, NaN]) {
317+
const snap = parseSnapshot({
318+
total_primary: 0,
319+
miners: [{ address: 'a', amount: bad, pct: 0 }],
320+
});
321+
assert.ok(Number.isFinite(snap.totalPrimary));
322+
assert.equal(snap.totalPrimary, 0);
323+
assert.equal(snap.miners.length, 0);
324+
}
325+
});

0 commit comments

Comments
 (0)