chore: linting

This commit is contained in:
zhom
2026-08-10 09:31:42 +04:00
parent 32a1728dee
commit 929f5a0ead
2 changed files with 84 additions and 11 deletions
+24 -11
View File
@@ -60,8 +60,12 @@ export function requestedTarget() {
return match[1].trim(); return match[1].trim();
} }
function sha256(path) { function sha256(bytes) {
return createHash("sha256").update(readFileSync(path)).digest("hex"); return createHash("sha256").update(bytes).digest("hex");
}
function sha256File(path) {
return sha256(readFileSync(path));
} }
export function xrayBinaryName(target) { 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. /// on every attempt, so a truncated or substituted archive still cannot pass.
const DOWNLOAD_ATTEMPTS = 3; const DOWNLOAD_ATTEMPTS = 3;
async function downloadVerifiedArchive(url, archive, expectedSha256) { export async function downloadVerifiedArchive(url, archive, expectedSha256) {
let lastError; let lastError;
for (let attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; attempt += 1) { 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})`, `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) { if (actual !== expectedSha256) {
throw new Error( throw new Error(
`Xray-core checksum mismatch: expected ${expectedSha256}, got ${actual}`, `Xray-core checksum mismatch: expected ${expectedSha256}, got ${actual}`,
); );
} }
writeFileSync(archive, payload);
return; return;
} catch (error) { } catch (error) {
lastError = error; lastError = error;
@@ -162,10 +172,13 @@ async function downloadVerifiedArchive(url, archive, expectedSha256) {
} }
export async function downloadXray(target = requestedTarget()) { export async function downloadXray(target = requestedTarget()) {
const asset = XRAY_ASSETS[target]; // `target` comes from --target/$TARGET, and it decides the file this writes
if (!asset) { // 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}'`); throw new Error(`Xray-core is not packaged for Rust target '${target}'`);
} }
const asset = XRAY_ASSETS[target];
const windowsTarget = target.includes("windows"); const windowsTarget = target.includes("windows");
const destinationDir = join(MANIFEST_DIR, "binaries"); const destinationDir = join(MANIFEST_DIR, "binaries");
@@ -183,8 +196,8 @@ export async function downloadXray(target = requestedTarget()) {
if ( if (
source.version === XRAY_VERSION && source.version === XRAY_VERSION &&
source.archiveSha256 === asset.sha256 && source.archiveSha256 === asset.sha256 &&
source.binarySha256 === sha256(destination) && source.binarySha256 === sha256File(destination) &&
source.licenseSha256 === sha256(licenseDestination) source.licenseSha256 === sha256File(licenseDestination)
) { ) {
return destination; return destination;
} }
@@ -220,8 +233,8 @@ export async function downloadXray(target = requestedTarget()) {
{ {
version: XRAY_VERSION, version: XRAY_VERSION,
archiveSha256: asset.sha256, archiveSha256: asset.sha256,
binarySha256: sha256(destination), binarySha256: sha256File(destination),
licenseSha256: sha256(licenseDestination), licenseSha256: sha256File(licenseDestination),
}, },
null, null,
2, 2,
+60
View File
@@ -1,7 +1,12 @@
import assert from "node:assert/strict"; 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 { readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test"; import test from "node:test";
import { import {
downloadVerifiedArchive,
downloadXray, downloadXray,
windowsExtractionInvocation, windowsExtractionInvocation,
XRAY_ASSETS, XRAY_ASSETS,
@@ -122,3 +127,58 @@ test("rejects an unsupported target before downloading", async () => {
/not packaged for Rust target/, /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);
});
});