diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 27fdee307..2ec8228b4 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -182,7 +182,10 @@ export default class IBMi { }); const delayedOperations: Function[] = [...onConnectedOperations]; - await this.client.connect(connectionObject as node_ssh.Config); + await this.client.connect({ + ...connectionObject, + privateKeyPath: connectionObject.privateKeyPath ? Tools.resolvePath(connectionObject.privateKeyPath) : undefined + } as node_ssh.Config); cancelToken.onCancellationRequested(() => { this.end(); @@ -1504,7 +1507,7 @@ export default class IBMi { return this.componentManager.get(name, ignoreState); } - getComponentStates(){ + getComponentStates() { return this.componentManager.getState(); } diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 6c6871308..4efed6a1d 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -957,15 +957,15 @@ export default class IBMiContent { target = localPath; } - target = IBMi.escapeForShell(target); - let result: CommandResult; if (assumeMember) { + target = IBMi.escapeForShell(target); result = await this.ibmi.sendQsh({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}`}); } else { + target = Tools.escapePath(target, true); // Take {DOES_THIS_WORK: `YESITDOES`} away, and all of a sudden names with # aren't found. - result = await this.ibmi.sendCommand({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}`, env: {DOES_THIS_WORK: `YESITDOES`}}); + result = await this.ibmi.sendCommand({ command: `${this.ibmi.remoteFeatures.attr} -p "${target}" ${operands.join(" ")}`, env: {DOES_THIS_WORK: `YESITDOES`}}); } if (result.code === 0) { diff --git a/src/api/Tools.ts b/src/api/Tools.ts index 6b68d3402..7c760ba1b 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -1,5 +1,6 @@ import Crypto from 'crypto'; import { readFileSync } from "fs"; +import os from "os"; import path from "path"; import vscode from "vscode"; import { IBMiMessage, IBMiMessages, QsysPath } from '../typings'; @@ -401,14 +402,14 @@ export namespace Tools { export function assumeType(str: string) { if (str.trim().length === 0) return ``; - + // The number is already generated on the server. // So, we assume that if the string starts with a 0, it is a string. if (/^0.+/.test(str) || str.length > 10) { return str } const number = Number(str); - if(isNaN(number)){ + if (isNaN(number)) { return str; } return number; @@ -463,4 +464,33 @@ export namespace Tools { } return 0; } + + /** + * Transforms a file path into an OS agnostic path. + * - Replaces full home directory path by ~ + * - Replaces all \ into / on Windows + * + * @param filePath + * @returns + */ + export function normalizePath(filePath: string) { + //Test path in lowercase since os.homedir doesn't always has the same case as filePath on Windows + if(filePath.toLowerCase().startsWith(os.homedir().toLowerCase())){ + filePath = path.join(`~`, filePath.substring(os.homedir().length)); + } + + return process.platform === "win32" ? filePath.replaceAll('\\', '/') : filePath; + } + + /** + * Transforms a normalized path into an OS specific path. + * - Replaces ~ with the current home directory + * - Changes all / to \ on Windows + * @param path + * @returns + */ + export function resolvePath(path: string) { + path = path.replace("~", os.homedir()); + return process.platform === "win32" ? path.replaceAll('/', '\\') : path; + } } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index c383bb514..7f204a824 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,7 @@ import { CustomUI } from "./api/CustomUI"; import { instance, loadAllofExtension } from './instantiate'; import { CompileTools } from "./api/CompileTools"; -import { ConnectionConfiguration, ConnectionManager, GlobalConfiguration, onCodeForIBMiConfigurationChange } from "./api/Configuration"; +import { ConnectionConfiguration, ConnectionManager, onCodeForIBMiConfigurationChange } from "./api/Configuration"; import IBMi from "./api/IBMi"; import { GlobalStorage } from "./api/Storage"; import { Tools } from "./api/Tools"; @@ -142,38 +142,6 @@ export async function activate(context: ExtensionContext): Promise }; } -async function fixLoginSettings() { - const connections = (GlobalConfiguration.get(`connections`) || []); - let update = false; - for (const connection of connections) { - //privateKey was used to hold privateKeyPath - if ('privateKey' in connection) { - const privateKey = connection["privateKey"] as string; - if (privateKey) { - connection.privateKeyPath = privateKey; - } - delete connection["privateKey"]; - update = true; - } - - //An empty privateKeyPath will crash the connection - if (!connection.privateKeyPath?.trim()) { - connection.privateKeyPath = undefined; - update = true; - } - - //buttons were added by the login settings page - if (`buttons` in connection) { - delete connection["buttons"]; - update = true; - } - } - - if (update) { - await GlobalConfiguration.set(`connections`, connections); - } -} - // this method is called when your extension is deactivated export async function deactivate() { await commands.executeCommand(`code-for-ibmi.disconnect`, true); diff --git a/src/testing/encoding.ts b/src/testing/encoding.ts index e89fe1327..bd486ad35 100644 --- a/src/testing/encoding.ts +++ b/src/testing/encoding.ts @@ -91,6 +91,44 @@ export const EncodingSuite: TestSuite = { assert.ok(result?.length); } }, + { + name: `Files and directories with spaces`, test: async () => { + const connection = instance.getConnection()!; + + await connection.withTempDirectory(async tempDir => { + const dirName = `hello world`; + const dirWithSpace = path.posix.join(tempDir, dirName); + const fileName = `hello world.txt`; + const nameWithSpace = path.posix.join(dirWithSpace, fileName); + + await connection.sendCommand({command: `mkdir -p "${dirWithSpace}"`}); + await connection.content.createStreamFile(nameWithSpace); + + // Resolve and get attributes + const resolved = await connection.content.streamfileResolve([fileName], [tempDir, dirWithSpace]); + assert.strictEqual(resolved, nameWithSpace); + + const attributes = await connection.content.getAttributes(resolved, `CCSID`); + assert.ok(attributes); + + // Write and read the files + const uri = Uri.from({scheme: `streamfile`, path: nameWithSpace}); + await workspace.fs.writeFile(uri, Buffer.from(`Hello world`, `utf8`)); + + const streamfileContents = await workspace.fs.readFile(uri); + assert.ok(streamfileContents.toString().includes(`Hello world`)); + + // List files + const files = await connection.content.getFileList(tempDir); + assert.strictEqual(files.length, 1); + assert.ok(files.some(f => f.name === dirName && f.path === dirWithSpace)); + + const files2 = await connection.content.getFileList(dirWithSpace); + assert.strictEqual(files2.length, 1); + assert.ok(files2.some(f => f.name === fileName && f.path === nameWithSpace)); + }); + } + }, { name: `Run variants through shells`, test: async () => { const connection = instance.getConnection(); diff --git a/src/typings.ts b/src/typings.ts index 381b4e6cb..753808116 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -83,7 +83,6 @@ export interface ConnectionData { port: number; username: string; password?: string; - privateKey?: string; privateKeyPath?: string; keepaliveInterval?: number; } diff --git a/src/webviews/login/index.ts b/src/webviews/login/index.ts index 2c6943541..137eba7c5 100644 --- a/src/webviews/login/index.ts +++ b/src/webviews/login/index.ts @@ -2,6 +2,7 @@ import vscode, { l10n, ThemeIcon } from "vscode"; import { ConnectionConfiguration, ConnectionManager } from "../../api/Configuration"; import { CustomUI, Section } from "../../api/CustomUI"; import IBMi from "../../api/IBMi"; +import { Tools } from "../../api/Tools"; import { disconnect, instance } from "../../instantiate"; import { ConnectionData } from '../../typings'; @@ -54,7 +55,7 @@ export class Login { page.panel.dispose(); data.port = Number(data.port); - data.privateKeyPath = data.privateKeyPath?.trim() ? data.privateKeyPath : undefined; + data.privateKeyPath = data.privateKeyPath?.trim() ? Tools.normalizePath(data.privateKeyPath) : undefined; if (data.name) { const existingConnection = ConnectionManager.getByName(data.name); diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 9f3e2083e..124753276 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -1,3 +1,4 @@ +import { existsSync } from "fs"; import vscode from "vscode"; import { ConnectionConfiguration, ConnectionManager, GlobalConfiguration } from "../../api/Configuration"; import { ComplexTab, CustomUI, Section } from "../../api/CustomUI"; @@ -352,14 +353,15 @@ export class SettingsUI { if (connection) { const storedPassword = await ConnectionManager.getStoredPassword(context, name); let { data: stored, index } = connection; - + const privateKeyPath = stored.privateKeyPath ? Tools.resolvePath(stored.privateKeyPath) : undefined; + const privateKeyWarning = !privateKeyPath || existsSync(privateKeyPath) ? "" : "⚠️ This private key doesn't exist on this system! ⚠️

"; const ui = new CustomUI() .addInput(`host`, vscode.l10n.t(`Host or IP Address`), undefined, { default: stored.host, minlength: 1 }) .addInput(`port`, vscode.l10n.t(`Port (SSH)`), undefined, { default: String(stored.port), minlength: 1, maxlength: 5, regexTest: `^\\d+$` }) .addInput(`username`, vscode.l10n.t(`Username`), undefined, { default: stored.username, minlength: 1 }) .addParagraph(vscode.l10n.t(`Only provide either the password or a private key - not both.`)) .addPassword(`password`, `${vscode.l10n.t(`Password`)}${storedPassword ? ` (${vscode.l10n.t(`stored`)})` : ``}`, vscode.l10n.t("Only provide a password if you want to update an existing one or set a new one.")) - .addFile(`privateKeyPath`, `${vscode.l10n.t(`Private Key`)}${stored.privateKeyPath ? ` (${vscode.l10n.t(`Private Key`)}: ${stored.privateKeyPath})` : ``}`, vscode.l10n.t("Only provide a private key if you want to update from the existing one or set one.") + '
' + vscode.l10n.t("OpenSSH, RFC4716 and PPK formats are supported.")) + .addFile(`privateKeyPath`, `${vscode.l10n.t(`Private Key`)}${privateKeyPath ? ` (${vscode.l10n.t(`Private Key`)}: ${privateKeyPath})` : ``}`, privateKeyWarning + vscode.l10n.t("Only provide a private key if you want to update from the existing one or set one.") + '
' + vscode.l10n.t("OpenSSH, RFC4716 and PPK formats are supported.")) .addButtons( { id: `submitButton`, label: vscode.l10n.t(`Save`), requiresValidation: true }, { id: `removeAuth`, label: vscode.l10n.t(`Remove auth methods`) } @@ -393,6 +395,7 @@ export class SettingsUI { // If no password was entered, but a keypath exists // then remove the password from the data and // use the keypath instead + data.privateKeyPath = Tools.normalizePath(data.privateKeyPath); await ConnectionManager.deleteStoredPassword(context, name); vscode.window.showInformationMessage(vscode.l10n.t(`Private key updated and will be used for "{0}".`, name)); }