diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index cd012d919..08bd82fcf 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -61,6 +61,8 @@ export default class IBMi { commandsExecuted: number = 0; + dangerousVariants = false; + constructor() { this.client = new node_ssh.NodeSSH; this.currentHost = ``; @@ -877,6 +879,9 @@ export default class IBMi { defaultCCSID: this.defaultCCSID }); + //Keep track of variant characters that can be uppercased + this.dangerousVariants = this.variantChars.local !== this.variantChars.local.toLocaleUpperCase(); + return { success: true }; @@ -1049,7 +1054,8 @@ export default class IBMi { const validQsysName = new RegExp(`^[A-Z0-9${variant_chars_local}][A-Z0-9_${variant_chars_local}.]{0,9}$`); // Remove leading slash - const path = string.startsWith(`/`) ? string.substring(1).toUpperCase().split(`/`) : string.toUpperCase().split(`/`); + const upperCasedString = this.upperCaseName(string); + const path = upperCasedString.startsWith(`/`) ? upperCasedString.substring(1).split(`/`) : upperCasedString.split(`/`); const basename = path[path.length - 1]; const file = path[path.length - 2]; @@ -1148,12 +1154,12 @@ export default class IBMi { } getLastDownloadLocation() { - if(this.config?.lastDownloadLocation && existsSync(Tools.fixWindowsPath(this.config.lastDownloadLocation))){ + if (this.config?.lastDownloadLocation && existsSync(Tools.fixWindowsPath(this.config.lastDownloadLocation))) { return this.config.lastDownloadLocation; } - else{ + else { return os.homedir(); - } + } } async setLastDownloadLocation(location: string) { @@ -1192,4 +1198,27 @@ export default class IBMi { throw new Error(`Failed to create temporary directory ${tempDirectory}: ${prepareDirectory.stderr}`); } } + + /** + * Uppercases an object name, keeping the variant chars case intact + * @param name + */ + upperCaseName(name: string) { + if (this.dangerousVariants && new RegExp(`[${this.variantChars.local}]`).test(name)) { + const upperCased = []; + for (const char of name) { + const upChar = char.toLocaleUpperCase(); + if (new RegExp(`[A-Z${this.variantChars.local}]`).test(upChar)) { + upperCased.push(upChar); + } + else { + upperCased.push(char); + } + } + return upperCased.join(""); + } + else{ + return name.toLocaleUpperCase(); + } + } } \ No newline at end of file diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 7bf92ffeb..b67be79b7 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -144,18 +144,33 @@ export default class IBMiContent { */ async downloadMemberContent(asp: string | undefined, library: string, sourceFile: string, member: string, localPath?: string) { asp = asp || this.config.sourceASP; - library = library.toUpperCase(); - sourceFile = sourceFile.toUpperCase(); - member = member.toUpperCase(); + library = this.ibmi.upperCaseName(library); + sourceFile = this.ibmi.upperCaseName(sourceFile); + member = this.ibmi.upperCaseName(member); let retry = false; let path = Tools.qualifyPath(library, sourceFile, member, asp); const tempRmt = this.getTempRemote(path); while (true) { - const copyResult = await this.ibmi.runCommand({ - command: `CPYTOSTMF FROMMBR('${path}') TOSTMF('${tempRmt}') STMFOPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${this.config.sourceFileCCSID})`, - noLibList: true - }); + let copyResult: CommandResult; + if (this.ibmi.dangerousVariants && new RegExp(`[${this.ibmi.variantChars.local}]`).test(path)) { + copyResult = { code: 0, stdout: '', stderr: '' }; + try { + await this.runSQL([ + `@QSYS/CPYF FROMFILE(${library}/${sourceFile}) TOFILE(QTEMP/QTEMPSRC) FROMMBR(${member}) TOMBR(TEMPMEMBER) MBROPT(*REPLACE) CRTFILE(*YES);`, + `@QSYS/CPYTOSTMF FROMMBR('${Tools.qualifyPath("QTEMP", "QTEMPSRC", "TEMPMEMBER", undefined)}') TOSTMF('${tempRmt}') STMFOPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${this.config.sourceFileCCSID});` + ].join("\n")); + } catch (error: any) { + copyResult.code = -1; + copyResult.stderr = String(error); + } + } + else { + copyResult = await this.ibmi.runCommand({ + command: `QSYS/CPYTOSTMF FROMMBR('${path}') TOSTMF('${tempRmt}') STMFOPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${this.config.sourceFileCCSID})`, + noLibList: true + }); + } if (copyResult.code === 0) { if (!localPath) { @@ -197,9 +212,9 @@ export default class IBMiContent { */ async uploadMemberContent(asp: string | undefined, library: string, sourceFile: string, member: string, content: string | Uint8Array) { asp = asp || this.config.sourceASP; - library = library.toUpperCase(); - sourceFile = sourceFile.toUpperCase(); - member = member.toUpperCase(); + library = this.ibmi.upperCaseName(library); + sourceFile = this.ibmi.upperCaseName(sourceFile); + member = this.ibmi.upperCaseName(member); const client = this.ibmi.client; const tmpobj = await tmpFile(); @@ -212,10 +227,26 @@ export default class IBMiContent { await client.putFile(tmpobj, tempRmt); while (true) { - const copyResult = await this.ibmi.runCommand({ - command: `QSYS/CPYFRMSTMF FROMSTMF('${tempRmt}') TOMBR('${path}') MBROPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${this.config.sourceFileCCSID})`, - noLibList: true - }); + let copyResult: CommandResult; + if (this.ibmi.dangerousVariants && new RegExp(`[${this.ibmi.variantChars.local}]`).test(path)) { + copyResult = { code: 0, stdout: '', stderr: '' }; + try { + await this.runSQL([ + `@QSYS/CPYF FROMFILE(${library}/${sourceFile}) FROMMBR(${member}) TOFILE(QTEMP/QTEMPSRC) TOMBR(TEMPMEMBER) MBROPT(*REPLACE) CRTFILE(*YES);`, + `@QSYS/CPYFRMSTMF FROMSTMF('${tempRmt}') TOMBR('${Tools.qualifyPath("QTEMP", "QTEMPSRC", "TEMPMEMBER", undefined)}') MBROPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${this.config.sourceFileCCSID})`, + `@QSYS/CPYF FROMFILE(QTEMP/QTEMPSRC) FROMMBR(TEMPMEMBER) TOFILE(${library}/${sourceFile}) TOMBR(${member}) MBROPT(*REPLACE);` + ].join("\n")); + } catch (error: any) { + copyResult.code = -1; + copyResult.stderr = String(error); + } + } + else { + copyResult = await this.ibmi.runCommand({ + command: `QSYS/CPYFRMSTMF FROMSTMF('${tempRmt}') TOMBR('${path}') MBROPT(*REPLACE) STMFCCSID(1208) DBFCCSID(${this.config.sourceFileCCSID})`, + noLibList: true + }); + } if (copyResult.code === 0) { const messages = Tools.parseMessages(copyResult.stderr); diff --git a/src/api/Tools.ts b/src/api/Tools.ts index 2befcb355..5512c3c7a 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -146,9 +146,8 @@ export namespace Tools { * @param iasp Optional: an iASP name */ export function qualifyPath(library: string, object: string, member: string, iasp?: string, sanitise?: boolean) { - library = library.toUpperCase(); const libraryPath = library === `QSYS` ? `QSYS.LIB` : `QSYS.LIB/${Tools.sanitizeLibraryNames([library]).join(``)}.LIB`; - const memberPath = `${object.toUpperCase()}.FILE/${member.toUpperCase()}.MBR` + const memberPath = `${object}.FILE/${member}.MBR` const memberSubpath = sanitise ? Tools.escapePath(memberPath) : memberPath; const result = (iasp && iasp.length > 0 ? `/${iasp}` : ``) + `/${libraryPath}/${memberSubpath}`;