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

[v2] Download special char issue #2362

Merged
merged 5 commits into from
Nov 20, 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
16 changes: 16 additions & 0 deletions packages/imperative/src/io/__tests__/IO.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,22 @@ describe("IO tests", () => {
expect(processed).toBe(original.replace(/\n/g, "\r\n").slice(1));
});

it("processNewlines should replace LF line endings with CRLF on Windows when input is a Buffer", () => {
jest.spyOn(os, "platform").mockReturnValueOnce(IO.OS_WIN32);
const originalBuffer = Buffer.from("\nabc\ndef\n");
const processedBuffer = IO.processNewlines(originalBuffer);
const expectedBuffer = Buffer.from("\r\nabc\r\ndef\r\n");
expect(processedBuffer.equals(expectedBuffer)).toBe(true);
});

it("processNewlines should not replace LF line ending when last byte is CR and input is a Buffer", () => {
jest.spyOn(os, "platform").mockReturnValueOnce(IO.OS_WIN32);
const originalBuffer = Buffer.from("\nabc\ndef\n");
const processedBuffer = IO.processNewlines(originalBuffer, "\r".charCodeAt(0));
const expectedBuffer = Buffer.from("\nabc\r\ndef\r\n");
expect(processedBuffer.equals(expectedBuffer)).toBe(true);
});

it("should get an error for no input on mkdirp", () => {
let error;
try {
Expand Down
33 changes: 27 additions & 6 deletions packages/imperative/src/io/src/IO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,17 +264,38 @@ export class IO {
* @returns {string} - input with removed newlines
* @memberof IO
*/
public static processNewlines(original: string, lastByte?: number): string {
public static processNewlines<T extends string | Buffer>(original: T, lastByte?: number): T {
zFernand0 marked this conversation as resolved.
Show resolved Hide resolved
ImperativeExpect.toNotBeNullOrUndefined(original, "Required parameter 'original' must not be null or undefined");

if (os.platform() !== IO.OS_WIN32) {
return original;
}
// otherwise, we're on windows
const processed = original.replace(/([^\r])\n/g, "$1\r\n");
if ((lastByte == null || lastByte !== "\r".charCodeAt(0)) && original.startsWith("\n")) {
return "\r" + processed;

let processed: T;

if (typeof original === "string") {
processed = original.replace(/([^\r])\n/g, "$1\r\n") as T;
if ((lastByte == null || lastByte !== "\r".charCodeAt(0)) && original.startsWith("\n")) {
return ("\r" + processed) as T;
}
return processed;
} else {
const bufferList: number[] = [];
let prevByte = lastByte;
for (let i = 0; i < original.length; i++) {
const currentByte = original[i];
//Check if previous byte is not Carriage Return (13) and if current byte is Line Feed (10)
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
if (currentByte === 10 && prevByte !== 13) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
bufferList.push(13);
}
bufferList.push(currentByte);
prevByte = currentByte;
}
processed = Buffer.from(bufferList) as T;
return processed;
}
return processed;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ export abstract class AbstractRestClient {
this.log.debug("Streaming data chunk of length " + respData.length + " to response stream");
if (this.mNormalizeResponseNewlines && this.mContentEncoding == null) {
this.log.debug("Normalizing new lines in data chunk to operating system appropriate line endings");
respData = Buffer.from(IO.processNewlines(respData.toString(), this.lastByteReceived));
respData = IO.processNewlines(respData, this.lastByteReceived);
}
if (this.mTask != null) {
// update the progress task if provided by the requester
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class CompressionUtils {
let lastByteReceived: number = 0;
return new Transform({
transform(chunk, _, callback) {
this.push(Buffer.from(IO.processNewlines(chunk.toString(), lastByteReceived)));
this.push(IO.processNewlines(chunk, lastByteReceived));
lastByteReceived = chunk[chunk.byteLength - 1];
callback();
}
Expand Down
4 changes: 4 additions & 0 deletions packages/zosfiles/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to the Zowe z/OS files SDK package will be documented in this file.

## Recent Changes

- BugFix: Resolved issue where special characters could be corrupted when downloading a large file. [#2362](https://github.com/zowe/zowe-cli/pull/2362)

## `7.29.5`

- BugFix: Added support for the `--encoding` flag to the `zowe upload dir-to-uss` to allow for encoding uploaded directories for command group consistency. [#2356](https://github.com/zowe/zowe-cli/pull/2356)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ const rimraf = require("rimraf").sync;
const delayTime = 2000;
const testData = "abcdefghijklmnopqrstuvwxyz";

// File must be larger than 8KB such that it breaks into multiple chunks
const specialCharTestData = fs.readFileSync(__dirname+"/testfiles/specialCharTestData.txt");

let REAL_SESSION: Session;
let testEnvironment: ITestEnvironment<ITestPropertiesSchema>;
let defaultSystem: ITestPropertiesSchema;
Expand Down Expand Up @@ -574,6 +577,40 @@ describe("Download Data Set", () => {
const regex = /\./gi;
file = dsname.replace(regex, "/") + "/member.dat";
});
it("should download a data set member full of special characters to test buffer chunk concatenation", async () => {
let error;
let response: IZosFilesResponse;

// upload data to the newly created data set
await Upload.bufferToDataSet(REAL_SESSION, Buffer.from(specialCharTestData), dsname + "(member)");
await delay(delayTime);

try {
response = await Download.allMembers(REAL_SESSION, dsname);
Imperative.console.info("Response: " + inspect(response));
} catch (err) {
error = err;
Imperative.console.info("Error: " + inspect(error));
}
expect(error).toBeFalsy();
expect(response).toBeTruthy();
expect(response.success).toBeTruthy();
expect(response.commandResponse).toContain(
ZosFilesMessages.datasetDownloadedSuccessfully.message.substring(0, "Data set downloaded successfully".length + 1));

// Convert the data set name to use as a path/file
const regex = /\./gi;
file = dsname.replace(regex, "/");
file = file.toLowerCase();

// Compare the downloaded contents to those uploaded
const fileContents = stripNewLines(fs.readFileSync(`${file}/member.txt`).toString());
const uniqueFileChars = Array.from(new Set(fileContents)).join('');
const expectedUniqueChars = "àèéìòùÀÈÉÌÒÙ° "; // Expected unique characters

// Ensure the file only contains the unique characters and nothing else
expect(uniqueFileChars).toEqual(expectedUniqueChars);
});
});

describe("Data sets matching - all data sets - PO", () => {
Expand Down
Loading