Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/comms/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"name": "index.html",
"request": "launch",
"type": "msedge",
"url": "http://localhost:5521/index.html",
"url": "http://localhost:5173/index.html",
"runtimeArgs": [
"--disable-web-security"
],
Expand Down
5 changes: 5 additions & 0 deletions packages/comms/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,19 @@ <h1>ESM Quick Test</h1>

Workunit.submit({ baseUrl: "http://localhost:8010" }, "hthor", "'Hello and Welcome!';")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

several console.logs left behind in here?

.then((wu) => {
console.log("Submitted WU: " + wu.Wuid);
return wu.watchUntilComplete();
}).then((wu) => {
console.log("WU: " + wu.Wuid, wu.StateID, wu.State, wu.isComplete());
return wu.fetchResults().then((results) => {
console.log("Results: " + results.length);
return results[0].fetchRows();
}).then((rows) => {
console.log("Rows: " + rows.length);
return wu;
});
}).then((wu) => {
console.log("Deleting WU: " + wu.Wuid);
return wu.delete().then(() => wu);
}).then(wu => {
console.log("Deleted WU: " + wu.Wuid, wu.isDeleted());
Expand Down
4 changes: 2 additions & 2 deletions packages/comms/src/connection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { join, promiseTimeout, scopedLogger } from "@hpcc-js/util";
import { join, promiseTimeout, scopedLogger, utf8ToBase64 } from "@hpcc-js/util";

const logger = scopedLogger("comms/connection.ts");

Expand Down Expand Up @@ -137,7 +137,7 @@ export function jsonp(opts: IOptions, action: string, request: any = {}, respons
}

function authHeader(opts: IOptions): object {
return opts.userID ? { Authorization: `Basic ${btoa(`${opts.userID}:${opts.password}`)}` } : {};
return opts.userID ? { Authorization: `Basic ${utf8ToBase64(`${opts.userID}:${opts.password}`)}` } : {};
}

// _omitMap is a workaround for older HPCC-Platform instances without credentials ---
Expand Down
1 change: 0 additions & 1 deletion packages/comms/src/index.common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from "./__package__.ts";

export * from "./services/fileSpray.ts";
export * from "./services/wsAccess.ts";
export * from "./services/wsAccount.ts";
Expand Down
9 changes: 1 addition & 8 deletions packages/comms/src/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ root.DOMParser = DOMParser;
import fetch, { Headers, Request, Response, } from "node-fetch";

import * as https from "node:https";
import { Buffer } from "node:buffer";
import { Agent, setGlobalDispatcher } from "undici";

if (typeof root.fetch === "undefined") {
Expand Down Expand Up @@ -57,14 +58,6 @@ root.fetch.__trustwaveAgent = new https.Agent({
ca: globalCA + trustwave
});

// btoa polyfill ---
import { Buffer } from "node:buffer";
if (typeof root.btoa === "undefined") {
root.btoa = function (str: string) {
return Buffer.from(str || "", "utf8").toString("base64");
};
}

export * from "./index.common.ts";

// Client Tools ---
Expand Down
1 change: 1 addition & 0 deletions packages/util/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from "./stack.ts";
export * from "./stateful.ts";
export * from "./string.ts";
export * from "./url.ts";
export * from "./utf8ToBase64.ts";
47 changes: 47 additions & 0 deletions packages/util/src/utf8ToBase64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

function toUTF8Bytes(value: string): Uint8Array {
if (typeof TextEncoder !== "undefined") {
return new TextEncoder().encode(value);
}

const encoded = encodeURIComponent(value);
const bytes: number[] = [];
for (let i = 0; i < encoded.length; ++i) {
if (encoded[i] === "%") {
bytes.push(parseInt(encoded.substring(i + 1, i + 3), 16));
i += 2;
} else {
bytes.push(encoded.charCodeAt(i));
}
}
return Uint8Array.from(bytes);
}

function bytesToBase64(bytes: Uint8Array): string {
let output = "";
for (let i = 0; i < bytes.length; i += 3) {
const byte1 = bytes[i];
const hasByte2 = i + 1 < bytes.length;
const hasByte3 = i + 2 < bytes.length;
const byte2 = hasByte2 ? bytes[i + 1] : 0;
const byte3 = hasByte3 ? bytes[i + 2] : 0;

output += BASE64_ALPHABET[byte1 >> 2];
output += BASE64_ALPHABET[((byte1 & 0x03) << 4) | (byte2 >> 4)];
output += hasByte2 ? BASE64_ALPHABET[((byte2 & 0x0f) << 2) | (byte3 >> 6)] : "=";
output += hasByte3 ? BASE64_ALPHABET[byte3 & 0x3f] : "=";
}
return output;
}

export function utf8ToBase64(value: string = ""): string {
const normalized = value == null ? "" : String(value);

const maybeBuffer = (globalThis as any)?.Buffer;
if (maybeBuffer?.from) {
return maybeBuffer.from(normalized, "utf8").toString("base64");
}

return bytesToBase64(toUTF8Bytes(normalized));
}
22 changes: 22 additions & 0 deletions packages/util/tests/utf8ToBase64.node.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it, expect } from "vitest";
import { utf8ToBase64 } from "@hpcc-js/util";

describe("utf8ToBase64", () => {
it("encodes multi-byte characters", () => {
const sample = "Привет, 世界 👋";
const expected = Buffer.from(sample, "utf8").toString("base64");
expect(utf8ToBase64(sample)).toEqual(expected);
});

it("falls back when Buffer is unavailable", () => {
const sample = "mañana ☀️";
const expected = Buffer.from(sample, "utf8").toString("base64");
const original = (globalThis as any).Buffer;
(globalThis as any).Buffer = undefined;
try {
expect(utf8ToBase64(sample)).toEqual(expected);
} finally {
(globalThis as any).Buffer = original;
}
});
});
32 changes: 32 additions & 0 deletions packages/util/tests/utf8ToBase64.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect } from "vitest";
import { utf8ToBase64 } from "@hpcc-js/util";

describe("utf8ToBase64", () => {
it("encodes ASCII input", () => {
expect(utf8ToBase64("hello world"))
.toEqual("aGVsbG8gd29ybGQ=");
});

it("treats nullish values as empty string", () => {
expect(utf8ToBase64(undefined as unknown as string)).toEqual("");
expect(utf8ToBase64(null as unknown as string)).toEqual("");
});

it("check frequest characters", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo? "frequest"

expect(utf8ToBase64("hello world")).toEqual(btoa("hello world"));
expect(utf8ToBase64("g@s*!")).toEqual(btoa("g@s*!"));
expect(utf8ToBase64("~@:!$%^&*()_-;'#***")).toEqual(btoa("~@:!$%^&*()_-;'#***"));
expect(utf8ToBase64("!$%^&*()_-;'#:@~")).toEqual(btoa("!$%^&*()_-;'#:@~"));
// expect(utf8ToBase64("¬!£$%^&*()_-;'#:@~")).toEqual(btoa("¬!£$%^&*()_-;'#:@~"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a commented out test case here

});

describe("incremental ASCII coverage", () => {
const printableAscii = Array.from({ length: 95 }, (_, i) => String.fromCharCode(32 + i)).join("");
const checkpoints = [1, 5, 10, 20, 32, 48, 64, 80, 95];
const cases = checkpoints.map((len) => [len, printableAscii.slice(0, len)] as const);

it.each(cases)("encodes first %i printable ASCII chars", (_length, sample) => {
expect(utf8ToBase64(sample)).toEqual(btoa(sample));
});
});
});
Loading