Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions packages/engine/src/services/audioVolumeEnvelope.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import { getFfmpegBinary } from "../utils/ffmpegBinaries.js";
import { applyVolumeEnvelopeToWav } from "./audioVolumeEnvelope.js";

// Make a staging-name collision reproducible without weakening production randomness.
vi.mock("crypto", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:crypto")>();
return { ...actual, randomBytes: () => Buffer.from("010203040506", "hex") };
});

const SAMPLE_RATE = 48000;
const CHANNELS = 2;
const HAS_FFMPEG = spawnSync(getFfmpegBinary(), ["-version"], { encoding: "utf-8" }).status === 0;
Expand Down Expand Up @@ -48,6 +61,27 @@ describe("applyVolumeEnvelopeToWav", () => {
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
});

// Creating file symlinks requires extra privileges on Windows; regular collisions run there.
it.each(process.platform === "win32" ? ["file"] : ["file", "symlink"])(
"preserves an existing staging %s and original WAV",
(kind) => {
const dir = tmp();
const path = join(dir, "collision.wav");
writeConstantWav(path, 16, 10000);
const original = readFileSync(path);
const tempPath = `${path}.010203040506.tmp`;
const victim = join(dir, "victim.txt");
writeFileSync(victim, "untouched");
if (kind === "symlink") symlinkSync(victim, tempPath);
else writeFileSync(tempPath, "untouched");

expect(applyVolumeEnvelopeToWav(path, [{ time: 0, volume: 0 }], 0, 0)).toBe(false);
expect(readFileSync(path)).toEqual(original);
expect(readFileSync(tempPath, "utf8")).toBe("untouched");
expect(readFileSync(victim, "utf8")).toBe("untouched");
},
);

it("applies a linear fade sample-accurately", () => {
const path = join(tmp(), "a.wav");
const frames = SAMPLE_RATE; // 1 second
Expand All @@ -64,6 +98,7 @@ describe("applyVolumeEnvelopeToWav", () => {
0,
);
expect(applied).toBe(true);
expect(readdirSync(dirname(path))).toEqual(["a.wav"]);

expect(sampleAt(path, 0)).toBe(0); // gain 0
expect(sampleAt(path, frames / 2)).toBeCloseTo(5000, -2); // gain ~0.5
Expand Down
6 changes: 3 additions & 3 deletions packages/engine/src/services/audioVolumeEnvelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,11 @@
scaleSamples(buffer, layout, gainAt);

// Write to a uniquely-named sibling then atomically rename over the
// original. The random name avoids following a pre-planted symlink at a
// predictable path, and the rename means a crash mid-write can't leave a
// original. Exclusive creation rejects any pre-existing file or symlink,
// and the rename means a crash mid-write can't leave a
// truncated WAV for the downstream mix.
const tempPath = `${wavPath}.${randomBytes(6).toString("hex")}.tmp`;
writeFileSync(tempPath, buffer);
writeFileSync(tempPath, buffer, { flag: "wx" });
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
renameSync(tempPath, wavPath);
return true;
} catch {
Expand Down
Loading