From 3ac3ec2e5aaf95398ce4a7ffa9091897da164c72 Mon Sep 17 00:00:00 2001 From: nivokvo Date: Fri, 21 Aug 2026 04:39:00 +0300 Subject: [PATCH] fix: bare integer tokens are aliases to numeric endings, not prices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pasted line like 'eggs 420' — meaning 'eggs aliases to the ending .420' — was parsed as a $420 price. All-numeric endings are legal here (.420, .911), so an unmarked number names the ending a line points at, and reading it as a price both silently minted four-figure prices on what were meant to be aliases and made an alias to a numeric ending impossible to write at all. A price must now say it is money: a '$', a decimal point, or a USD affix. The documented forms ($2, 2.00, USD 2) are unchanged; only the bare-integer reading moves from price to alias. --- lib/index.mjs | 24 ++++++++++++++++++------ test/name.test.mjs | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/lib/index.mjs b/lib/index.mjs index 1b3e65f..1ca303e 100644 --- a/lib/index.mjs +++ b/lib/index.mjs @@ -287,9 +287,12 @@ export function parseTldList(input, limit = MAX_BULK_TLDS) { price = parsePriceToken(`${field}${fields[index + 1]}`); if (price !== null) index++; } - // A price is unambiguous — it is the only field that can start with `$` - // or be all digits, and an all-numeric ending is rejected anyway. So - // anything that is not a price is the ending this one points at. + // A marked price — `$2`, `2.00`, `USD 2` — is unambiguous: it is the + // only field that can start with `$` or carry a decimal/USD marker. A + // bare integer is NOT taken as a price, because all-numeric endings are + // legal (`.420`, `.911`) and the token is far more likely to name the + // ending this one points at. Anything that is not a price is therefore + // the ending this one points at. if (price !== null) priceUsd = price; else aliasOf = normalizeToken(field); } @@ -334,10 +337,19 @@ function normalizeToken(value) { * Forgiving because it is typed by hand in a textarea next to a dollar sign, * and strict about the shape because the alternative reading of a stray token * is "the ending this one points at", which would silently mis-route a name. + * + * A bare integer (`420`) is NOT a price. All-numeric endings are legal here — + * `.420`, `.911` — so an unmarked number is far more likely to name the ending + * this line points at than to set a price, and reading it as one silently + * minted four-figure prices on what were meant to be aliases. A price must say + * it is money: a `$`, a decimal point, or a USD affix. */ function parsePriceToken(value) { - const raw = String(value ?? "").trim().toLowerCase().replace(/^usd/, "").replace(/usd$/, "").replace(/^\$/, "").trim(); - if (!raw || !/^\d+(\.\d{1,2})?$/.test(raw)) return null; - const price = Number(raw); + const raw = String(value ?? "").trim().toLowerCase(); + const markedAsMoney = /^\$/.test(raw) || /^usd/.test(raw) || /usd$/.test(raw) || raw.includes("."); + if (!markedAsMoney) return null; + const digits = raw.replace(/^usd/, "").replace(/usd$/, "").replace(/^\$/, "").trim(); + if (!digits || !/^\d+(\.\d{1,2})?$/.test(digits)) return null; + const price = Number(digits); return Number.isFinite(price) && price > 0 ? price : null; } diff --git a/test/name.test.mjs b/test/name.test.mjs index 004b5cf..9f6b3fc 100644 --- a/test/name.test.mjs +++ b/test/name.test.mjs @@ -165,6 +165,27 @@ test("a pasted list reads endings and names alike", async (t) => { [{ tld: "eggs", label: null, aliasOf: null, priceUsd: 2 }]); }); + await t.test("a bare integer is an alias to a numeric ending, not a price", () => { + // All-numeric endings are legal — `.420`, `.911` — so an unmarked number + // names the ending this line points at. Reading it as a price silently + // minted four-figure prices on what were meant to be aliases, and made an + // alias to a numeric ending impossible to write at all. + assert.deepEqual(parseTldList("eggs 420").entries, + [{ tld: "eggs", label: null, aliasOf: "420", priceUsd: null }]); + assert.deepEqual(parseTldList("eggs 911 $2").entries, + [{ tld: "eggs", label: null, aliasOf: "911", priceUsd: 2 }]); + // A name under a numeric ending aliases the same way. + assert.deepEqual(parseTldList("eggs blue.420").entries, + [{ tld: "eggs", label: null, aliasOf: "blue.420", priceUsd: null }]); + }); + + await t.test("documented price forms still read as prices", () => { + for (const [line, expected] of [["eggs $2", 2], ["eggs 2.00", 2], ["eggs $2.00usd", 2], ["eggs usd2", 2]]) { + assert.deepEqual(parseTldList(line).entries, + [{ tld: "eggs", label: null, aliasOf: null, priceUsd: expected }], line); + } + }); + await t.test("neither an ending nor a name survives to be refused by name", () => { // Three labels is neither. Dropping it here would leave it out of the // caller's report entirely, so it is handed on carrying its own bad text.