Skip to content

Commit e4dabf8

Browse files
fix(cli): prevent keyframe shots overwriting sources (#3534)
1 parent b28747d commit e4dabf8

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

packages/cli/src/commands/keyframes.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
1+
import { existsSync, linkSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import { beforeAll, describe, expect, it } from "vitest";
@@ -28,6 +28,29 @@ describe("keyframes direct composition scope", () => {
2828
});
2929

3030
describe("keyframes shot output", () => {
31+
it("rejects an output path that would overwrite the composition source", () => {
32+
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-source-"));
33+
const sourcePath = join(projectDir, "index.html");
34+
writeFileSync(sourcePath, wrap(""));
35+
36+
expect(() => ensureShotOutputDir(sourcePath, sourcePath)).toThrow(
37+
/must not overwrite the composition source/,
38+
);
39+
expect(readFileSync(sourcePath, "utf8")).toBe(wrap(""));
40+
});
41+
42+
it("rejects an existing output alias that refers to the composition source", () => {
43+
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-alias-"));
44+
const sourcePath = join(projectDir, "index.html");
45+
const aliasPath = join(projectDir, "shot.png");
46+
writeFileSync(sourcePath, wrap(""));
47+
linkSync(sourcePath, aliasPath);
48+
49+
expect(() => ensureShotOutputDir(aliasPath, sourcePath)).toThrow(
50+
/must not overwrite the composition source/,
51+
);
52+
});
53+
3154
it("creates a missing parent directory before writing --shot", () => {
3255
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-dir-"));
3356
const outputDir = join(projectDir, "nested", "proofs");

packages/cli/src/commands/motionShot.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
// exactly what it's editing. All geometry + SVG live in ./motionShotLayout.ts
1111
// (pure, tested); this file only drives the browser and SAMPLES.
1212

13-
import { mkdirSync, writeFileSync } from "node:fs";
14-
import { dirname } from "node:path";
13+
import { mkdirSync, statSync, writeFileSync } from "node:fs";
14+
import { dirname, resolve } from "node:path";
1515
import { resolveDiagnosticNavigationTimeoutMs } from "../utils/renderArgs.js";
1616
import { resolveCompositionViewportFromHtml } from "../utils/compositionViewport.js";
1717
import {
@@ -33,7 +33,25 @@ export interface ShotRequest {
3333
selector: string;
3434
}
3535

36-
export function ensureShotOutputDir(outPath: string): void {
36+
function pathsReferToSameFile(firstPath: string, secondPath: string): boolean {
37+
const first = resolve(firstPath);
38+
const second = resolve(secondPath);
39+
if (first === second) return true;
40+
try {
41+
const firstStat = statSync(first);
42+
const secondStat = statSync(second);
43+
return firstStat.dev === secondStat.dev && firstStat.ino === secondStat.ino;
44+
} catch {
45+
return false;
46+
}
47+
}
48+
49+
export function ensureShotOutputDir(outPath: string, sourcePath?: string): void {
50+
if (sourcePath && pathsReferToSameFile(outPath, sourcePath)) {
51+
throw new Error(
52+
`--shot output must not overwrite the composition source: ${sourcePath}. Choose a separate .png path.`,
53+
);
54+
}
3755
mkdirSync(dirname(outPath), { recursive: true });
3856
}
3957

@@ -644,7 +662,7 @@ export async function captureMotionPathShot(
644662
outPath: string,
645663
opts: ShotOptions = {},
646664
): Promise<string> {
647-
ensureShotOutputDir(outPath);
665+
ensureShotOutputDir(outPath, resolve(projectDir, opts.entryFile ?? "index.html"));
648666
let requests = requestsIn;
649667
const samples = Math.max(1, Math.min(60, opts.samples ?? 9));
650668
const layout = opts.layout ?? "path";

0 commit comments

Comments
 (0)