@@ -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+
5075class 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