diff --git a/extensions/data-workspace/src/common/constants.ts b/extensions/data-workspace/src/common/constants.ts index 1b544e677b..0173be6b24 100644 --- a/extensions/data-workspace/src/common/constants.ts +++ b/extensions/data-workspace/src/common/constants.ts @@ -67,7 +67,7 @@ export const whitespaceFilenameErrorMessage = localize('whitespaceFilenameErrorM export const invalidFileCharsErrorMessage = localize('invalidFileCharsErrorMessage', "Invalid file characters"); export const reservedWindowsFilenameErrorMessage = localize('reservedWindowsFilenameErrorMessage', "This file name is reserved for use by Windows. Choose another name and try again"); export const reservedValueErrorMessage = localize('reservedValueErrorMessage', "Reserved file name. Choose another name and try again"); -export const trailingWhitespaceErrorMessage = localize('trailingWhitespaceErrorMessage', "File name cannot end with a whitespace"); +export const trailingWhitespaceErrorMessage = localize('trailingWhitespaceErrorMessage', "File name cannot start or end with whitespace"); export const tooLongFilenameErrorMessage = localize('tooLongFilenameErrorMessage', "File name cannot be over 255 characters"); //Open Existing Dialog diff --git a/extensions/data-workspace/src/common/dataWorkspaceExtension.ts b/extensions/data-workspace/src/common/dataWorkspaceExtension.ts index 103321002e..03981aa9ed 100644 --- a/extensions/data-workspace/src/common/dataWorkspaceExtension.ts +++ b/extensions/data-workspace/src/common/dataWorkspaceExtension.ts @@ -59,7 +59,7 @@ export class DataWorkspaceExtension implements IExtension { return isValidBasename(name); } - isValidBasenameErrorMessage(name?: string): string { + isValidBasenameErrorMessage(name?: string): string | undefined { return isValidBasenameErrorMessage(name); } diff --git a/extensions/data-workspace/src/common/pathUtilsHelper.ts b/extensions/data-workspace/src/common/pathUtilsHelper.ts index ce81de5461..e0fada4e57 100644 --- a/extensions/data-workspace/src/common/pathUtilsHelper.ts +++ b/extensions/data-workspace/src/common/pathUtilsHelper.ts @@ -5,7 +5,6 @@ import * as constants from './constants'; import * as os from 'os'; -import * as path from 'path'; const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g; const UNIX_INVALID_FILE_CHARS = /[\\/]/g; @@ -49,88 +48,57 @@ export function sanitizeStringForFilename(s: string): string { /** * Returns true if the string is a valid filename * Logic is copied from src\vs\base\common\extpath.ts - * @param name filename to check + * @param fileName filename to check (without file path) */ -export function isValidBasename(name?: string): boolean { - const invalidFileChars = isWindows ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; - - if (!name) { - return false; +export function isValidBasename(fileName?: string): boolean { + if (isValidBasenameErrorMessage(fileName) !== undefined) { + return false; //Return false depicting filename is invalid + } else { + return true; } - if (isWindows && name[name.length - 1] === '.') { - return false; // Windows: file cannot end with a "." - } - let basename = path.parse(name).name; - if (!basename || basename.length === 0 || /^\s+$/.test(basename)) { - return false; // require a name that is not just whitespace - } - - invalidFileChars.lastIndex = 0; - if (invalidFileChars.test(basename)) { - return false; // check for certain invalid file characters - } - - if (isWindows && WINDOWS_FORBIDDEN_NAMES.test(basename)) { - return false; // check for certain invalid file names - } - - if (basename === '.' || basename === '..') { - return false; // check for reserved values - } - - if (isWindows && basename.length !== basename.trim().length) { - return false; // Windows: file cannot end with a whitespace - } - - if (basename.length > 255) { - return false; // most file systems do not allow files > 255 length - } - - return true; } /** - * Returns specific error message if file name is invalid + * Returns specific error message if file name is invalid otherwise returns undefined * Logic is copied from src\vs\base\common\extpath.ts - * @param name filename to check + * @param fileName filename to check (without file path) */ -export function isValidBasenameErrorMessage(name?: string): string { +export function isValidBasenameErrorMessage(fileName?: string): string | undefined { const invalidFileChars = isWindows ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; - if (!name) { + if (!fileName) { return constants.undefinedFilenameErrorMessage; } - if (isWindows && name[name.length - 1] === '.') { + if (isWindows && fileName[fileName.length - 1] === '.') { return constants.filenameEndingIsPeriodErrorMessage; // Windows: file cannot end with a "." } - let basename = path.parse(name).name; - if (!basename || basename.length === 0 || /^\s+$/.test(basename)) { + if (!fileName || fileName.length === 0 || /^\s+$/.test(fileName)) { return constants.whitespaceFilenameErrorMessage; // require a name that is not just whitespace } invalidFileChars.lastIndex = 0; - if (invalidFileChars.test(basename)) { + if (invalidFileChars.test(fileName)) { return constants.invalidFileCharsErrorMessage; // check for certain invalid file characters } - if (isWindows && WINDOWS_FORBIDDEN_NAMES.test(basename)) { + if (isWindows && WINDOWS_FORBIDDEN_NAMES.test(fileName)) { return constants.reservedWindowsFilenameErrorMessage; // check for certain invalid file names } - if (basename === '.' || basename === '..') { + if (fileName === '.' || fileName === '..') { return constants.reservedValueErrorMessage; // check for reserved values } - if (isWindows && basename.length !== basename.trim().length) { - return constants.trailingWhitespaceErrorMessage; // Windows: file cannot end with a whitespace + if (isWindows && fileName.length !== fileName.trim().length) { + return constants.trailingWhitespaceErrorMessage; // Windows: file cannot start or end with a whitespace } - if (basename.length > 255) { + if (fileName.length > 255) { return constants.tooLongFilenameErrorMessage; // most file systems do not allow files > 255 length } - return ''; + return undefined; } diff --git a/extensions/data-workspace/src/dataworkspace.d.ts b/extensions/data-workspace/src/dataworkspace.d.ts index e774b22423..56de84102b 100644 --- a/extensions/data-workspace/src/dataworkspace.d.ts +++ b/extensions/data-workspace/src/dataworkspace.d.ts @@ -75,11 +75,11 @@ declare module 'dataworkspace' { isValidBasename(name: string | null | undefined): boolean; /** - * Returns specific error message if file name is invalid + * Returns specific error message if file name is invalid otherwise returns undefined * Logic is copied from src\vs\base\common\extpath.ts * @param name filename to check */ - isValidBasenameErrorMessage(name: string | null | undefined): string; + isValidBasenameErrorMessage(name: string | null | undefined): string | undefined; } /** diff --git a/extensions/data-workspace/src/dialogs/newProjectDialog.ts b/extensions/data-workspace/src/dialogs/newProjectDialog.ts index 99f7bba00a..0f01e38edf 100644 --- a/extensions/data-workspace/src/dialogs/newProjectDialog.ts +++ b/extensions/data-workspace/src/dialogs/newProjectDialog.ts @@ -187,7 +187,7 @@ export class NewProjectDialog extends DialogBase { this.register(projectNameTextBox.onTextChanged(text => { const errorMessage = isValidBasenameErrorMessage(text); - if (errorMessage) { + if (errorMessage !== undefined) { // Set validation error message if project name is invalid return void projectNameTextBox.updateProperty('validationErrorMessage', errorMessage); } else { diff --git a/extensions/data-workspace/src/dialogs/newProjectQuickpick.ts b/extensions/data-workspace/src/dialogs/newProjectQuickpick.ts index fae6a7ca96..3b07392d9f 100644 --- a/extensions/data-workspace/src/dialogs/newProjectQuickpick.ts +++ b/extensions/data-workspace/src/dialogs/newProjectQuickpick.ts @@ -9,7 +9,7 @@ import * as constants from '../common/constants'; import { directoryExist, showInfoMessageWithLearnMoreLink } from '../common/utils'; import { defaultProjectSaveLocation } from '../common/projectLocationHelper'; import { WorkspaceService } from '../services/workspaceService'; -import { isValidBasename, isValidBasenameErrorMessage } from '../common/pathUtilsHelper'; +import { isValidBasenameErrorMessage } from '../common/pathUtilsHelper'; /** * Create flow for a New Project using only VS Code-native APIs such as QuickPick @@ -40,7 +40,7 @@ export async function createNewProjectWithQuickpick(workspaceService: WorkspaceS { title: constants.EnterProjectName, validateInput: (value) => { - return isValidBasename(value) ? undefined : isValidBasenameErrorMessage(value); + return isValidBasenameErrorMessage(value); }, ignoreFocusOut: true }); diff --git a/extensions/data-workspace/src/test/dialogs/pathUtilsHelper.test.ts b/extensions/data-workspace/src/test/dialogs/pathUtilsHelper.test.ts index 1373b97db1..17029101c0 100644 --- a/extensions/data-workspace/src/test/dialogs/pathUtilsHelper.test.ts +++ b/extensions/data-workspace/src/test/dialogs/pathUtilsHelper.test.ts @@ -6,7 +6,6 @@ import * as should from 'should'; import * as constants from '../../common/constants'; import * as os from 'os'; -import * as path from 'path'; import { isValidBasename, isValidBasenameErrorMessage } from '../../common/pathUtilsHelper'; const isWindows = os.platform() === 'win32'; @@ -14,21 +13,21 @@ const isWindows = os.platform() === 'win32'; suite('Check for invalid filename tests', function (): void { test('Should determine invalid filenames', async () => { // valid filename - should(isValidBasename(formatFileName('ValidName.sqlproj'))).equal(true); + should(isValidBasename('ValidName')).equal(true); // invalid for both Windows and non-Windows let invalidNames: string[] = [ - ' .sqlproj', - ' .sqlproj', - ' .sqlproj', - '..sqlproj', - '...sqlproj', + ' ', + ' ', + ' ', + '.', + '..', // most file systems do not allow files > 255 length - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.sqlproj' + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ]; for (let invalidName of invalidNames) { - should(isValidBasename(formatFileName(invalidName))).equal(false); + should(isValidBasename(invalidName)).equal(false, `InvalidName that failed:${invalidName}`); } should(isValidBasename(undefined)).equal(false); @@ -39,52 +38,55 @@ suite('Check for invalid filename tests', function (): void { test('Should determine invalid Windows filenames', async () => { let invalidNames: string[] = [ // invalid characters only for Windows - '?.sqlproj', - ':.sqlproj', - '*.sqlproj', - '<.sqlproj', - '>.sqlproj', - '|.sqlproj', - '".sqlproj', - // Windows filenames cannot end with a whitespace - 'test .sqlproj', - 'test .sqlproj' + '?', + ':', + '*', + '<', + '>', + '|', + '"', + '/', + '\\', + // Windows filenames cannot start or end with a whitespace + 'test ', + 'test ', + ' test' ]; for (let invalidName of invalidNames) { - should(isValidBasename(formatFileName(invalidName))).equal(isWindows ? false : true); + should(isValidBasename(invalidName)).equal(isWindows ? false : true, `InvalidName that failed:${invalidName}`); } }); test('Should determine Windows forbidden filenames', async () => { let invalidNames: string[] = [ // invalid only for Windows - 'CON.sqlproj', - 'PRN.sqlproj', - 'AUX.sqlproj', - 'NUL.sqlproj', - 'COM1.sqlproj', - 'COM2.sqlproj', - 'COM3.sqlproj', - 'COM4.sqlproj', - 'COM5.sqlproj', - 'COM6.sqlproj', - 'COM7.sqlproj', - 'COM8.sqlproj', - 'COM9.sqlproj', - 'LPT1.sqlproj', - 'LPT2.sqlproj', - 'LPT3.sqlproj', - 'LPT4.sqlproj', - 'LPT5.sqlproj', - 'LPT6.sqlproj', - 'LPT7.sqlproj', - 'LPT8.sqlproj', - 'LPT9.sqlproj', + 'CON', + 'PRN', + 'AUX', + 'NUL', + 'COM1', + 'COM2', + 'COM3', + 'COM4', + 'COM5', + 'COM6', + 'COM7', + 'COM8', + 'COM9', + 'LPT1', + 'LPT2', + 'LPT3', + 'LPT4', + 'LPT5', + 'LPT6', + 'LPT7', + 'LPT8', + 'LPT9', ]; for (let invalidName of invalidNames) { - should(isValidBasename(formatFileName(invalidName))).equal(isWindows ? false : true); + should(isValidBasename(invalidName)).equal(isWindows ? false : true); } }); }); @@ -92,76 +94,77 @@ suite('Check for invalid filename tests', function (): void { suite('Check for invalid filename error tests', function (): void { test('Should determine invalid filenames', async () => { // valid filename - should(isValidBasenameErrorMessage(formatFileName('ValidName.sqlproj'))).equal(''); + should(isValidBasenameErrorMessage('ValidName')).equal(undefined); // invalid for both Windows and non-Windows - should(isValidBasenameErrorMessage(formatFileName(' .sqlproj'))).equal(constants.whitespaceFilenameErrorMessage); - should(isValidBasenameErrorMessage(formatFileName(' .sqlproj'))).equal(constants.whitespaceFilenameErrorMessage); - should(isValidBasenameErrorMessage(formatFileName(' .sqlproj'))).equal(constants.whitespaceFilenameErrorMessage); - should(isValidBasenameErrorMessage(formatFileName('..sqlproj'))).equal(constants.reservedValueErrorMessage); - should(isValidBasenameErrorMessage(formatFileName('...sqlproj'))).equal(constants.reservedValueErrorMessage); + should(isValidBasenameErrorMessage(' ')).equal(constants.whitespaceFilenameErrorMessage); + should(isValidBasenameErrorMessage(' ')).equal(constants.whitespaceFilenameErrorMessage); + should(isValidBasenameErrorMessage(' ')).equal(constants.whitespaceFilenameErrorMessage); + should(isValidBasenameErrorMessage('.')).equal(constants.filenameEndingIsPeriodErrorMessage); + should(isValidBasenameErrorMessage('..')).equal(constants.filenameEndingIsPeriodErrorMessage); should(isValidBasenameErrorMessage(undefined)).equal(constants.undefinedFilenameErrorMessage); - should(isValidBasenameErrorMessage('\\')).equal(isWindows ? constants.whitespaceFilenameErrorMessage : constants.invalidFileCharsErrorMessage); - should(isValidBasenameErrorMessage('/')).equal(constants.whitespaceFilenameErrorMessage); + should(isValidBasenameErrorMessage('\\')).equal(constants.invalidFileCharsErrorMessage); + should(isValidBasenameErrorMessage('/')).equal(constants.invalidFileCharsErrorMessage); + should(isValidBasenameErrorMessage(' ')).equal(constants.whitespaceFilenameErrorMessage); // most file systems do not allow files > 255 length - should(isValidBasenameErrorMessage(formatFileName('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.sqlproj'))).equal(constants.tooLongFilenameErrorMessage); + should(isValidBasenameErrorMessage('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).equal(constants.tooLongFilenameErrorMessage); }); test('Should determine invalid Windows filenames', async () => { let invalidNames: string[] = [ // invalid characters only for Windows - '?.sqlproj', - ':.sqlproj', - '*.sqlproj', - '<.sqlproj', - '>.sqlproj', - '|.sqlproj', - '".sqlproj' + '?', + ':', + '*', + '<', + '>', + '|', + '"', + '\\', + '/' ]; for (let invalidName of invalidNames) { - should(isValidBasenameErrorMessage(formatFileName(invalidName))).equal(isWindows ? constants.invalidFileCharsErrorMessage : ''); + should(isValidBasenameErrorMessage(invalidName)).equal(isWindows ? constants.invalidFileCharsErrorMessage : ''); } - // Windows filenames cannot end with a whitespace - should(isValidBasenameErrorMessage(formatFileName('test .sqlproj'))).equal(isWindows ? constants.trailingWhitespaceErrorMessage : ''); - should(isValidBasenameErrorMessage(formatFileName('test .sqlproj'))).equal(isWindows ? constants.trailingWhitespaceErrorMessage : ''); + // Windows filenames cannot start or end with a whitespace + should(isValidBasenameErrorMessage('test ')).equal(isWindows ? constants.trailingWhitespaceErrorMessage : ''); + should(isValidBasenameErrorMessage('test ')).equal(isWindows ? constants.trailingWhitespaceErrorMessage : ''); + should(isValidBasenameErrorMessage(' test')).equal(isWindows ? constants.trailingWhitespaceErrorMessage : ''); }); test('Should determine Windows forbidden filenames', async () => { let invalidNames: string[] = [ // invalid only for Windows - 'CON.sqlproj', - 'PRN.sqlproj', - 'AUX.sqlproj', - 'NUL.sqlproj', - 'COM1.sqlproj', - 'COM2.sqlproj', - 'COM3.sqlproj', - 'COM4.sqlproj', - 'COM5.sqlproj', - 'COM6.sqlproj', - 'COM7.sqlproj', - 'COM8.sqlproj', - 'COM9.sqlproj', - 'LPT1.sqlproj', - 'LPT2.sqlproj', - 'LPT3.sqlproj', - 'LPT4.sqlproj', - 'LPT5.sqlproj', - 'LPT6.sqlproj', - 'LPT7.sqlproj', - 'LPT8.sqlproj', - 'LPT9.sqlproj', + 'CON', + 'PRN', + 'AUX', + 'NUL', + 'COM1', + 'COM2', + 'COM3', + 'COM4', + 'COM5', + 'COM6', + 'COM7', + 'COM8', + 'COM9', + 'LPT1', + 'LPT2', + 'LPT3', + 'LPT4', + 'LPT5', + 'LPT6', + 'LPT7', + 'LPT8', + 'LPT9', ]; for (let invalidName of invalidNames) { - should(isValidBasenameErrorMessage(formatFileName(invalidName))).equal(isWindows ? constants.reservedWindowsFilenameErrorMessage : ''); + should(isValidBasenameErrorMessage(invalidName)).equal(isWindows ? constants.reservedWindowsFilenameErrorMessage : '', `InvalidName that failed:${invalidName}`); } }); }); -function formatFileName(filename: string): string { - return path.join(os.tmpdir(), filename); -} diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index aa3e626a3e..267e52a754 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -785,7 +785,7 @@ export function isValidBasename(name?: string): boolean { * Returns specific error message if file name is invalid * @param name filename to check */ -export function isValidBasenameErrorMessage(name?: string): string { +export function isValidBasenameErrorMessage(name?: string): string | undefined { return getDataWorkspaceExtensionApi().isValidBasenameErrorMessage(name); } diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 14cd7561b8..399ce328c9 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -677,7 +677,7 @@ export class ProjectsController { prompt: constants.newObjectNamePrompt(itemType.friendlyName), value: `${suggestedName}${counter}`, validateInput: (value) => { - return utils.isValidBasename(value) ? undefined : utils.isValidBasenameErrorMessage(value); + return utils.isValidBasenameErrorMessage(value); }, ignoreFocusOut: true, }); @@ -1339,7 +1339,7 @@ export class ProjectsController { prompt: constants.autorestProjectName, value: defaultName, validateInput: (value) => { - return utils.isValidBasename(value.trim()) ? undefined : utils.isValidBasenameErrorMessage(value.trim()); + return utils.isValidBasenameErrorMessage(value); } }); diff --git a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts index c50c62c06e..abab573f18 100644 --- a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseDialog.ts @@ -303,7 +303,7 @@ export class CreateProjectFromDatabaseDialog { this.projectNameTextBox.onTextChanged(text => { const errorMessage = isValidBasenameErrorMessage(text); - if (errorMessage) { + if (errorMessage !== undefined) { // Set validation error message if project name is invalid void this.projectNameTextBox!.updateProperty('validationErrorMessage', errorMessage); } else { diff --git a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseQuickpick.ts index 8af567c339..6c4a87653d 100644 --- a/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/createProjectFromDatabaseQuickpick.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as constants from '../common/constants'; -import { exists, getVscodeMssqlApi, isValidBasename, isValidBasenameErrorMessage, sanitizeStringForFilename } from '../common/utils'; +import { exists, getVscodeMssqlApi, isValidBasenameErrorMessage, sanitizeStringForFilename } from '../common/utils'; import { IConnectionInfo } from 'vscode-mssql'; import { defaultProjectNameFromDb, defaultProjectSaveLocation } from '../tools/newProjectTool'; import { ImportDataModel } from '../models/api/import'; @@ -72,7 +72,7 @@ export async function createNewProjectFromDatabaseWithQuickpick(connectionInfo?: title: constants.projectNamePlaceholderText, value: defaultProjectNameFromDb(sanitizeStringForFilename(selectedDatabase)), validateInput: (value) => { - return isValidBasename(value) ? undefined : isValidBasenameErrorMessage(value); + return isValidBasenameErrorMessage(value); }, ignoreFocusOut: true });