1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 19:23:19 +00:00

refactor: introduce @bitwarden/encoding

This commit is contained in:
addisonbeck
2025-08-12 15:09:02 -04:00
parent b1cea2359e
commit db5b08bd0f
15 changed files with 317 additions and 0 deletions

1
.github/CODEOWNERS vendored
View File

@@ -103,6 +103,7 @@ libs/core-test-utils @bitwarden/team-platform-dev
libs/state @bitwarden/team-platform-dev
libs/state-test-utils @bitwarden/team-platform-dev
libs/device-type @bitwarden/team-platform-dev
libs/encoding @bitwarden/team-platform-dev
# Web utils used across app and connectors
apps/web/src/utils/ @bitwarden/team-platform-dev
# Web core and shared files

View File

@@ -71,6 +71,7 @@ export class Utils {
}
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromB64ToArray(str: string): Uint8Array {
if (str == null) {
return null;
@@ -88,10 +89,12 @@ export class Utils {
}
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromUrlB64ToArray(str: string): Uint8Array {
return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str));
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromHexToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "hex"));
@@ -104,6 +107,7 @@ export class Utils {
}
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromUtf8ToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "utf8"));
@@ -117,6 +121,7 @@ export class Utils {
}
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromByteStringToArray(str: string): Uint8Array {
if (str == null) {
return null;
@@ -128,6 +133,7 @@ export class Utils {
return arr;
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromBufferToB64(buffer: ArrayBuffer): string {
if (buffer == null) {
return null;
@@ -144,23 +150,28 @@ export class Utils {
}
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromB64toUrlB64(b64Str: string) {
return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromBufferToUtf8(buffer: ArrayBuffer): string {
return BufferLib.from(buffer).toString("utf8");
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromBufferToByteString(buffer: ArrayBuffer): string {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
}
// ref: https://stackoverflow.com/a/40031979/1090359
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromBufferToHex(buffer: ArrayBuffer): string {
if (Utils.isNode) {
return Buffer.from(buffer).toString("hex");
@@ -179,6 +190,7 @@ export class Utils {
* @param {string} hexString - A string of hexadecimal characters.
* @returns {ArrayBuffer} The ArrayBuffer representation of the hex string.
*/
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static hexStringToArrayBuffer(hexString: string): ArrayBuffer {
// Check if the hexString has an even length, as each hex digit represents half a byte (4 bits),
// and it takes two hex digits to represent a full byte (8 bits).
@@ -209,6 +221,7 @@ export class Utils {
return arrayBuffer;
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromUrlB64ToB64(urlB64Str: string): string {
let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/");
switch (output.length % 4) {
@@ -227,10 +240,12 @@ export class Utils {
return output;
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromUrlB64ToUtf8(urlB64Str: string): string {
return Utils.fromB64ToUtf8(Utils.fromUrlB64ToB64(urlB64Str));
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromUtf8ToB64(utfStr: string): string {
if (Utils.isNode) {
return Buffer.from(utfStr, "utf8").toString("base64");
@@ -239,10 +254,12 @@ export class Utils {
}
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromUtf8ToUrlB64(utfStr: string): string {
return Utils.fromBufferToUrlB64(Utils.fromUtf8ToArray(utfStr));
}
/** @deprecated Moved to @bitwarden/encoding as a directly exported function*/
static fromB64ToUtf8(b64Str: string): string {
if (Utils.isNode) {
return Buffer.from(b64Str, "base64").toString("utf8");

5
libs/encoding/README.md Normal file
View File

@@ -0,0 +1,5 @@
# encoding
Owned by: platform
Encoding helpers

View File

@@ -0,0 +1,3 @@
import baseConfig from "../../eslint.config.mjs";
export default [...baseConfig];

View File

@@ -0,0 +1,10 @@
module.exports = {
displayName: "encoding",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/encoding",
};

View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/encoding",
"version": "0.0.1",
"description": "Encoding helpers",
"private": true,
"type": "commonjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "GPL-3.0",
"author": "platform"
}

View File

@@ -0,0 +1,33 @@
{
"name": "encoding",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/encoding/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/encoding",
"main": "libs/encoding/src/index.ts",
"tsConfig": "libs/encoding/tsconfig.lib.json",
"assets": ["libs/encoding/*.md"]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/encoding/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/encoding/jest.config.js"
}
}
}
}

View File

@@ -0,0 +1,8 @@
import * as lib from "./index";
describe("encoding", () => {
// This test will fail until something is exported from index.ts
it("should work", () => {
expect(lib).toBeDefined();
});
});

180
libs/encoding/src/index.ts Normal file
View File

@@ -0,0 +1,180 @@
import { Buffer as BufferLib } from "buffer/";
const isNode = typeof (globalThis as any)?.process?.versions?.node === "string";
// We cast globalThis to any to keep typescript happy
const g: any = globalThis as any;
export function fromB64ToArray(str: string): Uint8Array | null {
if (str == null) {
return null;
}
if (isNode) {
return new Uint8Array(Buffer.from(str, "base64"));
} else {
const binaryString = g.atob(str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
}
export function fromUrlB64ToArray(str: string): Uint8Array | null {
return fromB64ToArray(fromUrlB64ToB64(str));
}
export function fromHexToArray(str: string): Uint8Array {
if (isNode) {
return new Uint8Array(Buffer.from(str, "hex"));
} else {
const bytes = new Uint8Array(str.length / 2);
for (let i = 0; i < str.length; i += 2) {
bytes[i / 2] = parseInt(str.substr(i, 2), 16);
}
return bytes;
}
}
export function fromUtf8ToArray(str: string): Uint8Array {
if (isNode) {
return new Uint8Array(Buffer.from(str, "utf8"));
} else {
const strUtf8 = unescape(encodeURIComponent(str));
const arr = new Uint8Array(strUtf8.length);
for (let i = 0; i < strUtf8.length; i++) {
arr[i] = strUtf8.charCodeAt(i);
}
return arr;
}
}
export function fromByteStringToArray(str: string): Uint8Array | null {
if (str == null) {
return null;
}
const arr = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
arr[i] = str.charCodeAt(i);
}
return arr;
}
export function fromBufferToB64(buffer: ArrayBuffer): string | null {
if (buffer == null) {
return null;
}
if (isNode) {
return Buffer.from(buffer).toString("base64");
} else {
let binary = "";
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return g.btoa(binary);
}
}
export function fromBufferToUrlB64(buffer: ArrayBuffer): string | null {
const b64 = fromBufferToB64(buffer);
return b64 == null ? null : fromB64toUrlB64(b64);
}
export function fromB64toUrlB64(b64Str: string): string {
return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
export function fromBufferToUtf8(buffer: ArrayBuffer): string {
return BufferLib.from(buffer).toString("utf8");
}
export function fromBufferToByteString(buffer: ArrayBuffer): string {
return String.fromCharCode.apply(null, new Uint8Array(buffer) as unknown as number[]);
}
// ref: https://stackoverflow.com/a/40031979/1090359
export function fromBufferToHex(buffer: ArrayBuffer): string {
if (isNode) {
return Buffer.from(buffer).toString("hex");
} else {
const bytes = new Uint8Array(buffer);
return Array.prototype.map
.call(bytes, (x: number) => ("00" + x.toString(16)).slice(-2))
.join("");
}
}
/**
* Converts a hex string to an ArrayBuffer.
* Note: this doesn't need any Node specific code as parseInt() / ArrayBuffer / Uint8Array
* work the same in Node and the browser.
* @param {string} hexString - A string of hexadecimal characters.
* @returns {ArrayBuffer} The ArrayBuffer representation of the hex string.
*/
export function hexStringToArrayBuffer(hexString: string): ArrayBuffer {
// Check if the hexString has an even length, as each hex digit represents half a byte (4 bits),
// and it takes two hex digits to represent a full byte (8 bits).
if (hexString.length % 2 !== 0) {
throw "HexString has to be an even length";
}
// Create an ArrayBuffer with a length that is half the length of the hex string,
// because each pair of hex digits will become a single byte.
const arrayBuffer = new ArrayBuffer(hexString.length / 2);
// Create a Uint8Array view on top of the ArrayBuffer (each position represents a byte)
// as ArrayBuffers cannot be edited directly.
const uint8Array = new Uint8Array(arrayBuffer);
// Loop through the bytes
for (let i = 0; i < uint8Array.length; i++) {
// Extract two hex characters (1 byte)
const hexByte = hexString.substr(i * 2, 2);
// Convert hexByte into a decimal value from base 16. (ex: ff --> 255)
const byteValue = parseInt(hexByte, 16);
// Place the byte value into the uint8Array
uint8Array[i] = byteValue;
}
return arrayBuffer;
}
export function fromUrlB64ToB64(urlB64Str: string): string {
let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/");
switch (output.length % 4) {
case 0:
break;
case 2:
output += "==";
break;
case 3:
output += "=";
break;
default:
throw new Error("Illegal base64url string!");
}
return output;
}
export function fromUtf8ToB64(utfStr: string): string {
if (isNode) {
return Buffer.from(utfStr, "utf8").toString("base64");
} else {
return BufferLib.from(utfStr, "utf8").toString("base64");
}
}
export function fromUtf8ToUrlB64(utfStr: string): string | null {
return fromBufferToUrlB64(fromUtf8ToArray(utfStr));
}
export function fromB64ToUtf8(b64Str: string): string {
if (isNode) {
return Buffer.from(b64Str, "base64").toString("utf8");
} else {
return BufferLib.from(b64Str, "base64").toString("utf8");
}
}

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["**/build", "**/dist"]
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

9
package-lock.json generated
View File

@@ -347,6 +347,11 @@
"version": "0.0.0",
"license": "GPL-3.0"
},
"libs/encoding": {
"name": "@bitwarden/encoding",
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/guid": {
"name": "@bitwarden/guid",
"version": "0.0.1",
@@ -4612,6 +4617,10 @@
"resolved": "libs/dirt/card",
"link": true
},
"node_modules/@bitwarden/encoding": {
"resolved": "libs/encoding",
"link": true
},
"node_modules/@bitwarden/generator-components": {
"resolved": "libs/tools/generator/components",
"link": true

View File

@@ -31,6 +31,7 @@
"@bitwarden/core-test-utils": ["libs/core-test-utils/src/index.ts"],
"@bitwarden/device-type": ["libs/device-type/src/index.ts"],
"@bitwarden/dirt-card": ["./libs/dirt/card/src"],
"@bitwarden/encoding": ["libs/encoding/src/index.ts"],
"@bitwarden/generator-components": ["./libs/tools/generator/components/src"],
"@bitwarden/generator-core": ["./libs/tools/generator/core/src"],
"@bitwarden/generator-history": ["./libs/tools/generator/extensions/history/src"],