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();
}
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,
+60
View File
@@ -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);
});
});