diff --git a/src/api/CompileTools.ts b/src/api/CompileTools.ts index 1c94a44ee..50ddc6940 100644 --- a/src/api/CompileTools.ts +++ b/src/api/CompileTools.ts @@ -187,9 +187,9 @@ export namespace CompileTools { function buildLiblistCommands(connection: IBMi, config: ILELibrarySettings): string[] { return [ - `liblist -d ${IBMi.escapeForShell(Tools.sanitizeObjNamesForPase(...connection.defaultUserLibraries).join(` `))}`, - `liblist -c ${IBMi.escapeForShell(Tools.sanitizeObjNamesForPase(config.currentLibrary || "*CRTDFT")[0])}`, - `liblist -a ${IBMi.escapeForShell(Tools.sanitizeObjNamesForPase(...buildLibraryList(config)).join(` `))}` + `liblist -d ${Tools.escapeForShell(Tools.sanitizeObjNamesForPase(...connection.defaultUserLibraries).join(` `))}`, + `liblist -c ${Tools.escapeForShell(Tools.sanitizeObjNamesForPase(config.currentLibrary || "*CRTDFT")[0])}`, + `liblist -a ${Tools.escapeForShell(Tools.sanitizeObjNamesForPase(...buildLibraryList(config)).join(` `))}` ]; } } \ No newline at end of file diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 22f98207f..0441bd520 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -985,8 +985,8 @@ export default class IBMi { const result = await this.sendQsh({ command: [ - `liblist -d ` + IBMi.escapeForShell(this.defaultUserLibraries.join(` `)), - ...this.config.libraryList.map(lib => `liblist -a ` + IBMi.escapeForShell(lib)) + `liblist -d ` + Tools.escapeForShell(this.defaultUserLibraries.join(` `)), + ...this.config.libraryList.map(lib => `liblist -a ` + Tools.escapeForShell(lib)) ].join(`; `) }); @@ -1185,10 +1185,6 @@ export default class IBMi { return CompileTools.runCommand(this, data); } - static escapeForShell(command: string) { - return command.replace(/\$/g, `\\$`) - } - async sendQsh(options: CommandData) { options.stdin = options.command; @@ -1208,10 +1204,10 @@ export default class IBMi { let commands: string[] = []; if (options.env) { if (this.usingBash()) { - commands.push(...Object.entries(options.env).map(([key, value]) => `export ${key}="${value ? IBMi.escapeForShell(value) : ``}"`)); + commands.push(...Object.entries(options.env).map(([key, value]) => `export ${key}="${value ? Tools.escapeForShell(value) : ``}"`)); } else { // bourne shell doesn't support the same export syntax as bash - commands.push(...Object.entries(options.env).map(([key, value]) => `${key}="${value ? IBMi.escapeForShell(value) : ``}" export ${key}`)); + commands.push(...Object.entries(options.env).map(([key, value]) => `${key}="${value ? Tools.escapeForShell(value) : ``}" export ${key}`)); } } diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 0dbce8832..df257b708 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -949,7 +949,7 @@ export default class IBMiContent { // Escape names for shell const pathList: string[] = []; for (const file of files) { - pathList.push(Tools.qualifyPath(inAmerican(file.library), inAmerican(file.name), inAmerican(member), await this.ibmi.getLibraryIAsp(file.library), true)); + pathList.push(Tools.escapeForShell(Tools.qualifyPath(inAmerican(file.library), inAmerican(file.name), inAmerican(member), await this.ibmi.getLibraryIAsp(file.library), true))); } const command = `for f in ${pathList.join(' ').toUpperCase()}; do if [ -f $f ]; then echo $f; break; fi; done`; @@ -975,7 +975,7 @@ export default class IBMiContent { } async objectResolve(object: string, libraries: string[]): Promise { - const command = `for f in ${libraries.map(lib => `/QSYS.LIB/${this.ibmi.sysNameInAmerican(lib)}.LIB/${this.ibmi.sysNameInAmerican(object)}.*`).join(` `)}; do if [ -f $f ] || [ -d $f ]; then echo $f; break; fi; done`; + const command = `for f in ${Tools.escapeForShell(libraries.map(lib => `/QSYS.LIB/${this.ibmi.sysNameInAmerican(lib)}.LIB/${this.ibmi.sysNameInAmerican(object)}.*`).join(` `))}; do if [ -f $f ] || [ -d $f ]; then echo $f; break; fi; done`; const result = await this.ibmi.sendCommand({ command, @@ -1109,7 +1109,7 @@ export default class IBMiContent { let result: CommandResult; if (assumeMember) { - target = IBMi.escapeForShell(target); + target = Tools.escapeForShell(target); result = await this.ibmi.sendQsh({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}` }); } else { target = Tools.escapePath(target, true); diff --git a/src/api/Tools.ts b/src/api/Tools.ts index ca4c86858..0a1b2be88 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -303,4 +303,8 @@ export namespace Tools { export function getCurLib(value?:string){ return !value || /\*CRTDFT/i.test(value) ? "*CURLIB" : value; } + + export function escapeForShell(command: string) { + return command.replace(/\$/g, `\\$`) + } } \ No newline at end of file diff --git a/src/api/tests/suites/content.test.ts b/src/api/tests/suites/content.test.ts index 49fde30b6..31117060a 100644 --- a/src/api/tests/suites/content.test.ts +++ b/src/api/tests/suites/content.test.ts @@ -90,6 +90,42 @@ describe('Content Tests', { concurrent: true }, () => { } }); + it.each(['$', '#', '#$'])('memberResolve with %s', async (prefix) => { + const content = connection.getContent(); + const tempLib = connection.getConfig().tempLibrary; + const tempSPF = `${prefix}ABCD`.concat(connection.variantChars.local); + const tempMbr = tempSPF; + + try { + const result = await connection.runCommand({ + command: `QSYS/CRTSRCPF ${tempLib}/${tempSPF} MBR(${tempMbr})`, + environment: 'ile' + }); + expect(result.code).toBe(0); + + const member = await content.memberResolve(tempMbr, [ + { library: 'QSYSINC', name: 'MIH' }, // Doesn't exist here + { library: 'NOEXIST', name: 'SUP' }, // Doesn't exist here + { library: tempLib, name: tempSPF } // Does exist here + ]); + + expect(member).toEqual({ + asp: undefined, + library: tempLib, + file: tempSPF, + name: tempMbr, + extension: 'MBR', + basename: `${tempMbr}.MBR` + }); + } + finally { + await connection.runCommand({ + command: `QSYS/DLTF ${tempLib}/${tempSPF}`, + environment: 'ile' + }) + } + }); + it('memberResolve with bad name', async () => { const content = connection.getContent(); @@ -152,6 +188,35 @@ describe('Content Tests', { concurrent: true }, () => { } }); + it.each(['$', '#', '#$'])('objectResolve with %s', async (prefix) => { + const content = connection.getContent(); + const tempLib = connection.getConfig().tempLibrary; + const tempObj = `${prefix}ABCD`.concat(connection.variantChars.local); + + try { + const result = await connection.runCommand({ + command: `QSYS/CRTDTAARA ${tempLib}/${tempObj} TYPE(*CHAR)`, + environment: 'ile' + }); + expect(result.code).toBe(0); + + const lib = await content.objectResolve(tempObj, [ + 'QSYSINC', // Doesn't exist here + 'QSYS2', // Doesn't exist here + tempLib // Does exist here + ]); + + expect(lib).toBe(tempLib); + } + finally{ + // Cleanup... + await connection.runCommand({ + command: `QSYS/DLTDTAARA ${tempLib}/${tempObj}`, + environment: 'ile' + }) + } + }); + it('objectResolve with bad name', async () => { const content = connection.getContent(); diff --git a/src/api/tests/suites/encoding.test.ts b/src/api/tests/suites/encoding.test.ts index 07ed3c4b2..0c6c4cec9 100644 --- a/src/api/tests/suites/encoding.test.ts +++ b/src/api/tests/suites/encoding.test.ts @@ -130,7 +130,7 @@ describe('Encoding tests', { concurrent: true }, () => { it('Run variants through shells', async () => { const text = `Hello${connection?.variantChars.local}world`; - const basicCommandA = `echo "${IBMi.escapeForShell(text)}"`; + const basicCommandA = `echo "${Tools.escapeForShell(text)}"`; const basicCommandB = `echo '${text}'`; const basicCommandC = `echo 'abc'\\''123'`; const printEscapeChar = `echo "\\\\"`; diff --git a/src/api/tests/suites/tools.test.ts b/src/api/tests/suites/tools.test.ts index 34f81b965..16eed8177 100644 --- a/src/api/tests/suites/tools.test.ts +++ b/src/api/tests/suites/tools.test.ts @@ -287,4 +287,10 @@ describe('Tools.parseFilterDate tests', { concurrent: true }, () => { it('should reject an SQL injection attempt', () => { expect(Tools.parseFilterDate(`2024-01-01') OR 1=1 --`)).toBeUndefined(); }); + + it('should escape $ for shell', () => { + expect(Tools.escapeForShell('$ABCD')).toBe('\\$ABCD'); + expect(Tools.escapeForShell('#$ABC$D')).toBe('#\\$ABC\\$D'); + expect(Tools.escapeForShell('ABCD')).toBe('ABCD'); + }) });