Skip to content

Commit 8679ce0

Browse files
committed
fix: enforce cross-platform folder rules
1 parent ecb75c4 commit 8679ce0

File tree

1 file changed

+64
-13
lines changed

1 file changed

+64
-13
lines changed

src/engine/TemplateEngine.ts

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,31 @@ type FolderSelection = {
4747
isEmpty: boolean;
4848
};
4949

50+
const RESERVED_WINDOWS_DEVICE_NAMES = new Set([
51+
"CON",
52+
"PRN",
53+
"AUX",
54+
"NUL",
55+
"COM1",
56+
"COM2",
57+
"COM3",
58+
"COM4",
59+
"COM5",
60+
"COM6",
61+
"COM7",
62+
"COM8",
63+
"COM9",
64+
"LPT1",
65+
"LPT2",
66+
"LPT3",
67+
"LPT4",
68+
"LPT5",
69+
"LPT6",
70+
"LPT7",
71+
"LPT8",
72+
"LPT9",
73+
]);
74+
5075
class InvalidFolderPathError extends Error {
5176
constructor(message: string) {
5277
super(message);
@@ -282,22 +307,48 @@ export abstract class TemplateEngine extends QuickAddEngine {
282307

283308
const segments = trimmed.split("/");
284309
for (const segment of segments) {
285-
if (!segment) {
286-
throw new InvalidFolderPathError("Folder name cannot be empty.");
287-
}
310+
this.validateFolderSegment(segment);
311+
}
312+
}
288313

289-
if (segment === "." || segment === "..") {
290-
throw new InvalidFolderPathError(
291-
"Folder name cannot be '.' or '..'.",
292-
);
293-
}
314+
private validateFolderSegment(segment: string): void {
315+
if (!segment) {
316+
throw new InvalidFolderPathError("Folder name cannot be empty.");
317+
}
294318

295-
if (/[\\:]/u.test(segment)) {
296-
throw new InvalidFolderPathError(
297-
"Folder name cannot contain any of the following characters: \\ / :",
298-
);
299-
}
319+
if (segment === "." || segment === "..") {
320+
throw new InvalidFolderPathError("Folder name cannot be '.' or '..'.");
321+
}
322+
323+
if (/[\u0000-\u001F]/u.test(segment)) {
324+
throw new InvalidFolderPathError(
325+
"Folder name cannot contain control characters.",
326+
);
300327
}
328+
329+
if (/[\\/:*?"<>|]/u.test(segment)) {
330+
throw new InvalidFolderPathError(
331+
"Folder name cannot contain any of the following characters: \\ / : * ? \" < > |",
332+
);
333+
}
334+
335+
if (/[. ]$/u.test(segment)) {
336+
throw new InvalidFolderPathError(
337+
"Folder name cannot end with a space or a period.",
338+
);
339+
}
340+
341+
const normalized = segment.replace(/[. ]+$/u, "");
342+
const base = normalized.split(".")[0]?.toUpperCase();
343+
if (base && this.isReservedWindowsName(base)) {
344+
throw new InvalidFolderPathError(
345+
"Folder name cannot be a reserved name like CON, PRN, AUX, NUL, COM1-9, or LPT1-9.",
346+
);
347+
}
348+
}
349+
350+
private isReservedWindowsName(name: string): boolean {
351+
return RESERVED_WINDOWS_DEVICE_NAMES.has(name);
301352
}
302353

303354
private isPathAllowed(path: string, roots: string[]): boolean {

0 commit comments

Comments
 (0)