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

Add encoding utils in core utils #26592

Merged
merged 13 commits into from
Aug 2, 2023
8 changes: 2 additions & 6 deletions sdk/core/core-util/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# Release History

## 1.3.3 (Unreleased)
## 1.3.3 (2023-08-01)

### Features Added

### Breaking Changes

### Bugs Fixed

### Other Changes
- Add helper functions `uint8ArrayToString` and `stringToUint8Array` for transform between string and bytes array with different character encodings.

## 1.3.2 (2023-05-05)

Expand Down
3 changes: 2 additions & 1 deletion sdk/core/core-util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"module": "dist-esm/src/index.js",
"browser": {
"./dist-esm/src/sha256.js": "./dist-esm/src/sha256.browser.js",
"./dist-esm/src/uuidUtils.js": "./dist-esm/src/uuidUtils.browser.js"
"./dist-esm/src/uuidUtils.js": "./dist-esm/src/uuidUtils.browser.js",
"./dist-esm/src/bytesEncoding.js": "./dist-esm/src/bytesEncoding.browser.js"
},
"react-native": {
"./dist/index.js": "./dist-esm/src/index.js",
Expand Down
9 changes: 9 additions & 0 deletions sdk/core/core-util/review/core-util.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export interface DelayOptions {
abortSignal?: AbortSignalLike;
}

// @public
export type EncodingType = "utf-8" | "base64" | "base64url";

// @public
export function getErrorMessage(e: unknown): string;

Expand Down Expand Up @@ -73,6 +76,12 @@ export function objectHasProperty<Thing, PropertyName extends string>(thing: Thi
// @public
export function randomUUID(): string;

// @public
export function stringToUint8Array(value: string, format: EncodingType): Uint8Array;

// @public
export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string;

// @public
export type UnknownObject = {
[s: string]: unknown;
Expand Down
106 changes: 106 additions & 0 deletions sdk/core/core-util/src/bytesEncoding.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

declare global {
// stub these out for the browser
function btoa(input: string): string;
function atob(input: string): string;
class TextDecoder {
constructor(encoding?: string);
decode(input?: ArrayBufferView | ArrayBuffer): string;
}
class TextEncoder {
constructor(encoding?: string);
encode(input?: string): Uint8Array;
}
}

/** The supported character encoding type */
export type EncodingType = "utf-8" | "base64" | "base64url";

/**
* The helper that transforms bytes with specific character encoding into string
* @param bytes - the uint8array bytes
* @param format - the format we use to encode the byte
* @returns a string of the encoded string
*/
export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string {
switch (format) {
case "utf-8":
return uint8ArrayToUtf8String(bytes);
case "base64":
return uint8ArrayToBase64(bytes);
case "base64url":
return uint8ArrayToBase64Url(bytes);
}
}

/**
* The helper that transforms string to specific character encoded bytes array.
* @param value - the string to be converted
* @param format - the format we use to decode the value
* @returns a uint8array
*/
export function stringToUint8Array(value: string, format: EncodingType): Uint8Array {
switch (format) {
case "utf-8":
return utf8StringToUint8Array(value);
case "base64":
return base64ToUint8Array(value);
case "base64url":
return base64UrlToUint8Array(value);
}
}

/**
* Decodes a Uint8Array into a Base64 string.
* @internal
*/
export function uint8ArrayToBase64(uint8Array: Uint8Array): string {
const decoder = new TextDecoder();
const dataString = decoder.decode(uint8Array);
return btoa(dataString);
}

/**
* Decodes a Uint8Array into a Base64Url string.
* @internal
*/
export function uint8ArrayToBase64Url(bytes: Uint8Array): string {
return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

/**
* Decodes a Uint8Array into a javascript string.
* @internal
*/
export function uint8ArrayToUtf8String(uint8Array: Uint8Array): string {
const decoder = new TextDecoder("utf-8");
const dataString = decoder.decode(uint8Array);
return dataString;
}

/**
* Encodes a JavaScript string into a Uint8Array.
* @internal
*/
export function utf8StringToUint8Array(value: string): Uint8Array {
return new TextEncoder("utf-8").encode(value);
}

/**
* Encodes a Base64 string into a Uint8Array.
* @internal
*/
export function base64ToUint8Array(value: string): Uint8Array {
return new TextEncoder().encode(atob(value));
}

/**
* Encodes a Base64Url string into a Uint8Array.
* @internal
*/
export function base64UrlToUint8Array(value: string): Uint8Array {
const base64String = value.replace(/-/g, "+").replace(/_/g, "/");
return base64ToUint8Array(base64String);
}
87 changes: 87 additions & 0 deletions sdk/core/core-util/src/bytesEncoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/** The supported character encoding type */
export type EncodingType = "utf-8" | "base64" | "base64url";

/**
* The helper that transforms bytes with specific character encoding into string
* @param bytes - the uint8array bytes
* @param format - the format we use to encode the byte
* @returns a string of the encoded string
*/
export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string {
switch (format) {
case "utf-8":
return uint8ArrayToUtf8String(bytes);
case "base64":
return uint8ArrayToBase64(bytes);
case "base64url":
return uint8ArrayToBase64Url(bytes);
}
}

/**
* The helper that transforms string to specific character encoded bytes array.
* @param value - the string to be converted
* @param format - the format we use to decode the value
* @returns a uint8array
*/
export function stringToUint8Array(value: string, format: EncodingType): Uint8Array {
switch (format) {
case "utf-8":
return utf8StringToUint8Array(value);
case "base64":
return base64ToUint8Array(value);
case "base64url":
return base64UrlToUint8Array(value);
}
}

/**
* Decodes a Uint8Array into a Base64 string.
* @internal
*/
export function uint8ArrayToBase64(bytes: Uint8Array): string {
return Buffer.from(bytes).toString("base64");
}

/**
* Decodes a Uint8Array into a Base64Url string.
* @internal
*/
export function uint8ArrayToBase64Url(bytes: Uint8Array): string {
return Buffer.from(bytes).toString("base64url");
}

/**
* Decodes a Uint8Array into a javascript string.
* @internal
*/
export function uint8ArrayToUtf8String(bytes: Uint8Array): string {
return Buffer.from(bytes).toString("utf-8");
}

/**
* Encodes a JavaScript string into a Uint8Array.
* @internal
*/
export function utf8StringToUint8Array(value: string): Uint8Array {
return Buffer.from(value);
}

/**
* Encodes a Base64 string into a Uint8Array.
* @internal
*/
export function base64ToUint8Array(value: string): Uint8Array {
return Buffer.from(value, "base64");
}

/**
* Encodes a Base64Url string into a Uint8Array.
* @internal
*/
export function base64UrlToUint8Array(value: string): Uint8Array {
return Buffer.from(value, "base64url");
}
1 change: 1 addition & 0 deletions sdk/core/core-util/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { computeSha256Hash, computeSha256Hmac } from "./sha256";
export { isDefined, isObjectWithProperties, objectHasProperty } from "./typeGuards";
export { randomUUID } from "./uuidUtils";
export { isBrowser, isBun, isNode, isDeno, isReactNative, isWebWorker } from "./checkEnvironment";
export { uint8ArrayToString, stringToUint8Array, EncodingType } from "./bytesEncoding";
115 changes: 115 additions & 0 deletions sdk/core/core-util/test/public/browser/bytesEncoding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { isBrowser, isNode } from "../../../src";
import { stringToUint8Array, uint8ArrayToString } from "../../../src/bytesEncoding";
import { assert } from "chai";

describe("Base64", function () {
describe("isBrowser (node)", function () {
it("should return true", async function () {
assert.isTrue(isBrowser);
});
});
describe("isNode (node)", function () {
it("should return false", async function () {
assert.isFalse(isNode);
});
});
describe("base64ToBytes", function () {
it("converts a base64 string to bytes", function () {
const input = "YXp1cmU="; // 'azure' in base64.

const output = stringToUint8Array(input, "base64");
assert.deepEqual(
output,
new Uint8Array([97, 122, 117, 114, 101]),
"Incorrect conversion of base64 to bytes."
);
});

it("converts a utf-8 string to bytes", function () {
const input = "\x61\x7A\x75\x72\x65"; // 'azure' in utf8.

const output = stringToUint8Array(input, "utf-8");
assert.deepEqual(
output,
new Uint8Array([97, 122, 117, 114, 101]),
"Incorrect conversion of utf-8 to bytes."
);
});

it("converts a base64url string to bytes", function () {
const input = "YXp1cmU"; // 'azure' in base64url.

const output = stringToUint8Array(input, "base64url");
assert.deepEqual(
output,
new Uint8Array([97, 122, 117, 114, 101]),
"Incorrect conversion of utf-8 to bytes."
);
});
});

describe("bufferToBase64", function () {
it("converts an uint8array to a base64 string", function () {
const input = new Uint8Array([97, 122, 117, 114, 101]); // 'azure' in base64.

const output = uint8ArrayToString(input, "base64");
assert.deepEqual(output, "YXp1cmU=", "Incorrect conversion of bytes to base64.");
});

it("converts an uint8array to a utf-8 string", function () {
const input = new Uint8Array([97, 122, 117, 114, 101]); // 'azure' in utf8.

const output = uint8ArrayToString(input, "utf-8");
assert.deepEqual(output, "\x61\x7A\x75\x72\x65", "Incorrect conversion of bytes to utf-8.");
});

it("converts an uint8array to a base64url string", function () {
const input = new Uint8Array([97, 122, 117, 114, 101]); // 'azure' in base64.

const output = uint8ArrayToString(input, "base64url");
assert.deepEqual(output, "YXp1cmU", "Incorrect conversion of bytes to base64.");
});

it("has proper padding", function () {
const scenarios = [
{ bytes: new Uint8Array([65]), expected: "QQ==" },
{ bytes: new Uint8Array([65, 90]), expected: "QVo=" },
{ bytes: new Uint8Array([65, 66, 67]), expected: "QUJD" },
];

for (const scenario of scenarios) {
const output = uint8ArrayToString(scenario.bytes, "base64");
assert.equal(output, scenario.expected, "Incorrect conversion of bytes to base64.");
}
});

it("has proper padding", function () {
const scenarios = [
{ bytes: new Uint8Array([65]), expected: "A" },
{ bytes: new Uint8Array([65, 90]), expected: "AZ" },
{ bytes: new Uint8Array([65, 66, 67]), expected: "ABC" },
];

for (const scenario of scenarios) {
const output = uint8ArrayToString(scenario.bytes, "utf-8");
assert.equal(output, scenario.expected, "Incorrect conversion of bytes to utf-8.");
}
});

it("has proper padding", function () {
const scenarios = [
{ bytes: new Uint8Array([65]), expected: "QQ" },
{ bytes: new Uint8Array([65, 90]), expected: "QVo" },
{ bytes: new Uint8Array([65, 66, 67]), expected: "QUJD" },
];

for (const scenario of scenarios) {
const output = uint8ArrayToString(scenario.bytes, "base64url");
assert.equal(output, scenario.expected, "Incorrect conversion of bytes to base64.");
}
});
});
});
Loading