mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(download): ignore OS-managed root artifacts when claiming a base (#7268)
ensureManagedBase() rejected a managed download base as not-owned whenever any file sat at its root before the first claim, including .DS_Store, which Finder writes automatically the moment that folder is ever viewed. This is the same defect class fixed for the desktop updater's own store root in #7259 (apps/desktop/src/main/updater/store.ts), but in a separate, unpatched directoryIsEmpty() in this package's own fs-io.ts. It's reachable from the same desktop updater via downloadsRoot (downloadCopyAndClear -> managedDownload -> ensureManagedBase). Add a package-local OS_MANAGED_ROOT_ARTIFACTS allowlist (duplicated rather than shared with apps/desktop, since apps/desktop depends on this package and not the reverse) and apply it before deciding a fresh base has real, blocking content. Verified via two independent adversarial reviews (Claude Code and Codex on gpt-5.6-terra) that no other rejection path in this package (remove.ts, prune.ts, run.ts, manifest.ts, lock.ts, transfer.ts) has the same unpatched pattern. Fixes #7266.
This commit is contained in:
@@ -13,7 +13,26 @@ import { join } from "node:path";
|
||||
|
||||
import { LOCK_DIR, PARTIAL_DIR, STATE_DIR, STORE_KIND, STORE_SCHEMA_VERSION, STORE_SENTINEL } from "./constants.js";
|
||||
import { MANAGED_DOWNLOAD_ERROR_CODES, ManagedDownloadError } from "./errors.js";
|
||||
import { directoryIsEmpty, readJson, writeJson } from "./fs-io.js";
|
||||
import { readJson, writeJson } from "./fs-io.js";
|
||||
|
||||
/**
|
||||
* @internal Filesystem-browser artifacts the OS drops into any directory it
|
||||
* renders, independent of anything this package does. Their mere presence
|
||||
* must never be treated as "not empty": Finder writes .DS_Store the moment a
|
||||
* managed base is viewed even once, and Explorer does the same with
|
||||
* Thumbs.db/desktop.ini. Mirrors apps/desktop's isOsManagedRootArtifact —
|
||||
* duplicated rather than shared since apps/desktop depends on this package,
|
||||
* not the reverse.
|
||||
*/
|
||||
const OS_MANAGED_ROOT_ARTIFACTS = new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".localized"]);
|
||||
|
||||
/**
|
||||
* @internal Whether `name` is a benign, OS-managed root artifact that should
|
||||
* never count as real content when deciding whether a directory is claimable.
|
||||
*/
|
||||
function isOsManagedRootArtifact(name: string): boolean {
|
||||
return OS_MANAGED_ROOT_ARTIFACTS.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal On-disk ownership marker written at the root of a managed base.
|
||||
@@ -84,7 +103,9 @@ export async function ensureManagedBase(basePath: string): Promise<void> {
|
||||
const sentinelPath = join(basePath, STORE_SENTINEL);
|
||||
const sentinel = await readJson<unknown>(sentinelPath);
|
||||
if (sentinel == null) {
|
||||
if (!(await directoryIsEmpty(basePath))) {
|
||||
const entries = await readdir(basePath);
|
||||
const content = entries.filter((name) => !isOsManagedRootArtifact(name));
|
||||
if (content.length > 0) {
|
||||
throw new ManagedDownloadError(MANAGED_DOWNLOAD_ERROR_CODES.STORE_NOT_OWNED, `download base is not empty and has no ownership marker: ${basePath}`);
|
||||
}
|
||||
await writeSentinel(basePath);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
@@ -439,4 +439,46 @@ describe("managed download package", () => {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([".DS_Store", "Thumbs.db", "desktop.ini", ".localized"])(
|
||||
"claims a fresh managed base that contains only the OS-managed artifact %s",
|
||||
async (artifactName) => {
|
||||
const root = tmpRoot("os-artifact-fresh-claim");
|
||||
const basePath = join(root, "downloads");
|
||||
mkdirSync(basePath, { recursive: true });
|
||||
writeFileSync(join(basePath, artifactName), "binary-os-junk");
|
||||
|
||||
const pruned = await pruneManagedDownloads({ basePath });
|
||||
|
||||
expect(pruned.removed).toBe(0);
|
||||
expect(existsSync(join(basePath, ".open-design-download-root.json"))).toBe(true);
|
||||
expect(existsSync(join(basePath, artifactName))).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("still rejects a fresh managed base containing a genuine stray file alongside .DS_Store", async () => {
|
||||
const root = tmpRoot("os-artifact-stray-file");
|
||||
const basePath = join(root, "downloads");
|
||||
mkdirSync(basePath, { recursive: true });
|
||||
writeFileSync(join(basePath, ".DS_Store"), "binary-finder-junk");
|
||||
writeFileSync(join(basePath, "stray-file.bin"), "junk");
|
||||
|
||||
await expect(pruneManagedDownloads({ basePath })).rejects.toMatchObject({
|
||||
code: MANAGED_DOWNLOAD_ERROR_CODES.STORE_NOT_OWNED,
|
||||
});
|
||||
expect(existsSync(join(basePath, ".open-design-download-root.json"))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not extend the OS-artifact allowance to arbitrary directories such as a stray .git", async () => {
|
||||
const root = tmpRoot("os-artifact-stray-dir");
|
||||
const basePath = join(root, "downloads");
|
||||
mkdirSync(basePath, { recursive: true });
|
||||
writeFileSync(join(basePath, ".DS_Store"), "binary-finder-junk");
|
||||
mkdirSync(join(basePath, ".git"));
|
||||
|
||||
await expect(pruneManagedDownloads({ basePath })).rejects.toMatchObject({
|
||||
code: MANAGED_DOWNLOAD_ERROR_CODES.STORE_NOT_OWNED,
|
||||
});
|
||||
expect(existsSync(join(basePath, ".open-design-download-root.json"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user