Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit b9e9ae9

Browse files
author
Akos Kitta
committed
fix: validate against trailing dot + reserved name
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 7b4ef9e commit b9e9ae9

File tree

4 files changed

+96
-1
lines changed

4 files changed

+96
-1
lines changed

‎arduino-ide-extension/package.json‎

Lines changed: 1 addition & 0 deletions
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",

‎arduino-ide-extension/src/common/protocol/sketches-service.ts‎

Lines changed: 52 additions & 0 deletions
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)

‎arduino-ide-extension/src/test/common/sketches-service.test.ts‎

Lines changed: 41 additions & 1 deletion
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)

‎i18n/en.json‎

Lines changed: 2 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /