From b2ff6910927dc599f4b40771b73ba9bc134f87d4 Mon Sep 17 00:00:00 2001 From: Nev Date: Wed, 25 Feb 2026 20:04:53 -0800 Subject: [PATCH 1/2] feat: Add comprehensive encoding utilities (Base64, Hex, URI) with ES5 polyfill support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete encoding/decoding functions with cross-environment compatibility: Core Functions: - encodeAsBase64/decodeBase64: Standard Base64 encoding with native btoa/atob fallback - encodeAsBase64Url/decodeBase64Url: URL-safe Base64 (+ → -, / → _, no padding) - encodeAsHex/decodeHex: Hexadecimal character encoding - encodeAsUri/decodeUri: URI component encoding with encodeURIComponent fallback Documentation: - Updated README with new "String Manipulation & Encoding" section - Added documentation links to all 8 new functions in utilities table - Updated lib/src/index.ts exports with wrapped format (140 char limit) --- .size-limit.json | 12 +- README.md | 9 +- lib/src/helpers/customError.ts | 2 +- lib/src/helpers/encode.ts | 344 ++++++++++++++++- lib/src/index.ts | 5 +- lib/test/bundle-size-check.js | 8 +- lib/test/src/common/helpers/encode.test.ts | 411 ++++++++++++++++++++- 7 files changed, 773 insertions(+), 18 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index a014aba9..3876dd09 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,42 +2,42 @@ { "name": "es5-full", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "22.5 kb", + "limit": "24.5 kb", "brotli": false, "running": false }, { "name": "es6-full", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "21.5 kb", + "limit": "23.5 kb", "brotli": false, "running": false }, { "name": "es5-full-brotli", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "8.5 kb", + "limit": "9 kb", "brotli": true, "running": false }, { "name": "es6-full-brotli", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "8 kb", + "limit": "9 kb", "brotli": true, "running": false }, { "name": "es5-zip", "path": "lib/dist/es5/mod/ts-utils.js", - "limit": "9 Kb", + "limit": "9.5 Kb", "gzip": true, "running": false }, { "name": "es6-zip", "path": "lib/dist/es6/mod/ts-utils.js", - "limit": "9 Kb", + "limit": "9.5 Kb", "gzip": true, "running": false }, diff --git a/README.md b/README.md index 52d17cc2..b4af81f9 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,13 @@ npm install @nevware21/ts-utils --save - Deep copy/extend - Object transformation helpers -### String Manipulation +### String Manipulation & Encoding - Case conversion (camelCase, kebab-case, snake_case) - String transformation utilities -- HTML and JSON encoding +- Data encoding: Base64 (standard and URL-safe), Hexadecimal, URI +- Data decoding with ES5-compatible polyfill support +- HTML and JSON encoding for security +- Cross-environment compatibility with optimized implementations ### Function Helpers - Function binding and proxying utilities @@ -123,7 +126,7 @@ Below is a categorized list of all available utilities with direct links to thei | String | [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isString](https://nevware21.github.io/ts-utils/typedoc/functions/isString.html)(); [strEndsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strEndsWith.html)(); [strIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strIndexOf.html)(); [strIsNullOrEmpty](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrEmpty.html)(); [strIsNullOrWhiteSpace](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrWhiteSpace.html)(); [strLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strLastIndexOf.html)(); [strLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strLeft.html)(); [strPadEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strPadEnd.html)(); [strPadStart](https://nevware21.github.io/ts-utils/typedoc/functions/strPadStart.html)(); [strRepeat](https://nevware21.github.io/ts-utils/typedoc/functions/strRepeat.html)(); [strRight](https://nevware21.github.io/ts-utils/typedoc/functions/strRight.html)(); [strSlice](https://nevware21.github.io/ts-utils/typedoc/functions/strSlice.html)(); [strSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSplit.html)(); [strStartsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strStartsWith.html)(); [strSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstr.html)(); [strSubstring](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstring.html)(); [strSymSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSymSplit.html)(); [strTrim](https://nevware21.github.io/ts-utils/typedoc/functions/strTrim.html)(); [strTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimEnd.html)(); [strTrimLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimLeft.html)(); [strTrimRight](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimRight.html)(); [strTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimStart.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); [strContains](https://nevware21.github.io/ts-utils/typedoc/functions/strContains.html)(); [strIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/strIncludes.html)();
[polyStrSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrSubstr.html)(); [polyStrTrim](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrim.html)(); [polyStrTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimEnd.html)(); [polyStrTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimStart.html)(); [polyStrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrIncludes.html)();
| Symbol | [WellKnownSymbols](https://nevware21.github.io/ts-utils/typedoc/enums/WellKnownSymbols.html) (const enum);
[getKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getKnownSymbol.html)(); [getSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getSymbol.html)(); [hasSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/hasSymbol.html)(); [isSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/isSymbol.html)(); [newSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/newSymbol.html)(); [symbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolFor.html)(); [symbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolKeyFor.html)();
[polyGetKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyGetKnownSymbol.html)(); [polyNewSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyNewSymbol.html)(); [polySymbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolFor.html)(); [polySymbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolKeyFor.html)();

Polyfills are used to automatically backfill runtimes that do not support `Symbol`, not all of the Symbol functionality is provided. | Timer | [createTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeout.html)(); [createTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeoutWith.html)(); [elapsedTime](https://nevware21.github.io/ts-utils/typedoc/functions/elapsedTime.html)(); [perfNow](https://nevware21.github.io/ts-utils/typedoc/functions/perfNow.html)(); [setGlobalTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setGlobalTimeoutOverrides.html)(); [setTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setTimeoutOverrides.html)(); [utcNow](https://nevware21.github.io/ts-utils/typedoc/functions/utcNow.html)(); [scheduleIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleIdleCallback.html)(); [scheduleInterval](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleInterval.html)(); [scheduleTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeout.html)(); [scheduleTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeoutWith.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)();
For runtimes that don't support `requestIdleCallback` normal setTimeout() is used with the values from [`setDefaultIdleTimeout`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultIdleTimeout.html)() and [`setDefaultMaxExecutionTime`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultMaxExecutionTime.html)();
[polyUtcNow](https://nevware21.github.io/ts-utils/typedoc/functions/polyUtcNow.html)(); -| Conversion | [encodeAsJson](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsJson.html)(); [encodeAsHtml](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHtml.html)(); [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [normalizeJsName](https://nevware21.github.io/ts-utils/typedoc/functions/normalizeJsName.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); +| Conversion & Encoding | [encodeAsJson](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsJson.html)(); [encodeAsHtml](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHtml.html)(); [encodeAsBase64](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsBase64.html)(); [decodeBase64](https://nevware21.github.io/ts-utils/typedoc/functions/decodeBase64.html)(); [encodeAsBase64Url](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsBase64Url.html)(); [decodeBase64Url](https://nevware21.github.io/ts-utils/typedoc/functions/decodeBase64Url.html)(); [encodeAsHex](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHex.html)(); [decodeHex](https://nevware21.github.io/ts-utils/typedoc/functions/decodeHex.html)(); [encodeAsUri](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsUri.html)(); [decodeUri](https://nevware21.github.io/ts-utils/typedoc/functions/decodeUri.html)(); [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [normalizeJsName](https://nevware21.github.io/ts-utils/typedoc/functions/normalizeJsName.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); | Cache | [createCachedValue](https://nevware21.github.io/ts-utils/typedoc/functions/createCachedValue.html)(); [createDeferredCachedValue](https://nevware21.github.io/ts-utils/typedoc/functions/createDeferredCachedValue.html)(); [getDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/getDeferred.html)(); [getWritableDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/getWritableDeferred.html)(); | Lazy | [getLazy](https://nevware21.github.io/ts-utils/typedoc/functions/getLazy.html)(); [getWritableLazy](https://nevware21.github.io/ts-utils/typedoc/functions/getWritableLazy.html)(); [lazySafeGetInst](https://nevware21.github.io/ts-utils/typedoc/functions/lazySafeGetInst.html)(); [safeGetLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetLazy.html)(); [safeGetLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetLazy.html)(); [setBypassLazyCache](https://nevware21.github.io/ts-utils/typedoc/functions/setBypassLazyCache.html)(); | Safe | [safe](https://nevware21.github.io/ts-utils/typedoc/functions/safe.html)(); [safeGetLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetLazy.html)(); [safeGet](https://nevware21.github.io/ts-utils/typedoc/functions/safeGet.html)(); [safeGetDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetDeferred.html)(); [safeGetWritableDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetWritableDeferred.html)(); [lazySafeGetInst](https://nevware21.github.io/ts-utils/typedoc/functions/lazySafeGetInst.html)(); [safeGetWritableLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetWritableLazy.html)(); diff --git a/lib/src/helpers/customError.ts b/lib/src/helpers/customError.ts index 9e710c29..b9ef350f 100644 --- a/lib/src/helpers/customError.ts +++ b/lib/src/helpers/customError.ts @@ -55,7 +55,7 @@ function _setName(baseClass: any, name: string) { * @param constructCb - [Optional] An optional callback function to call when a * new Custom Error instance is being created. * @param errorBase - [Optional] (since v0.9.6) The error class to extend for this class, defaults to Error. - * @param superArgsFn - [Optional] (since v0.12.7) An optional function that receives the constructor arguments and + * @param superArgsFn - [Optional] (since v0.13.0) An optional function that receives the constructor arguments and * returns the arguments to pass to the base class constructor. When not provided all constructor * arguments are forwarded to the base class. Use this to support a different argument order or * to pass a subset of arguments to the base class (similar to calling `super(...)` in a class). diff --git a/lib/src/helpers/encode.ts b/lib/src/helpers/encode.ts index 2487146f..5b309336 100644 --- a/lib/src/helpers/encode.ts +++ b/lib/src/helpers/encode.ts @@ -6,17 +6,26 @@ * Licensed under the MIT license. */ -import { NULL_VALUE, TO_STRING, UNDEF_VALUE } from "../internal/constants"; +import { EMPTY, NULL_VALUE, TO_STRING, UNDEF_VALUE } from "../internal/constants"; import { asString } from "../string/as_string"; import { strCamelCase } from "../string/conversion"; import { strPadStart } from "../string/pad"; +import { strRepeat } from "../string/repeat"; +import { strSubstr } from "../string/substring"; import { strUpper } from "../string/upper_lower"; -import { isNumber, isString, isUndefined } from "./base"; +import { isNumber, isStrictNullOrUndefined, isString, isUndefined } from "./base"; +import { ICachedValue } from "./cache"; +import { safeGetDeferred } from "./safe_lazy"; import { dumpObj } from "./diagnostics"; const DBL_QUOTE = "\""; const INVALID_JS_NAME = /([^\w\d_$])/g; +const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +const HEX_CHARS = "0123456789abcdef"; +let _btoa: ICachedValue<(value: string) => string>; +let _atob: ICachedValue<(value: string) => string>; let _htmlEntityCache: { [key: string]: string}; +let _base64Cache: { [key: string]: number }; /** * Validates that the string name conforms to the JS IdentifierName specification and if not @@ -177,3 +186,334 @@ export function encodeAsHtml(value: string) { return asString(value).replace(/[&<>"']/g, match => "&" + _htmlEntityCache[match] + ";"); } + +/** + * Encode a string value as Base64 + * @since 0.13.0 + * @group Encoding + * @group Conversion + * @param value - The string value to encode + * @returns The Base64 encoded string + * @example + * ```ts + * encodeAsBase64("Hello"); // "SGVsbG8=" + * encodeAsBase64("Hello World"); // "SGVsbG8gV29ybGQ=" + * encodeAsBase64(""); // "" + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function encodeAsBase64(value: string): string { + let result: string; + + if (value || !isStrictNullOrUndefined(value)) { + result = asString(value); + + try { + // Use native btoa if available + !_btoa && (_btoa = safeGetDeferred(() => !isUndefined(btoa) ? btoa : _encodeBase64Polyfill, _encodeBase64Polyfill)); + result = _btoa.v(result); + } catch (e) { + // Use polyfill on error + result = _encodeBase64Polyfill(result); + } + } + + return result || EMPTY; +} + +/** + * Decode a Base64 encoded string + * @since 0.13.0 + * @group Encoding + * @group Conversion + * @param value - The Base64 encoded string to decode + * @returns The decoded string + * @example + * ```ts + * decodeBase64("SGVsbG8="); // "Hello" + * decodeBase64("SGVsbG8gV29ybGQ="); // "Hello World" + * decodeBase64(""); // "" + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function decodeBase64(value: string): string { + let result: string; + + if (value || !isStrictNullOrUndefined(value)) { + result = asString(value); + + try { + // Use native atob if available + !_atob && (_atob = safeGetDeferred(() => !isUndefined(atob) ? atob : _decodeBase64Polyfill, _decodeBase64Polyfill)); + result = _atob.v(result); + } catch (e) { + // Use polyfill on error + result = _decodeBase64Polyfill(result); + } + } + + return result || value || EMPTY; +} + +/** + * Encode a string as URL-safe Base64 + * @since 0.13.0 + * @group Encoding + * @group Conversion + * @param value - The string value to encode + * @returns The URL-safe Base64 encoded string (replaces + with -, / with _, removes padding) + * @example + * ```ts + * encodeAsBase64Url("Hello"); // "SGVsbG8" + * encodeAsBase64Url("+++///"); // "LSsvLw" + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function encodeAsBase64Url(value: string): string { + let encoded = encodeAsBase64(value); + + if (encoded) { + encoded = encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + } + + return encoded || EMPTY; +} + +/** + * Decode a URL-safe Base64 encoded string + * @since 0.13.0 + * @group Encoding + * @group Conversion + * @param value - The URL-safe Base64 encoded string to decode + * @returns The decoded string + * @example + * ```ts + * decodeBase64Url("SGVsbG8"); // "Hello" + * decodeBase64Url("LSsvLw"); // "+++///" + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function decodeBase64Url(value: string): string { + let result: string; + + if (value || !isStrictNullOrUndefined(value)) { + result = asString(value); + let pad = 4 - (result.length % 4); // Add padding back + + if (pad && pad !== 4) { + result = result + strRepeat("=", pad); + } + + result = decodeBase64(result.replace(/-/g, "+").replace(/_/g, "/")) || EMPTY; + } + + return result || value || EMPTY; +} + +/** + * Encode a string as hexadecimal + * @since 0.13.0 + * @group Encoding + * @group Conversion + * @param value - The string value to encode + * @returns The hexadecimal encoded string + * @example + * ```ts + * encodeAsHex("Hello"); // "48656c6c6f" + * encodeAsHex("A"); // "41" + * encodeAsHex(""); // "" + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function encodeAsHex(value: string): string { + let result: string; + + if (value || !isStrictNullOrUndefined(value)) { + result = asString(value); + + let hex = []; + for (let idx = 0; idx < result.length; idx++) { + let code = result.charCodeAt(idx); + + hex.push(HEX_CHARS[(code >> 4) & 0xf]); + hex.push(HEX_CHARS[code & 0xf]); + } + + result = hex.join(""); + } + + return result || value || EMPTY; +} + +/** + * Decode a hexadecimal encoded string + * @since 0.13.0 + * @group Encoding + * @group Conversion + * @param value - The hexadecimal encoded string to decode + * @returns The decoded string + * @example + * ```ts + * decodeHex("48656c6c6f"); // "Hello" + * decodeHex("41"); // "A" + * decodeHex(""); // "" + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function decodeHex(value: string): string { + let result = []; + + if (value || !isStrictNullOrUndefined(value)) { + let theValue = asString(value); + + for (let idx = 0; idx < theValue.length; idx += 2) { + result.push(String.fromCharCode(parseInt(strSubstr(theValue, idx, 2), 16))); + } + } + + return result.join(""); +} + +/** + * Encode a string using URI encoding + * @since 0.13.0 + * @group Encoding + * @group Conversion + * @param value - The string value to encode + * @returns The URI encoded string + * @example + * ```ts + * encodeAsUri("Hello World"); // "Hello%20World" + * encodeAsUri("a+b=c"); // "a%2Bb%3Dc" + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function encodeAsUri(value: string): string { + let result: string; + + if (value || !isStrictNullOrUndefined(value)) { + try { + result = encodeURIComponent(asString(value)); + } catch (e) { + // Use original string on error + } + } + + return result || value || EMPTY; +} + +/** + * Decode a URI encoded string + * @since 0.13.0 + * @group Encoding + * @group Conversion + * @param value - The URI encoded string to decode + * @returns The decoded string + * @example + * ```ts + * decodeUri("Hello%20World"); // "Hello World" + * decodeUri("a%2Bb%3Dc"); // "a+b=c" + * ``` + */ +/*#__NO_SIDE_EFFECTS__*/ +export function decodeUri(value: string): string { + let result: string; + + if (value || !isStrictNullOrUndefined(value)) { + try { + result = decodeURIComponent(asString(value)); + } catch (e) { + // Use original string on error + } + } + + return result || value || EMPTY; +} + +/** + * Internal Base64 encoding polyfill for ES5 environments without btoa + * @internal + * @ignore + */ +export function _encodeBase64Polyfill(str: string): string { + let result = []; + + if (str || !isStrictNullOrUndefined(str)) { + str = asString(str); + + let len = str.length; + let lp = 0; + + while (lp < len) { + let a = str.charCodeAt(lp++); + + let hasB = lp < len; + let b = hasB ? str.charCodeAt(lp++) : 0; + + let hasC = lp < len; + let c = hasC ? str.charCodeAt(lp++) : 0; + + let bitmap = (a << 16) | (b << 8) | c; + + result.push(BASE64_CHARS[(bitmap >> 18) & 63]); + result.push(BASE64_CHARS[(bitmap >> 12) & 63]); + + if (hasB) { + result.push(BASE64_CHARS[(bitmap >> 6) & 63]); + } else { + result.push("="); + } + + if (hasC) { + result.push(BASE64_CHARS[bitmap & 63]); + } else { + result.push("="); + } + } + } + + return result.join(""); +} + +/** + * Internal Base64 decoding polyfill for ES5 environments without atob + * @internal + * @ignore + */ +export function _decodeBase64Polyfill(str: string): string { + let result = []; + + if (str || !isStrictNullOrUndefined(str)) { + str = asString(str); + let len = str.length; + + // Build the Base64 character cache if it doesn't exist + !_base64Cache && (_base64Cache = {}); + if (!_base64Cache["A"]) { + for (let i = 0; i < BASE64_CHARS.length; i++) { + _base64Cache[BASE64_CHARS[i]] = i; + } + } + + let idx = 0; + while (idx < len) { + let a = _base64Cache[str[idx++]] || 0; + let b = _base64Cache[str[idx++]] || 0; + let c = _base64Cache[str[idx++]] || 0; + let d = _base64Cache[str[idx++]] || 0; + let bitmap = (a << 18) | (b << 12) | (c << 6) | d; + + result.push(String.fromCharCode((bitmap >> 16) & 255)); + + if (str[idx - 2] !== "=") { + result.push(String.fromCharCode((bitmap >> 8) & 255)); + } + + if (str[idx - 1] !== "=") { + result.push(String.fromCharCode(bitmap & 255)); + } + } + } + + return result.join(""); +} diff --git a/lib/src/index.ts b/lib/src/index.ts index 3c0dcdf1..0c5fb18e 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -41,7 +41,10 @@ export { getGlobal, getInst, lazySafeGetInst, hasDocument, getDocument, hasWindow, getWindow, hasNavigator, getNavigator, hasHistory, getHistory, isNode, isWebWorker } from "./helpers/environment"; -export { encodeAsHtml, encodeAsJson, normalizeJsName } from "./helpers/encode"; +export { + encodeAsHtml, encodeAsJson, normalizeJsName, encodeAsBase64, decodeBase64, encodeAsBase64Url, + decodeBase64Url, encodeAsHex, decodeHex, encodeAsUri, decodeUri +} from "./helpers/encode"; export { deepExtend, objExtend } from "./helpers/extend"; export { getValueByKey, setValueByKey, getValueByIter, setValueByIter } from "./helpers/get_set_value"; export { ILazyValue, getLazy, setBypassLazyCache, getWritableLazy } from "./helpers/lazy"; diff --git a/lib/test/bundle-size-check.js b/lib/test/bundle-size-check.js index 1ea01258..215987f0 100644 --- a/lib/test/bundle-size-check.js +++ b/lib/test/bundle-size-check.js @@ -7,25 +7,25 @@ const configs = [ { name: "es5-min-full", path: "../bundle/es5/umd/ts-utils.min.js", - limit: 25.5 * 1024, // 25.5 kb in bytes + limit: 27.5 * 1024, // 27.5 kb in bytes compress: false }, { name: "es6-min-full", path: "../bundle/es6/umd/ts-utils.min.js", - limit: 25 * 1024, // 25 kb in bytes + limit: 27 * 1024, // 27 kb in bytes compress: false }, { name: "es5-min-zip", path: "../bundle/es5/umd/ts-utils.min.js", - limit: 10.5 * 1024, // 10.5 kb in bytes + limit: 11 * 1024, // 11 kb in bytes compress: true }, { name: "es6-min-zip", path: "../bundle/es6/umd/ts-utils.min.js", - limit: 10.5 * 1024, // 10.5 kb in bytes + limit: 11 * 1024, // 11 kb in bytes compress: true }, { diff --git a/lib/test/src/common/helpers/encode.test.ts b/lib/test/src/common/helpers/encode.test.ts index 68f56aad..54a5fc42 100644 --- a/lib/test/src/common/helpers/encode.test.ts +++ b/lib/test/src/common/helpers/encode.test.ts @@ -7,7 +7,11 @@ */ import { assert } from "@nevware21/tripwire-chai"; -import { encodeAsHtml, encodeAsJson, normalizeJsName } from "../../../../src/helpers/encode"; +import { + encodeAsHtml, encodeAsJson, normalizeJsName, encodeAsBase64, decodeBase64, encodeAsBase64Url, + decodeBase64Url, encodeAsHex, decodeHex, encodeAsUri, decodeUri, _encodeBase64Polyfill, + _decodeBase64Polyfill +} from "../../../../src/helpers/encode"; import { arrContains } from "../../../../src/array/includes"; describe("encodeAsJson helper", () => { @@ -153,4 +157,409 @@ describe("encodeAsHtml", () => { it("Basic html encoding", () => { assert.equal(encodeAsHtml(""), "<script src="javascript:alert('Hello');"></script>"); }); +}); + +describe("encodeAsBase64 / decodeBase64", () => { + it("null / undefined / empty string", () => { + assert.equal(encodeAsBase64(null as any), ""); + assert.equal(encodeAsBase64(undefined as any), ""); + assert.equal(encodeAsBase64(""), ""); + assert.equal(decodeBase64(""), ""); + }); + + it("basic string encoding/decoding", () => { + assert.equal(encodeAsBase64("Hello"), "SGVsbG8="); + assert.equal(decodeBase64("SGVsbG8="), "Hello"); + + assert.equal(encodeAsBase64("Hello World"), "SGVsbG8gV29ybGQ="); + assert.equal(decodeBase64("SGVsbG8gV29ybGQ="), "Hello World"); + + assert.equal(encodeAsBase64("A"), "QQ=="); + assert.equal(decodeBase64("QQ=="), "A"); + + assert.equal(encodeAsBase64("AB"), "QUI="); + assert.equal(decodeBase64("QUI="), "AB"); + + assert.equal(encodeAsBase64("ABC"), "QUJD"); + assert.equal(decodeBase64("QUJD"), "ABC"); + }); + + it("round-trip encoding/decoding", () => { + let values = ["", "a", "ab", "abc", "test123", "The quick brown fox", "~!@#$%^&*()"]; + for (let i = 0; i < values.length; i++) { + let val = values[i]; + assert.equal(decodeBase64(encodeAsBase64(val)), val, "Failed for: " + val); + } + }); +}); + +describe("encodeAsBase64Url / decodeBase64Url", () => { + it("null / undefined / empty string", () => { + assert.equal(encodeAsBase64Url(null as any), ""); + assert.equal(encodeAsBase64Url(undefined as any), ""); + assert.equal(encodeAsBase64Url(""), ""); + assert.equal(decodeBase64Url(""), ""); + }); + + it("URL safe encoding/decoding", () => { + assert.equal(encodeAsBase64Url("Hello"), "SGVsbG8"); + assert.equal(decodeBase64Url("SGVsbG8"), "Hello"); + + assert.equal(encodeAsBase64Url("Hello World"), "SGVsbG8gV29ybGQ"); + assert.equal(decodeBase64Url("SGVsbG8gV29ybGQ"), "Hello World"); + }); + + it("special character handling for URL safety", () => { + // Test strings that produce + or / in standard base64 + let values = ["", "?>>", "test"]; + for (let i = 0; i < values.length; i++) { + let val = values[i]; + let encoded = encodeAsBase64Url(val); + // Should not contain +, /, or = + assert.ok(!encoded.includes("+"), "Should not contain + in: " + encoded); + assert.ok(!encoded.includes("/"), "Should not contain / in: " + encoded); + assert.ok(!encoded.includes("="), "Should not contain = in: " + encoded); + assert.equal(decodeBase64Url(encoded), val, "Failed round-trip for: " + val); + } + }); +}); + +describe("encodeAsHex / decodeHex", () => { + it("null / undefined / empty string", () => { + assert.equal(encodeAsHex(null as any), ""); + assert.equal(encodeAsHex(undefined as any), ""); + assert.equal(encodeAsHex(""), ""); + assert.equal(decodeHex(""), ""); + }); + + it("basic hex encoding/decoding", () => { + assert.equal(encodeAsHex("A"), "41"); + assert.equal(decodeHex("41"), "A"); + + assert.equal(encodeAsHex("Hello"), "48656c6c6f"); + assert.equal(decodeHex("48656c6c6f"), "Hello"); + + assert.equal(encodeAsHex("test"), "74657374"); + assert.equal(decodeHex("74657374"), "test"); + + assert.equal(encodeAsHex("0"), "30"); + assert.equal(decodeHex("30"), "0"); + }); + + it("round-trip hex encoding/decoding", () => { + let values = ["", "a", "Hello", "Test123", "The quick brown fox"]; + for (let i = 0; i < values.length; i++) { + let val = values[i]; + assert.equal(decodeHex(encodeAsHex(val)), val, "Failed for: " + val); + } + }); + + it("special characters in hex", () => { + assert.equal(encodeAsHex("!@#"), "214023"); + }); +}); + +describe("encodeAsUri / decodeUri", () => { + it("null / undefined / empty string", () => { + assert.equal(encodeAsUri(null as any), ""); + assert.equal(encodeAsUri(undefined as any), ""); + assert.equal(encodeAsUri(""), ""); + assert.equal(decodeUri(""), ""); + }); + + it("basic URI encoding/decoding", () => { + assert.equal(encodeAsUri("Hello"), "Hello"); + assert.equal(decodeUri("Hello"), "Hello"); + + assert.equal(encodeAsUri("Hello World"), "Hello%20World"); + assert.equal(decodeUri("Hello%20World"), "Hello World"); + + assert.equal(encodeAsUri("a+b=c"), "a%2Bb%3Dc"); + assert.equal(decodeUri("a%2Bb%3Dc"), "a+b=c"); + }); + + it("special URI characters", () => { + assert.ok(encodeAsUri("?") !== "?"); + assert.equal(decodeUri(encodeAsUri("?")), "?"); + + assert.ok(encodeAsUri("&") !== "&"); + assert.equal(decodeUri(encodeAsUri("&")), "&"); + + assert.ok(encodeAsUri("#") !== "#"); + assert.equal(decodeUri(encodeAsUri("#")), "#"); + }); + + it("round-trip URI encoding/decoding", () => { + let values = ["", "test", "hello world", "a=b&c=d", "test?query=1"]; + for (let i = 0; i < values.length; i++) { + let val = values[i]; + assert.equal(decodeUri(encodeAsUri(val)), val, "Failed for: " + val); + } + }); +}); + +describe("Base64 Polyfill Coverage - Iteration & Padding", () => { + it("various input lengths to exercise polyfill iteration", () => { + // Test different lengths to ensure all code paths are hit + for (let len = 1; len <= 30; len++) { + let str = ""; + for (let i = 0; i < len; i++) { + str += String.fromCharCode((i % 26) + 65); // A-Z cycle + } + let encoded = encodeAsBase64(str); + let decoded = decodeBase64(encoded); + assert.equal(decoded, str, "Round-trip failed for length: " + len); + } + }); + + it("padding scenarios 1 byte (1 mod 3)", () => { + // Lengths: 1, 4, 7, 10... should have 2 padding chars + assert.equal(decodeBase64(encodeAsBase64("A")), "A"); + assert.equal(decodeBase64(encodeAsBase64("ABCD")), "ABCD"); + assert.equal(decodeBase64(encodeAsBase64("ABCDEFG")), "ABCDEFG"); + assert.equal(decodeBase64(encodeAsBase64("ABCDEFGHIJ")), "ABCDEFGHIJ"); + }); + + it("padding scenarios 2 bytes (2 mod 3)", () => { + // Lengths: 2, 5, 8, 11... should have 1 padding char + assert.equal(decodeBase64(encodeAsBase64("AB")), "AB"); + assert.equal(decodeBase64(encodeAsBase64("ABCDE")), "ABCDE"); + assert.equal(decodeBase64(encodeAsBase64("ABCDEFGH")), "ABCDEFGH"); + assert.equal(decodeBase64(encodeAsBase64("ABCDEFGHIJK")), "ABCDEFGHIJK"); + }); + + it("padding scenarios 0 bytes (0 mod 3)", () => { + // Lengths: 3, 6, 9, 12... should have no padding + assert.equal(decodeBase64(encodeAsBase64("ABC")), "ABC"); + assert.equal(decodeBase64(encodeAsBase64("ABCDEF")), "ABCDEF"); + assert.equal(decodeBase64(encodeAsBase64("ABCDEFGHI")), "ABCDEFGHI"); + assert.equal(decodeBase64(encodeAsBase64("ABCDEFGHIJKL")), "ABCDEFGHIJKL"); + }); + + it("all ASCII characters", () => { + // Test ASCII range to ensure polyfill handles all byte values + let ascii = ""; + for (let i = 32; i < 127; i++) { + ascii += String.fromCharCode(i); + } + let encoded = encodeAsBase64(ascii); + let decoded = decodeBase64(encoded); + assert.equal(decoded, ascii, "Failed for ASCII 32-126"); + }); + + it("special byte values across full range", () => { + // Test with various byte values (0-255 range) + let binaryStr = ""; + for (let i = 0; i < 256; i++) { + binaryStr += String.fromCharCode(i); + } + let encoded = encodeAsBase64(binaryStr); + let decoded = decodeBase64(encoded); + assert.equal(decoded, binaryStr, "Failed for full byte range 0-255"); + }); + + it("large input to validate iteration logic", () => { + // Test with larger inputs - 3000 bytes + let large = ""; + for (let i = 0; i < 3000; i++) { + large += String.fromCharCode((i % 256)); + } + let encoded = encodeAsBase64(large); + let decoded = decodeBase64(encoded); + assert.equal(decoded, large, "Failed for large 3000-byte input"); + }); + + it("repeated characters at different lengths", () => { + // Ensures polyfill iteration works with repetitive data + for (let len = 1; len <= 50; len++) { + let str = strRepeat("X", len); + assert.equal(decodeBase64(encodeAsBase64(str)), str, "Failed for " + len + " X's"); + } + }); + + it("polyfill cache initialization persistence", () => { + // Verify cache works correctly across multiple calls + let values = ["hello", "world", "test123", "base64encoding"]; + for (let i = 0; i < values.length; i++) { + let val = values[i]; + let encoded = encodeAsBase64(val); + let decoded = decodeBase64(encoded); + assert.equal(decoded, val, "Failed for: " + val); + } + }); + + it("inputs generating base64 special chars", () => { + // Ensure proper handling of +, /, = in output + let testCases = [ + "Q@@@@" + ]; + for (let i = 0; i < testCases.length; i++) { + let val = testCases[i]; + let encoded = encodeAsBase64(val); + let decoded = decodeBase64(encoded); + assert.equal(decoded, val, "Failed for: " + val); + } + }); +}); + +// Helper function +function strRepeat(str: string, count: number): string { + let result = ""; + for (let i = 0; i < count; i++) { + result += str; + } + return result; +} + +describe("Base64 Polyfill Verification Against Native Implementation", () => { + // Check if native implementations are available + let hasNativeBase64 = typeof btoa !== "undefined" && typeof atob !== "undefined"; + + if (hasNativeBase64) { + it("_encodeBase64Polyfill matches native btoa encoding", () => { + let testStrings = [ + "", + "A", + "AB", + "ABC", + "Hello", + "Hello World", + "The quick brown fox jumps over the lazy dog", + "test123!@#$%^&*()", + "0", + "01", + "012", + "0123456789" + ]; + + for (let i = 0; i < testStrings.length; i++) { + let str = testStrings[i]; + let polyfillResult = _encodeBase64Polyfill(str); + let nativeResult = btoa(str); + assert.equal(polyfillResult, nativeResult, "Polyfill encode mismatch for: " + str); + } + }); + + it("_decodeBase64Polyfill matches native atob decoding", () => { + let testStrings = [ + "", + "QQ==", + "QUI=", + "QUJD", + "SGVsbG8=", + "SGVsbG8gV29ybGQ=", + "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==", + "dGVzdDEyMyFAIyQlXiYqKCk=" + ]; + + for (let i = 0; i < testStrings.length; i++) { + let str = testStrings[i]; + let polyfillResult = _decodeBase64Polyfill(str); + let nativeResult = atob(str); + assert.equal(polyfillResult, nativeResult, "Polyfill decode mismatch for: " + str); + } + }); + + it("Polyfill encode/decode round-trip matches native", () => { + let testStrings = [ + "", + "a", + "ab", + "abc", + "test", + "Hello World", + "The quick brown fox", + "!@#$%^&*()", + "0", + "01", + "012" + ]; + + for (let i = 0; i < testStrings.length; i++) { + let original = testStrings[i]; + + // Test polyfill consistency + let polyfillEncoded = _encodeBase64Polyfill(original); + let polyfillDecoded = _decodeBase64Polyfill(polyfillEncoded); + assert.equal(polyfillDecoded, original, "Polyfill round-trip failed for: " + original); + + // Test native consistency + let nativeEncoded = btoa(original); + let nativeDecoded = atob(nativeEncoded); + assert.equal(nativeDecoded, original, "Native round-trip failed for: " + original); + + // Test polyfill matches native + assert.equal(polyfillEncoded, nativeEncoded, "Polyfill/native encode mismatch for: " + original); + assert.equal(polyfillDecoded, nativeDecoded, "Polyfill/native decode mismatch for: " + original); + } + }); + + it("Polyfill handles all byte values 0-255", () => { + // Create string with all possible byte values + let allBytes = ""; + for (let i = 0; i < 256; i++) { + allBytes += String.fromCharCode(i); + } + + let polyfillEncoded = _encodeBase64Polyfill(allBytes); + let nativeEncoded = btoa(allBytes); + assert.equal(polyfillEncoded, nativeEncoded, "Polyfill/native mismatch for byte range 0-255"); + + let polyfillDecoded = _decodeBase64Polyfill(nativeEncoded); + assert.equal(polyfillDecoded, allBytes, "Polyfill cannot decode native output for byte range"); + }); + + it("Polyfill handles various input lengths to test padding", () => { + // Test lengths 0-30 to ensure padding is correct + for (let len = 0; len <= 30; len++) { + let str = strRepeat("X", len); + let polyfillEncoded = _encodeBase64Polyfill(str); + let nativeEncoded = btoa(str); + assert.equal(polyfillEncoded, nativeEncoded, "Padding mismatch for length: " + len); + } + }); + + it("Polyfill handles large inputs consistently with native", () => { + // Test with a 10KB input + let large = ""; + for (let i = 0; i < 10000; i++) { + large += String.fromCharCode(i % 256); + } + + let polyfillEncoded = _encodeBase64Polyfill(large); + let nativeEncoded = btoa(large); + assert.equal(polyfillEncoded, nativeEncoded, "Large input encoding mismatch"); + + let polyfillDecoded = _decodeBase64Polyfill(polyfillEncoded); + assert.equal(polyfillDecoded, large, "Large input round-trip failed"); + }); + + it("Polyfill decode works with both uppercase and standard characters", () => { + // Generate some base64 and verify polyfill can decode it + let testStrings = ["test", "hello world", "The quick brown fox"]; + + for (let i = 0; i < testStrings.length; i++) { + let original = testStrings[i]; + let nativeEncoded = btoa(original); + let polyfillDecoded = _decodeBase64Polyfill(nativeEncoded); + assert.equal(polyfillDecoded, original, "Polyfill cannot decode native encoding for: " + original); + } + }); + + it("Polyfill encode produces output that native atob can decode", () => { + let testStrings = ["", "a", "ab", "abc", "Hello", "Test123!"]; + + for (let i = 0; i < testStrings.length; i++) { + let original = testStrings[i]; + let polyfillEncoded = _encodeBase64Polyfill(original); + let nativeDecoded = atob(polyfillEncoded); + assert.equal(nativeDecoded, original, "Native atob cannot decode polyfill output for: " + original); + } + }); + } else { + // Fallback if native implementations aren't available (very rare) + it("Native btoa/atob are available", () => { + assert.ok(hasNativeBase64, "Native btoa/atob should be available in this environment"); + }); + } }); \ No newline at end of file From 589111791533a1afe9db7d314b32f54dfda2510e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:53:48 -0800 Subject: [PATCH 2/2] test(encode): use existing strRepeat from string utilities instead of redefining it (#524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The encode test file had a locally defined `strRepeat` helper that duplicated the already-exported `strRepeat` from `src/string/repeat`. ## Changes - Removed local `strRepeat` function definition from `encode.test.ts` - Added import of `strRepeat` from `../../../../src/string/repeat` --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nev21 <82737406+nev21@users.noreply.github.com> --- lib/test/src/common/helpers/encode.test.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/test/src/common/helpers/encode.test.ts b/lib/test/src/common/helpers/encode.test.ts index 54a5fc42..4977e37c 100644 --- a/lib/test/src/common/helpers/encode.test.ts +++ b/lib/test/src/common/helpers/encode.test.ts @@ -13,6 +13,7 @@ import { _decodeBase64Polyfill } from "../../../../src/helpers/encode"; import { arrContains } from "../../../../src/array/includes"; +import { strRepeat } from "../../../../src/string/repeat"; describe("encodeAsJson helper", () => { it("null/undefined", () => { @@ -402,15 +403,6 @@ describe("Base64 Polyfill Coverage - Iteration & Padding", () => { }); }); -// Helper function -function strRepeat(str: string, count: number): string { - let result = ""; - for (let i = 0; i < count; i++) { - result += str; - } - return result; -} - describe("Base64 Polyfill Verification Against Native Implementation", () => { // Check if native implementations are available let hasNativeBase64 = typeof btoa !== "undefined" && typeof atob !== "undefined";