mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
Sanitize Windows-invalid characters in exported filenames (#12407)
* Sanitize Windows-invalid ZIP filename characters Agent-Logs-Url: https://github.com/outline/outline/sessions/539082bc-597f-463d-b77c-6eb1bcf9bffa Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * Refine Windows filename sanitization regex handling Agent-Logs-Url: https://github.com/outline/outline/sessions/539082bc-597f-463d-b77c-6eb1bcf9bffa Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * PR feedback --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
@@ -19,6 +19,18 @@ describe("serializeFilename", () => {
|
||||
"this %5C and %5C this"
|
||||
);
|
||||
});
|
||||
|
||||
it("should serialize Windows-invalid filename characters", () => {
|
||||
expect(serializeFilename(`this: * ? " < > | that`)).toBe(
|
||||
"this%3A %2A %3F %22 %3C %3E %7C that"
|
||||
);
|
||||
});
|
||||
|
||||
it("should serialize trailing Windows-invalid filename characters", () => {
|
||||
expect(serializeFilename("file.")).toBe("file%2E");
|
||||
expect(serializeFilename("file ")).toBe("file%20");
|
||||
expect(serializeFilename("file. ")).toBe("file%2E%20");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deserializeFilename", () => {
|
||||
@@ -35,6 +47,18 @@ describe("deserializeFilename", () => {
|
||||
`this \\ and \\ this`
|
||||
);
|
||||
});
|
||||
|
||||
it("should deserialize Windows-invalid filename characters", () => {
|
||||
expect(deserializeFilename("%3A %2A %3F %22 %3C %3E %7C")).toBe(
|
||||
`: * ? " < > |`
|
||||
);
|
||||
});
|
||||
|
||||
it("should deserialize trailing Windows-invalid filename characters", () => {
|
||||
expect(deserializeFilename("file%2E")).toBe("file.");
|
||||
expect(deserializeFilename("file%20")).toBe("file ");
|
||||
expect(deserializeFilename("file%2E%20")).toBe("file. ");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stringByteLength", () => {
|
||||
|
||||
+39
-2
@@ -1,6 +1,33 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
|
||||
const windowsInvalidFileNameCharsRegex = /[\\/:*?"<>|]/g;
|
||||
const windowsTrailingFileNameCharsRegex = /[. ]+$/g;
|
||||
const encodedWindowsCharacters: Record<string, string> = {
|
||||
"%2F": "/",
|
||||
"%5C": "\\",
|
||||
"%3A": ":",
|
||||
"%2A": "*",
|
||||
"%3F": "?",
|
||||
"%22": '"',
|
||||
"%3C": "<",
|
||||
"%3E": ">",
|
||||
"%7C": "|",
|
||||
"%2E": ".",
|
||||
"%20": " ",
|
||||
};
|
||||
const encodedWindowsCharactersRegex = /%(?:2F|5C|3A|2A|3F|22|3C|3E|7C|2E|20)/gi;
|
||||
|
||||
/**
|
||||
* Encodes a single character to uppercase percent-encoding.
|
||||
*
|
||||
* @param char The character to encode.
|
||||
* @returns The encoded character.
|
||||
*/
|
||||
function encodeWindowsUnsafeCharacter(char: string): string {
|
||||
return `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a file name for inclusion in a ZIP.
|
||||
*
|
||||
@@ -8,7 +35,14 @@ import fs from "fs-extra";
|
||||
* @returns The serialized file name.
|
||||
*/
|
||||
export function serializeFilename(text: string): string {
|
||||
return text.replace(/\//g, "%2F").replace(/\\/g, "%5C");
|
||||
const encoded = text.replace(
|
||||
windowsInvalidFileNameCharsRegex,
|
||||
encodeWindowsUnsafeCharacter
|
||||
);
|
||||
|
||||
return encoded.replace(windowsTrailingFileNameCharsRegex, (trailing) =>
|
||||
trailing.split("").map(encodeWindowsUnsafeCharacter).join("")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,7 +52,10 @@ export function serializeFilename(text: string): string {
|
||||
* @returns The deserialized file name.
|
||||
*/
|
||||
export function deserializeFilename(text: string): string {
|
||||
return text.replace(/%2F/g, "/").replace(/%5C/g, "\\");
|
||||
return text.replace(
|
||||
encodedWindowsCharactersRegex,
|
||||
(match) => encodedWindowsCharacters[match.toUpperCase()] ?? match
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user