From 929f5a0ead42f36b123fc720ffa094c1aee068ee Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:31:42 +0400 Subject: [PATCH] chore: linting --- src-tauri/download-xray.mjs | 35 +++++++++++++------ src-tauri/download-xray.test.mjs | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src-tauri/download-xray.mjs b/src-tauri/download-xray.mjs index c7a8cc1..d766e11 100644 --- a/src-tauri/download-xray.mjs +++ b/src-tauri/download-xray.mjs @@ -60,8 +60,12 @@ export function requestedTarget() { return match[1].trim(); } -function sha256(path) { - return createHash("sha256").update(readFileSync(path)).digest("hex"); +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex"); +} + +function sha256File(path) { + return sha256(readFileSync(path)); } export function xrayBinaryName(target) { @@ -127,7 +131,7 @@ function extractArchive(archive, destinationDir, windowsTarget) { /// on every attempt, so a truncated or substituted archive still cannot pass. const DOWNLOAD_ATTEMPTS = 3; -async function downloadVerifiedArchive(url, archive, expectedSha256) { +export async function downloadVerifiedArchive(url, archive, expectedSha256) { let lastError; for (let attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; attempt += 1) { @@ -138,14 +142,20 @@ async function downloadVerifiedArchive(url, archive, expectedSha256) { `Failed to download Xray-core (${response.status} ${response.statusText})`, ); } - writeFileSync(archive, Buffer.from(await response.arrayBuffer())); - const actual = sha256(archive); + // The response body is verified in memory and only then written out, so + // bytes that fail the pinned digest never reach the file system at all. + // Writing first and checking afterwards left an unverified archive on + // disk for the rest of the attempt, and any later reader of that path + // would have been trusting a plain network download. + const payload = Buffer.from(await response.arrayBuffer()); + const actual = sha256(payload); if (actual !== expectedSha256) { throw new Error( `Xray-core checksum mismatch: expected ${expectedSha256}, got ${actual}`, ); } + writeFileSync(archive, payload); return; } catch (error) { lastError = error; @@ -162,10 +172,13 @@ async function downloadVerifiedArchive(url, archive, expectedSha256) { } export async function downloadXray(target = requestedTarget()) { - const asset = XRAY_ASSETS[target]; - if (!asset) { + // `target` comes from --target/$TARGET, and it decides the file this writes + // into src-tauri/binaries. Only an own key of the pinned table is a target; + // a plain lookup also answers for inherited names like `constructor`. + if (!Object.hasOwn(XRAY_ASSETS, target)) { throw new Error(`Xray-core is not packaged for Rust target '${target}'`); } + const asset = XRAY_ASSETS[target]; const windowsTarget = target.includes("windows"); const destinationDir = join(MANIFEST_DIR, "binaries"); @@ -183,8 +196,8 @@ export async function downloadXray(target = requestedTarget()) { if ( source.version === XRAY_VERSION && source.archiveSha256 === asset.sha256 && - source.binarySha256 === sha256(destination) && - source.licenseSha256 === sha256(licenseDestination) + source.binarySha256 === sha256File(destination) && + source.licenseSha256 === sha256File(licenseDestination) ) { return destination; } @@ -220,8 +233,8 @@ export async function downloadXray(target = requestedTarget()) { { version: XRAY_VERSION, archiveSha256: asset.sha256, - binarySha256: sha256(destination), - licenseSha256: sha256(licenseDestination), + binarySha256: sha256File(destination), + licenseSha256: sha256File(licenseDestination), }, null, 2, diff --git a/src-tauri/download-xray.test.mjs b/src-tauri/download-xray.test.mjs index bd1c143..dff1f87 100644 --- a/src-tauri/download-xray.test.mjs +++ b/src-tauri/download-xray.test.mjs @@ -1,7 +1,12 @@ import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import test from "node:test"; import { + downloadVerifiedArchive, downloadXray, windowsExtractionInvocation, XRAY_ASSETS, @@ -122,3 +127,58 @@ test("rejects an unsupported target before downloading", async () => { /not packaged for Rust target/, ); }); + +// `constructor`, `__proto__` and friends answer a plain `XRAY_ASSETS[target]` +// lookup, and `target` picks the path this script writes into src-tauri. +test("rejects inherited object keys as targets", async () => { + for (const target of ["__proto__", "constructor", "toString"]) { + await assert.rejects( + downloadXray(target), + /not packaged for Rust target/, + target, + ); + } +}); + +async function withStubbedFetch(body, run) { + const scratch = mkdtempSync(join(tmpdir(), "donut-xray-test-")); + const archive = join(scratch, "Xray-linux-64.zip"); + const realFetch = globalThis.fetch; + globalThis.fetch = async () => new Response(body); + try { + await run(archive); + } finally { + globalThis.fetch = realFetch; + rmSync(scratch, { recursive: true, force: true }); + } +} + +test("writes the archive once the pinned digest matches", async () => { + const body = Buffer.from("xray archive bytes"); + const digest = createHash("sha256").update(body).digest("hex"); + + await withStubbedFetch(body, async (archive) => { + await downloadVerifiedArchive( + "https://example.invalid/x.zip", + archive, + digest, + ); + assert.deepEqual(readFileSync(archive), body); + }); +}); + +// The bytes are hashed in memory and only then written, so a substituted or +// truncated response never lands on disk for a later step to pick up. +test("leaves nothing on disk when the payload fails its checksum", async () => { + await withStubbedFetch(Buffer.from("tampered"), async (archive) => { + await assert.rejects( + downloadVerifiedArchive( + "https://example.invalid/x.zip", + archive, + "0".repeat(64), + ), + /checksum mismatch/, + ); + assert.equal(existsSync(archive), false); + }); +});