Skip to content

Commit 7f4fc33

Browse files
michelleangelasean-mcmanus
authored andcommitted
Add input validation on configuration UI (#3563)
* show errors on config UI
1 parent 788cee3 commit 7f4fc33

4 files changed

Lines changed: 245 additions & 78 deletions

File tree

Extension/src/LanguageServer/configurations.ts

Lines changed: 172 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ export interface Configuration {
6464
browse?: Browse;
6565
}
6666

67+
export interface ConfigurationErrors {
68+
compilerPath?: string;
69+
includePath?: string;
70+
intelliSenseMode?: string;
71+
}
72+
6773
export interface Browse {
6874
path?: string[];
6975
limitSymbolsToIncludedHeaders?: boolean | string;
@@ -227,7 +233,9 @@ export class CppProperties {
227233
// Clear out any modifications we may have made internally by parsing the json file
228234
if (this.parsePropertiesFile(false)) {
229235
// Update the UI with new selected configuration
230-
this.settingsPanel.updateConfigUI(this.configurationJson.configurations[this.currentConfigurationIndex.Value]);
236+
this.settingsPanel.updateConfigUI(
237+
this.configurationJson.configurations[this.currentConfigurationIndex.Value],
238+
this.getErrorsForConfigUI());
231239
} else {
232240
// Parse failed, open json file
233241
vscode.workspace.openTextDocument(this.propertiesFile);
@@ -392,8 +400,13 @@ export class CppProperties {
392400
private isCompilerIntelliSenseModeCompatible(): boolean {
393401
// Check if intelliSenseMode and compilerPath are compatible
394402
// cl.exe and msvc mode should be used together
395-
// Ignore if compiler path is not set
396-
if (this.CurrentConfiguration.compilerPath === undefined) {
403+
// Ignore if compiler path is not set or intelliSenseMode is not set
404+
if (this.CurrentConfiguration.compilerPath === undefined ||
405+
this.CurrentConfiguration.compilerPath === "" ||
406+
this.CurrentConfiguration.compilerPath === "${default}" ||
407+
this.CurrentConfiguration.intelliSenseMode === undefined ||
408+
this.CurrentConfiguration.intelliSenseMode === "" ||
409+
this.CurrentConfiguration.intelliSenseMode === "${default}") {
397410
return true;
398411
}
399412
let compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs(this.CurrentConfiguration.compilerPath);
@@ -628,7 +641,9 @@ export class CppProperties {
628641
this.settingsPanel.ConfigValuesChanged(() => this.saveConfigurationUI());
629642
this.disposables.push(this.settingsPanel);
630643
}
631-
this.settingsPanel.createOrShow(this.configurationJson.configurations[this.currentConfigurationIndex.Value]);
644+
this.settingsPanel.createOrShow(
645+
this.configurationJson.configurations[this.currentConfigurationIndex.Value],
646+
this.getErrorsForConfigUI());
632647
} else {
633648
// Parse failed, open json file
634649
vscode.workspace.openTextDocument(this.propertiesFile).then((document: vscode.TextDocument) => {
@@ -646,7 +661,9 @@ export class CppProperties {
646661
if (this.parsePropertiesFile(false)) {
647662
// The settings UI became visible or active.
648663
// Ensure settingsPanel has copy of latest current configuration
649-
this.settingsPanel.updateConfigUI(this.configurationJson.configurations[this.currentConfigurationIndex.Value]);
664+
this.settingsPanel.updateConfigUI(
665+
this.configurationJson.configurations[this.currentConfigurationIndex.Value],
666+
this.getErrorsForConfigUI());
650667
} else {
651668
// Parse failed, open json file
652669
vscode.workspace.openTextDocument(this.propertiesFile);
@@ -660,6 +677,7 @@ export class CppProperties {
660677
this.parsePropertiesFile(false); // Clear out any modifications we may have made internally.
661678
let config: Configuration = this.settingsPanel.getLastValuesFromConfigUI();
662679
this.configurationJson.configurations[this.currentConfigurationIndex.Value] = config;
680+
this.settingsPanel.updateErrors(this.getErrorsForConfigUI());
663681
this.writeToJson();
664682
}
665683

@@ -822,6 +840,150 @@ export class CppProperties {
822840
return success;
823841
}
824842

843+
private resolvePath(path: string, isWindows: boolean): string {
844+
if (!path || path === "${default}") {
845+
return "";
846+
}
847+
848+
let result: string = "";
849+
850+
// first resolve variables
851+
result = util.resolveVariables(path, this.ExtendedEnvironment);
852+
if (result.includes("${workspaceFolder}")) {
853+
result = result.replace("${workspaceFolder}", this.rootUri.fsPath);
854+
}
855+
if (result.includes("${workspaceRoot}")) {
856+
result = result.replace("${workspaceRoot}", this.rootUri.fsPath);
857+
}
858+
if (result.includes("${vcpkgRoot}") && util.getVcpkgRoot()) {
859+
result = result.replace("${vcpkgRoot}", util.getVcpkgRoot());
860+
}
861+
if (result.includes("*")) {
862+
result = result.replace(/\*/g, "");
863+
}
864+
865+
// resolve WSL paths
866+
if (isWindows && result.startsWith("/")) {
867+
const mntStr: string = "/mnt/";
868+
if (result.length > "/mnt/c/".length && result.substr(0, mntStr.length) === mntStr) {
869+
result = result.substr(mntStr.length);
870+
result = result.substr(0, 1) + ":" + result.substr(1);
871+
} else if (this.rootfs && this.rootfs.length > 0) {
872+
result = this.rootfs + result.substr(1);
873+
// TODO: Handle WSL symlinks.
874+
}
875+
}
876+
877+
return result;
878+
}
879+
880+
private getErrorsForConfigUI(): ConfigurationErrors {
881+
let errors: ConfigurationErrors = {};
882+
const isWindows: boolean = os.platform() === 'win32';
883+
884+
// Validate compilerPath
885+
let resolvedCompilerPath: string = this.resolvePath(this.CurrentConfiguration.compilerPath, isWindows);
886+
let compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs(resolvedCompilerPath);
887+
if (resolvedCompilerPath &&
888+
// Don't error cl.exe paths because it could be for an older preview build.
889+
!(isWindows && compilerPathAndArgs.compilerPath.endsWith("cl.exe"))) {
890+
resolvedCompilerPath = resolvedCompilerPath.trim();
891+
892+
// Error when the compiler's path has spaces without quotes but args are used.
893+
// Except, exclude cl.exe paths because it could be for an older preview build.
894+
let compilerPathNeedsQuotes: boolean =
895+
compilerPathAndArgs.additionalArgs &&
896+
!resolvedCompilerPath.startsWith('"') &&
897+
compilerPathAndArgs.compilerPath.includes(" ");
898+
899+
let compilerPathErrors: string[] = [];
900+
if (compilerPathNeedsQuotes) {
901+
compilerPathErrors.push(`Compiler path with spaces and arguments is missing double quotes " around the path.`);
902+
}
903+
904+
// Get compiler path without arguments before checking if it exists
905+
resolvedCompilerPath = compilerPathAndArgs.compilerPath;
906+
907+
let pathExists: boolean = true;
908+
let existsWithExeAdded: (path: string) => boolean = (path: string) => {
909+
return isWindows && !path.startsWith("/") && fs.existsSync(path + ".exe");
910+
};
911+
if (!fs.existsSync(resolvedCompilerPath)) {
912+
if (existsWithExeAdded(resolvedCompilerPath)) {
913+
resolvedCompilerPath += ".exe";
914+
} else {
915+
// Check again for a relative path.
916+
const relativePath: string = this.rootUri.fsPath + path.sep + resolvedCompilerPath;
917+
if (!fs.existsSync(relativePath)) {
918+
if (existsWithExeAdded(resolvedCompilerPath)) {
919+
resolvedCompilerPath += ".exe";
920+
} else {
921+
pathExists = false;
922+
}
923+
} else {
924+
resolvedCompilerPath = relativePath;
925+
}
926+
}
927+
}
928+
929+
if (!pathExists) {
930+
let message: string = `Cannot find: ${resolvedCompilerPath}`;
931+
compilerPathErrors.push(message);
932+
} else if (!util.checkFileExistsSync(resolvedCompilerPath)) {
933+
let message: string = `Path is not a file: ${resolvedCompilerPath}`;
934+
compilerPathErrors.push(message);
935+
}
936+
937+
if (compilerPathErrors.length > 0) {
938+
errors.compilerPath = compilerPathErrors.join('\n');
939+
}
940+
}
941+
942+
// Validate includePath
943+
let includePathErrors: string[] = [];
944+
for (let includePath of this.CurrentConfiguration.includePath) {
945+
let pathExists: boolean = true;
946+
let resolvedIncludePath: string = this.resolvePath(includePath, isWindows);
947+
if (!resolvedIncludePath) {
948+
continue;
949+
}
950+
951+
// Check if resolved path exists
952+
if (!fs.existsSync(resolvedIncludePath)) {
953+
// Check for relative path if resolved path does not exists
954+
const relativePath: string = this.rootUri.fsPath + path.sep + resolvedIncludePath;
955+
if (!fs.existsSync(relativePath)) {
956+
pathExists = false;
957+
} else {
958+
resolvedIncludePath = relativePath;
959+
}
960+
}
961+
962+
if (!pathExists) {
963+
let message: string = `Cannot find: ${resolvedIncludePath}`;
964+
includePathErrors.push(message);
965+
continue;
966+
}
967+
968+
// Check if path is a directory
969+
if (!util.checkDirectoryExistsSync(resolvedIncludePath)) {
970+
let message: string = `Path is not a directory: ${resolvedIncludePath}`;
971+
includePathErrors.push(message);
972+
}
973+
}
974+
975+
if (includePathErrors.length > 0) {
976+
errors.includePath = includePathErrors.join('\n');
977+
}
978+
979+
// Validate intelliSenseMode
980+
if (isWindows && !this.isCompilerIntelliSenseModeCompatible()) {
981+
errors.intelliSenseMode = `IntelliSense mode ${this.CurrentConfiguration.intelliSenseMode} is incompatible with compiler path.`;
982+
}
983+
984+
return errors;
985+
}
986+
825987
private handleSquiggles(): void {
826988
if (!this.propertiesFile) {
827989
return;
@@ -926,35 +1088,14 @@ export class CppProperties {
9261088
// TODO: Add squiggles for when the C_Cpp.default.* paths are invalid.
9271089
continue;
9281090
}
929-
let resolvedPath: string = util.resolveVariables(curPath, this.ExtendedEnvironment);
930-
if (resolvedPath.includes("${workspaceFolder}")) {
931-
resolvedPath = resolvedPath.replace("${workspaceFolder}", this.rootUri.fsPath);
932-
}
933-
if (resolvedPath.includes("${workspaceRoot}")) {
934-
resolvedPath = resolvedPath.replace("${workspaceRoot}", this.rootUri.fsPath);
935-
}
936-
if (resolvedPath.includes("${vcpkgRoot}")) {
937-
resolvedPath = resolvedPath.replace("${vcpkgRoot}", util.getVcpkgRoot());
938-
}
939-
if (resolvedPath.includes("*")) {
940-
resolvedPath = resolvedPath.replace(/\*/g, "");
1091+
1092+
let resolvedPath: string = this.resolvePath(curPath, isWindows);
1093+
if (!resolvedPath) {
1094+
continue;
9411095
}
9421096

9431097
// TODO: Invalid paths created from environment variables are not detected.
9441098

945-
// Handle WSL paths.
946-
const isWSL: boolean = isWindows && resolvedPath.startsWith("/");
947-
if (isWSL) {
948-
const mntStr: string = "/mnt/";
949-
if (resolvedPath.length > "/mnt/c/".length && resolvedPath.substr(0, mntStr.length) === mntStr) {
950-
resolvedPath = resolvedPath.substr(mntStr.length);
951-
resolvedPath = resolvedPath.substr(0, 1) + ":" + resolvedPath.substr(1);
952-
} else if (this.rootfs && this.rootfs.length > 0) {
953-
resolvedPath = this.rootfs + resolvedPath.substr(1);
954-
// TODO: Handle WSL symlinks.
955-
}
956-
}
957-
9581099
let compilerPathNeedsQuotes: boolean = false;
9591100
if (isCompilerPath) {
9601101
resolvedPath = resolvedPath.trim();
@@ -967,6 +1108,7 @@ export class CppProperties {
9671108
resolvedPath = compilerPathAndArgs.compilerPath;
9681109
}
9691110

1111+
const isWSL: boolean = isWindows && resolvedPath.startsWith("/");
9701112
let pathExists: boolean = true;
9711113
let existsWithExeAdded: (path: string) => boolean = (path: string) => {
9721114
return isCompilerPath && isWindows && !isWSL && fs.existsSync(path + ".exe");

Extension/src/LanguageServer/settingsPanel.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class SettingsPanel {
4040
);
4141
}
4242

43-
public createOrShow(activeConfiguration: config.Configuration): void {
43+
public createOrShow(activeConfiguration: config.Configuration, errors: config.ConfigurationErrors): void {
4444
const column: vscode.ViewColumn = vscode.window.activeTextEditor
4545
? vscode.window.activeTextEditor.viewColumn
4646
: undefined;
@@ -80,7 +80,7 @@ export class SettingsPanel {
8080

8181
this.panel.webview.html = this.getHtml();
8282

83-
this.updateWebview(activeConfiguration);
83+
this.updateWebview(activeConfiguration, errors);
8484
}
8585

8686
public get SettingsPanelActivated(): vscode.Event<void> {
@@ -95,18 +95,17 @@ export class SettingsPanel {
9595
return this.configValues;
9696
}
9797

98-
public updateConfigUI(configuration: config.Configuration): void {
98+
public updateConfigUI(configuration: config.Configuration, errors: config.ConfigurationErrors): void {
9999
if (this.panel) {
100-
this.updateWebview(configuration);
100+
this.updateWebview(configuration, errors);
101101
}
102102
}
103103

104-
//TODO: validate input paths
105-
// public validatePaths(invalid: boolean) {
106-
// if (this.panel) {
107-
// this.panel.webview.postMessage({ command: 'validateCompilerPath', invalid: invalid });
108-
// }
109-
// }
104+
public updateErrors(errors: config.ConfigurationErrors): void {
105+
if (this.panel) {
106+
this.panel.webview.postMessage({ command: 'updateErrors', errors: errors});
107+
}
108+
}
110109

111110
public dispose(): void {
112111
// Clean up resources
@@ -128,12 +127,13 @@ export class SettingsPanel {
128127
}
129128
}
130129

131-
private updateWebview(configuration: config.Configuration): void {
130+
private updateWebview(configuration: config.Configuration, errors: config.ConfigurationErrors): void {
132131
this.configValues = Object.assign({}, configuration); // Copy configuration values
133132
this.isIntelliSenseModeDefined = (this.configValues.intelliSenseMode !== undefined);
134133
if (this.panel) {
135-
// Send a message to the webview to update the values
136-
this.panel.webview.postMessage({ command: 'update', config: this.configValues });
134+
// Send a message to the webview to update the values and errors
135+
this.panel.webview.postMessage({ command: 'updateConfig', config: this.configValues});
136+
this.panel.webview.postMessage({ command: 'updateErrors', errors: errors});
137137
}
138138
}
139139

0 commit comments

Comments
 (0)