From c2c7f6445dbc885ff264109b77fbab0f08b7a545 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:48:24 -0400 Subject: [PATCH] refactor: centralize safe decode helpers --- js/magnetShared.js | 33 +++++------------- js/urlHealthObserver.js | 14 ++------ js/utils/safeDecode.js | 32 +++++++++++++++++ js/webtorrent.js | 14 ++------ tests/safe-decode.test.mjs | 70 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 48 deletions(-) create mode 100644 js/utils/safeDecode.js create mode 100644 tests/safe-decode.test.mjs diff --git a/js/magnetShared.js b/js/magnetShared.js index a71f18fe..2d347cf6 100644 --- a/js/magnetShared.js +++ b/js/magnetShared.js @@ -1,25 +1,14 @@ import { WSS_TRACKERS } from "./constants.js"; +import { + safeDecodeURIComponent, + safeDecodeURIComponentLoose, +} from "./utils/safeDecode.js"; const HEX_INFO_HASH = /^[0-9a-f]{40}$/i; const BTIH_PREFIX = "urn:btih:"; const MAGNET_SCHEME = "magnet:"; const ENCODED_BTih_PATTERN = /xt=urn%3Abtih%3A([0-9a-z]+)/gi; -function decodeLoose(value) { - if (typeof value !== "string") { - return ""; - } - const trimmed = value.trim(); - if (!trimmed) { - return ""; - } - try { - return decodeURIComponent(trimmed); - } catch (err) { - return trimmed; - } -} - export function safeDecodeMagnet(value) { if (typeof value !== "string") { return ""; @@ -35,15 +24,11 @@ export function safeDecodeMagnet(value) { break; } - try { - const candidate = decodeURIComponent(decoded); - if (!candidate || candidate === decoded) { - break; - } - decoded = candidate.trim(); - } catch (err) { + const candidate = safeDecodeURIComponent(decoded); + if (!candidate || candidate === decoded) { break; } + decoded = candidate.trim(); } return decoded; @@ -73,7 +58,7 @@ export function normalizeForComparison(value) { function createParam(key, value) { const trimmedValue = typeof value === "string" ? value.trim() : ""; - const decoded = decodeLoose(trimmedValue); + const decoded = safeDecodeURIComponentLoose(trimmedValue); const comparisonBasis = decoded || trimmedValue; return { key, @@ -154,7 +139,7 @@ export function normalizeMagnetInput(rawValue) { const lowerKey = key.toLowerCase(); let value = rawVal.trim(); if (lowerKey === "xt" && value) { - const decoded = decodeLoose(value); + const decoded = safeDecodeURIComponentLoose(value); if (decoded && decoded !== value) { value = decoded; didMutate = true; diff --git a/js/urlHealthObserver.js b/js/urlHealthObserver.js index 37410ebf..c3746190 100644 --- a/js/urlHealthObserver.js +++ b/js/urlHealthObserver.js @@ -1,19 +1,9 @@ import { createCardObserver } from "./dom/cardObserver.js"; +import { safeDecodeURIComponent } from "./utils/safeDecode.js"; const ROOT_MARGIN = "0px"; const THRESHOLD = 0.25; -function decodeUrl(value) { - if (typeof value !== "string" || !value) { - return ""; - } - try { - return decodeURIComponent(value); - } catch (err) { - return value; - } -} - const urlCardObserver = createCardObserver({ rootMargin: ROOT_MARGIN, threshold: THRESHOLD, @@ -51,7 +41,7 @@ const urlCardObserver = createCardObserver({ return; } - const url = decodeUrl(encodedUrl); + const url = safeDecodeURIComponent(encodedUrl); if (!url) { return; } diff --git a/js/utils/safeDecode.js b/js/utils/safeDecode.js new file mode 100644 index 00000000..202b240d --- /dev/null +++ b/js/utils/safeDecode.js @@ -0,0 +1,32 @@ +export function safeDecodeURIComponent(value) { + if (typeof value !== "string") { + return ""; + } + + if (value === "") { + return ""; + } + + try { + return decodeURIComponent(value); + } catch (err) { + return value; + } +} + +export function safeDecodeURIComponentLoose(value, { trim = true } = {}) { + if (typeof value !== "string") { + return ""; + } + + const working = trim ? value.trim() : value; + if (trim && !working) { + return ""; + } + + try { + return decodeURIComponent(working); + } catch (err) { + return working; + } +} diff --git a/js/webtorrent.js b/js/webtorrent.js index c237558a..3b1d8cd1 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -2,20 +2,10 @@ import WebTorrent from "./webtorrent.min.js"; import { WSS_TRACKERS } from "./constants.js"; +import { safeDecodeURIComponent } from "./utils/safeDecode.js"; const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]); -function decodeComponentSafe(value) { - if (typeof value !== "string") { - return ""; - } - try { - return decodeURIComponent(value); - } catch (err) { - return value; - } -} - function normalizeTrackerList(trackers) { const normalized = []; const seen = new Set(); @@ -73,7 +63,7 @@ function appendProbeTrackers(magnetURI, trackers) { if (!rawKey || rawKey.trim().toLowerCase() !== "tr") { return; } - const decoded = decodeComponentSafe(rawValue).trim().toLowerCase(); + const decoded = safeDecodeURIComponent(rawValue).trim().toLowerCase(); if (decoded) { trackerSet.add(decoded); } diff --git a/tests/safe-decode.test.mjs b/tests/safe-decode.test.mjs new file mode 100644 index 00000000..e6c424dc --- /dev/null +++ b/tests/safe-decode.test.mjs @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + safeDecodeURIComponent, + safeDecodeURIComponentLoose, +} from "../js/utils/safeDecode.js"; + +import { safeDecodeMagnet } from "../js/magnetUtils.js"; + +test("safeDecodeURIComponent returns original value on malformed sequences", () => { + const malformed = "%E0%A4%A"; + assert.equal( + safeDecodeURIComponent(malformed), + malformed, + "Malformed sequences should return the original input" + ); +}); + +test("safeDecode helpers handle double-encoded values when applied repeatedly", () => { + const raw = "magnet:?xt=urn:btih:abcdef"; + const doubleEncoded = encodeURIComponent(encodeURIComponent(raw)); + + const firstPass = safeDecodeURIComponent(doubleEncoded); + assert.equal( + firstPass, + encodeURIComponent(raw), + "First pass should peel one encoding layer" + ); + + const secondPass = safeDecodeURIComponent(firstPass); + assert.equal( + secondPass, + raw, + "Second pass should reveal the original string" + ); + + assert.equal( + safeDecodeMagnet(doubleEncoded), + raw, + "safeDecodeMagnet should continue to handle double-encoded magnets" + ); +}); + +test("safeDecodeURIComponentLoose trims inputs by default", () => { + const padded = " magnet:?dn=Example"; + assert.equal( + safeDecodeURIComponentLoose(padded), + "magnet:?dn=Example", + "Loose decoder should trim whitespace when decoding" + ); +}); + +test("safeDecodeURIComponentLoose preserves whitespace when trim is false", () => { + const padded = " magnet:?dn=Example"; + assert.equal( + safeDecodeURIComponentLoose(padded, { trim: false }), + padded, + "Loose decoder should return the original string when trim is disabled and decoding is unnecessary" + ); +}); + +test("safeDecodeURIComponent handles empty strings consistently", () => { + assert.equal(safeDecodeURIComponent(""), "", "Empty input should stay empty"); + assert.equal( + safeDecodeURIComponentLoose(""), + "", + "Loose decoder should return empty for empty input" + ); +});