fix: preserve external output path spelling (#7) (thanks @jesse-merhi)

This commit is contained in:
Peter Steinberger 2026-05-07 03:03:33 +01:00
parent a6c7e86dc2
commit d144817e7c
No known key found for this signature in database
3 changed files with 23 additions and 2 deletions

View File

@ -4,6 +4,7 @@
### Fixes
- Add `writeExternalFileWithinRoot()` for libraries that require an output path while preserving caller-provided destination names. (#7; thanks @jesse-merhi)
- Reject `fileStore()` and `fileStoreSync()` writes through symlinked parent directories so store commits cannot escape the configured root.
- Harden Root fallback mutators, archive merges, private store reads/writes, durable queue ids, JSON fallback writes, sibling temp writes, temp filename sanitization, and trash moves against symlink-swap and path traversal edge cases.
- Centralize safe path segment validation, directory identity guards, and guarded mutation wrappers so future filesystem helpers reuse the same race-resistant checks.

View File

@ -65,8 +65,8 @@ export async function writeExternalFileWithinRoot<T = void>(
options: ExternalFileWriteOptions<T>,
): Promise<ExternalFileWriteResult<T>> {
const targetRoot = await root(options.rootDir);
const requestedTargetPath = options.path.trim();
if (!requestedTargetPath) {
const requestedTargetPath = options.path;
if (requestedTargetPath.length === 0) {
throw new FsSafeError("invalid-path", "target path is required");
}
const targetPath = toRootPathInput({

View File

@ -42,6 +42,26 @@ describe("writeExternalFileWithinRoot", () => {
await expect(fs.stat(tempPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("preserves caller-provided destination filename spacing", async () => {
const rootDir = await tempRoot("fs-safe-output-spaces-");
const fileName = " report .txt ";
const result = await writeExternalFileWithinRoot({
rootDir,
path: fileName,
write: async (candidate) => {
await fs.writeFile(candidate, "spaced", "utf8");
},
});
const finalPath = path.join(rootDir, fileName);
expect(result.path).toBe(path.join(await fs.realpath(rootDir), fileName));
await expect(fs.readFile(finalPath, "utf8")).resolves.toBe("spaced");
await expect(fs.stat(path.join(rootDir, fileName.trim()))).rejects.toMatchObject({
code: "ENOENT",
});
});
it("rejects empty target paths before invoking the external writer", async () => {
const rootDir = await tempRoot("fs-safe-output-default-");
let called = false;