Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correctly handle source members with variant characters in their name #1940

Merged
merged 3 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions src/api/IBMi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export default class IBMi {

commandsExecuted: number = 0;

dangerousVariants = false;

constructor() {
this.client = new node_ssh.NodeSSH;
this.currentHost = ``;
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}
}
}
59 changes: 45 additions & 14 deletions src/api/IBMiContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down
3 changes: 1 addition & 2 deletions src/api/Tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down