check / check (push) Successful in 37s
A file title or album name decrypted from server data could name a path outside the chosen directory (`../../.ssh/authorized_keys`). One module, src/filename.ts, now makes such names safe for `quak get`/`get-thumb` without `--out`, downloadFile/downloadThumbnail without outPath, and the backup and metadata backup trees. Originals-cache extensions are limited to letters and digits. A user-supplied path is still used as is. decryptFile reads a missing or non-string title as "" and rejects metadata that is not a JSON object. Model: opus-5-5
95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
// The CLI presents a file by its own decrypted metadata, not the PhotoRecord
|
|
// projection (issue #52). For a renamed file the two disagree: the projection
|
|
// prefers `editedName` and reports `editedTime` in milliseconds, while the CLI
|
|
// must print the raw `metadata.title` and `metadata.creationTime` (microseconds)
|
|
// and name downloads after the raw title, byte-identical to the pre-library CLI.
|
|
//
|
|
// This locks in that contrast: the shared output helpers emit the raw values,
|
|
// and the projection of the same file emits the edited ones — so a regression
|
|
// that re-sourced the CLI from the projection would fail here.
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import {
|
|
fileListRow,
|
|
fileListLine,
|
|
originalName,
|
|
thumbnailName,
|
|
} from "../../src/cli-output.js";
|
|
import { deriveRecords } from "../../src/library/records.js";
|
|
import type { EnteFile } from "../../src/model/types.js";
|
|
|
|
// Microseconds, as Ente stores times.
|
|
const RAW_CREATION = 1700000000000000;
|
|
const EDITED_TIME = 1710000000000000;
|
|
const RAW_TITLE = "IMG_0001.HEIC";
|
|
const EDITED_NAME = "Sunset.heic";
|
|
|
|
// A file the user has renamed and re-dated: basic metadata holds the original
|
|
// title and capture time; public magic metadata holds the edits.
|
|
const renamedFile: EnteFile = {
|
|
id: 100,
|
|
collectionID: 10,
|
|
ownerID: 42,
|
|
key: new Uint8Array(),
|
|
metadata: {
|
|
title: RAW_TITLE,
|
|
fileType: "image",
|
|
creationTime: RAW_CREATION,
|
|
modificationTime: RAW_CREATION,
|
|
},
|
|
pubMagicMetadata: { editedName: EDITED_NAME, editedTime: EDITED_TIME },
|
|
file: { decryptionHeader: "" },
|
|
thumbnail: { decryptionHeader: "" },
|
|
updationTime: RAW_CREATION,
|
|
};
|
|
|
|
describe("CLI file output (issue #52)", () => {
|
|
it("emits the raw title and microsecond creationTime for --json", () => {
|
|
expect(fileListRow(renamedFile)).toEqual({
|
|
id: 100,
|
|
title: RAW_TITLE,
|
|
fileType: "image",
|
|
creationTime: RAW_CREATION,
|
|
collectionID: 10,
|
|
});
|
|
});
|
|
|
|
it("emits the raw title in the human column", () => {
|
|
expect(fileListLine(renamedFile)).toBe(`100\timage\t${RAW_TITLE}`);
|
|
});
|
|
|
|
it("names downloads after the raw title", () => {
|
|
expect(originalName(renamedFile)).toBe(RAW_TITLE);
|
|
expect(thumbnailName(renamedFile)).toBe(`thumb_${RAW_TITLE}`);
|
|
});
|
|
|
|
it("sanitizes the title when naming `quak get` downloads", () => {
|
|
// Without `--out`, the server-supplied title names the file, so it must
|
|
// not be able to point outside the working directory.
|
|
const hostile = {
|
|
...renamedFile,
|
|
metadata: { ...renamedFile.metadata, title: "../../.bashrc" },
|
|
};
|
|
expect(originalName(hostile)).toBe("__.._.bashrc");
|
|
expect(thumbnailName(hostile)).toBe("thumb___.._.bashrc");
|
|
|
|
const untitled = {
|
|
...renamedFile,
|
|
metadata: { ...renamedFile.metadata, title: "" },
|
|
};
|
|
expect(originalName(untitled)).toBe("file-100");
|
|
expect(thumbnailName(untitled)).toBe("thumb_file-100");
|
|
});
|
|
|
|
it("does not use the editedName/editedTime projection", () => {
|
|
const record = deriveRecords([], [renamedFile]).photos.get(100);
|
|
// The projection prefers the edits and reports milliseconds; the CLI
|
|
// helpers above deliberately do not.
|
|
expect(record?.title).toBe(EDITED_NAME);
|
|
expect(record?.takenAt).toBe(Math.floor(EDITED_TIME / 1000));
|
|
expect(fileListRow(renamedFile).title).not.toBe(record?.title);
|
|
expect(fileListRow(renamedFile).creationTime).not.toBe(record?.takenAt);
|
|
});
|
|
});
|