Skip to content

Commit fef3b6a

Browse files
author
Akos Kitta
committed
fix: validate against trailing dot + reserved name
Signed-off-by: Akos Kitta <[email protected]>
1 parent ccbca88 commit fef3b6a

File tree

4 files changed

+96
-1
lines changed

4 files changed

+96
-1
lines changed

Diff for: arduino-ide-extension/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"electron-updater": "^4.6.5",
7474
"fast-json-stable-stringify": "^2.1.0",
7575
"fast-safe-stringify": "^2.1.1",
76+
"filename-reserved-regex": "^2.0.0",
7677
"glob": "^7.1.6",
7778
"google-protobuf": "^3.20.1",
7879
"hash.js": "^1.1.7",

Diff for: arduino-ide-extension/src/common/protocol/sketches-service.ts

+52
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ApplicationError } from '@theia/core/lib/common/application-error';
22
import { nls } from '@theia/core/lib/common/nls';
33
import URI from '@theia/core/lib/common/uri';
44
import * as dateFormat from 'dateformat';
5+
const filenameReservedRegex = require('filename-reserved-regex');
56

67
export namespace SketchesError {
78
export const Codes = {
@@ -160,6 +161,19 @@ export namespace Sketch {
160161
// (non-API) exported for the tests
161162
export const defaultFallbackChar = '_';
162163
// (non-API) exported for the tests
164+
export function reservedFilename(name: string): string {
165+
return nls.localize(
166+
'arduino/sketch/reservedFilename',
167+
"'{0}' is a reserved filename.",
168+
name
169+
);
170+
}
171+
// (non-API) exported for the tests
172+
export const noTrailingPeriod = nls.localize(
173+
'arduino/sketch/noTrailingPeriod',
174+
'A filename cannot end with a dot'
175+
);
176+
// (non-API) exported for the tests
163177
export const invalidSketchFolderNameMessage = nls.localize(
164178
'arduino/sketch/invalidSketchName',
165179
'The name must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters.'
@@ -175,6 +189,10 @@ export namespace Sketch {
175189
export function validateSketchFolderName(
176190
candidate: string
177191
): string | undefined {
192+
const validFilenameError = isValidFilename(candidate);
193+
if (validFilenameError) {
194+
return validFilenameError;
195+
}
178196
return /^[0-9a-zA-Z]{1}[0-9a-zA-Z_\.-]{0,62}$/.test(candidate)
179197
? undefined
180198
: invalidSketchFolderNameMessage;
@@ -186,11 +204,36 @@ export namespace Sketch {
186204
export function validateCloudSketchFolderName(
187205
candidate: string
188206
): string | undefined {
207+
const validFilenameError = isValidFilename(candidate);
208+
if (validFilenameError) {
209+
return validFilenameError;
210+
}
189211
return /^[0-9a-zA-Z]{1}[0-9a-zA-Z_\.-]{0,35}$/.test(candidate)
190212
? undefined
191213
: invalidCloudSketchFolderNameMessage;
192214
}
193215

216+
function isValidFilename(candidate: string): string | undefined {
217+
if (isReservedFilename(candidate)) {
218+
return reservedFilename(candidate);
219+
}
220+
if (endsWithPeriod(candidate)) {
221+
return noTrailingPeriod;
222+
}
223+
return undefined;
224+
}
225+
226+
function endsWithPeriod(candidate: string): boolean {
227+
return candidate.length > 1 && candidate[candidate.length - 1] === '.';
228+
}
229+
230+
function isReservedFilename(candidate: string): boolean {
231+
return (
232+
filenameReservedRegex().test(candidate) ||
233+
filenameReservedRegex.windowsNames().test(candidate)
234+
);
235+
}
236+
194237
/**
195238
* Transforms the `candidate` argument into a valid sketch folder name by replacing all invalid characters with underscore (`_`) and trimming the string after 63 characters.
196239
* If the argument is falsy, returns with `"sketch"`.
@@ -202,6 +245,12 @@ export namespace Sketch {
202245
*/
203246
appendTimestampSuffix: boolean | Date = false
204247
): string {
248+
if (
249+
!appendTimestampSuffix &&
250+
filenameReservedRegex.windowsNames().test(candidate)
251+
) {
252+
return defaultSketchFolderName;
253+
}
205254
const validName = candidate
206255
? candidate
207256
.replace(/^[^0-9a-zA-Z]{1}/g, defaultFallbackFirstChar)
@@ -230,6 +279,9 @@ export namespace Sketch {
230279
* Transforms the `candidate` argument into a valid cloud sketch folder name by replacing all invalid characters with underscore and trimming the string after 36 characters.
231280
*/
232281
export function toValidCloudSketchFolderName(candidate: string): string {
282+
if (filenameReservedRegex.windowsNames().test(candidate)) {
283+
return defaultSketchFolderName;
284+
}
233285
return candidate
234286
? candidate
235287
.replace(/^[^0-9a-zA-Z]{1}/g, defaultFallbackFirstChar)

Diff for: arduino-ide-extension/src/test/common/sketches-service.test.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,47 @@
11
import { expect } from 'chai';
22
import { Sketch } from '../../common/protocol';
33

4+
const windowsReservedFileNames = [
5+
'CON',
6+
'PRN',
7+
'AUX',
8+
'NUL',
9+
'COM1',
10+
'COM2',
11+
'COM3',
12+
'COM4',
13+
'COM5',
14+
'COM6',
15+
'COM7',
16+
'COM8',
17+
'COM9',
18+
'LPT1',
19+
'LPT2',
20+
'LPT3',
21+
'LPT4',
22+
'LPT5',
23+
'LPT6',
24+
'LPT7',
25+
'LPT8',
26+
'LPT9',
27+
];
28+
const windowsInvalidFilenames = ['trailingPeriod.', 'trailingSpace '];
29+
const invalidFilenames = [
30+
...windowsInvalidFilenames,
31+
...windowsReservedFileNames,
32+
].map((name) => <[string, boolean]>[name, false]);
33+
434
describe('sketch', () => {
535
describe('validateSketchFolderName', () => {
636
(
737
[
38+
...invalidFilenames,
39+
['com1', false], // Do not assume case sensitivity. (https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions)
840
['sketch', true],
941
['can-contain-slash-and-dot.ino', true],
1042
['regex++', false],
11-
['dots...', true],
43+
['trailing.dots...', false],
44+
['no.trailing.dots.._', true],
1245
['No Spaces', false],
1346
['_invalidToStartWithUnderscore', false],
1447
['Invalid+Char.ino', false],
@@ -42,6 +75,7 @@ describe('sketch', () => {
4275
describe('validateCloudSketchFolderName', () => {
4376
(
4477
[
78+
...invalidFilenames,
4579
['sketch', true],
4680
['can-contain-dashes', true],
4781
['can.contain.dots', true],
@@ -83,6 +117,9 @@ describe('sketch', () => {
83117
['foo bar', 'foo_bar'],
84118
['_foobar', '0foobar'],
85119
['vAlid', 'vAlid'],
120+
['COM1', Sketch.defaultSketchFolderName],
121+
['COM1.', 'COM1_'],
122+
['period.', 'period_'],
86123
].map(([input, expected]) =>
87124
toMapIt(input, expected, Sketch.toValidSketchFolderName)
88125
);
@@ -111,6 +148,9 @@ describe('sketch', () => {
111148
['fooBar-', 'fooBar_' + epochSuffix],
112149
['fooBar+', 'fooBar_' + epochSuffix],
113150
['vAlid', 'vAlid' + epochSuffix],
151+
['COM1', 'COM1' + epochSuffix],
152+
['COM1.', 'COM1_' + epochSuffix],
153+
['period.', 'period_' + epochSuffix],
114154
].map(([input, expected]) =>
115155
toMapIt(input, expected, (input: string) =>
116156
Sketch.toValidSketchFolderName(input, epoch)

Diff for: i18n/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -419,9 +419,11 @@
419419
"moving": "Moving",
420420
"movingMsg": "The file \"{0}\" needs to be inside a sketch folder named \"{1}\".\nCreate this folder, move the file, and continue?",
421421
"new": "New Sketch",
422+
"noTrailingPeriod": "A filename cannot end with a dot",
422423
"openFolder": "Open Folder",
423424
"openRecent": "Open Recent",
424425
"openSketchInNewWindow": "Open Sketch in New Window",
426+
"reservedFilename": "'{0}' is a reserved filename.",
425427
"saveFolderAs": "Save sketch folder as...",
426428
"saveSketch": "Save your sketch to open it again later.",
427429
"saveSketchAs": "Save sketch folder as...",

0 commit comments

Comments
 (0)