UNPKG

@hugoalh/base64

Version:

A module for Base64 encode and decode.

218 lines (217 loc) 6.91 kB
const specificationNoPadding = { padding: false }; const specificationPadding = { padding: true }; const specificationURL = { replace62: "-", replace63: "_" }; const specifications = { rfc1421: specificationPadding, rfc2045: specificationPadding, rfc2152: specificationNoPadding, rfc3501: { padding: false, replace63: "," }, "rfc4648-4": specificationPadding, "rfc4648-5": specificationURL, rfc9580: specificationPadding, standard: specificationPadding, url: specificationURL }; /** * Base64 decoder. */ export class Base64Decoder { get [Symbol.toStringTag]() { return "Base64Decoder"; } #variant; /** * Initialize. * @param {Base64DecodeOptions} [options={}] Base64 decode options. */ constructor(options = {}) { const { variant = "standard" } = options; if (typeof specifications[variant] === "undefined") { throw new RangeError(`\`${variant}\` is not a valid Base64 variant type! Only accept these values: ${Object.keys(specifications).sort().join(", ")}`); } this.#variant = variant; } /** * Variant of the Base64 decoder. * @returns {Base64Variant} */ get variant() { return this.#variant; } /** * Decode from Base64 to bytes. * @param {string | Uint8Array} item Item that need to decode. * @returns {Uint8Array} A decoded bytes. */ decodeToBytes(item) { const { replace62 = null, replace63 = null } = specifications[this.#variant]; let itemFmt = (typeof item === "string") ? item : new TextDecoder().decode(item); if (replace62 !== null) { itemFmt = itemFmt.replaceAll(replace62, "+"); } if (replace63 !== null) { itemFmt = itemFmt.replaceAll(replace63, "/"); } return Uint8Array.from(atob(itemFmt).split(""), (character) => { return character.codePointAt(0); }); } /** * Decode from Base64 to text. * @param {string | Uint8Array} item Item that need to decode. * @returns {string} A decoded text. */ decodeToText(item) { return new TextDecoder().decode(this.decodeToBytes(item)); } } /** * Base64 encoder. */ export class Base64Encoder { get [Symbol.toStringTag]() { return "Base64Encoder"; } #padding; #variant; /** * Initialize. * @param {Base64EncodeOptions} [options={}] Base64 encode options. */ constructor(options = {}) { const { padding = null, variant = "standard" } = options; const specification = specifications[variant]; if (typeof specification === "undefined") { throw new RangeError(`\`${variant}\` is not a valid Base64 variant type! Only accept these values: ${Object.keys(specifications).sort().join(", ")}`); } this.#padding = (padding === null) ? (specification.padding ?? true) : padding; this.#variant = variant; } /** * Whether padding in the Base64 encoder. * @returns {boolean} */ get padding() { return this.#padding; } /** * Variant of the Base64 encoder. * @returns {Base64Variant} */ get variant() { return this.#variant; } /** * Encode to Base64 bytes. * @param {string | Uint8Array} item Item that need to encode. * @returns {Uint8Array} A Base64 encoded bytes. */ encodeToBytes(item) { return new TextEncoder().encode(this.encodeToText(item)); } /** * Encode to Base64 text. * @param {string | Uint8Array} item Item that need to encode. * @returns {string} A Base64 encoded text. */ encodeToText(item) { const { replace62 = null, replace63 = null } = specifications[this.#variant]; const itemFmt = (typeof item === "string") ? new TextEncoder().encode(item) : item; let result = btoa(Array.from(itemFmt, (byte) => { return String.fromCodePoint(byte); }).join("")); if (replace62 !== null) { result = result.replaceAll("+", replace62); } if (replace63 !== null) { result = result.replaceAll("/", replace63); } return (this.#padding ? result : result.replaceAll("=", "")); } } /** * Transform from Base64 encoded bytes stream to bytes stream. */ export class Base64DecoderStream extends TransformStream { get [Symbol.toStringTag]() { return "Base64DecoderStream"; } #base64Decoder; #bin = []; /** * Initialize. * @param {Base64DecodeOptions} [options={}] Base64 decode options. */ constructor(options) { super({ transform: (chunk, controller) => { this.#bin.push(...Array.from(chunk)); if (this.#bin.length >= 4) { try { controller.enqueue(this.#base64Decoder.decodeToBytes(Uint8Array.from(this.#bin.splice(0, Math.floor(this.#bin.length / 4) * 4)))); } catch (error) { controller.error(error); } } }, flush: (controller) => { try { controller.enqueue(this.#base64Decoder.decodeToBytes(Uint8Array.from(this.#bin.splice(0, this.#bin.length)))); } catch (error) { controller.error(error); } } }); this.#base64Decoder = new Base64Decoder(options); } } /** * Transform from bytes stream to Base64 encoded bytes stream. */ export class Base64EncoderStream extends TransformStream { get [Symbol.toStringTag]() { return "Base64EncoderStream"; } #base64Encoder; #bin = []; /** * Initialize. * @param {Base64EncodeOptions} [options={}] Base64 encode options. */ constructor(options = {}) { super({ transform: (chunk, controller) => { this.#bin.push(...Array.from(chunk)); if (this.#bin.length >= 3) { try { controller.enqueue(this.#base64Encoder.encodeToBytes(Uint8Array.from(this.#bin.splice(0, Math.floor(this.#bin.length / 3) * 3)))); } catch (error) { controller.error(error); } } }, flush: (controller) => { try { controller.enqueue(this.#base64Encoder.encodeToBytes(Uint8Array.from(this.#bin.splice(0, this.#bin.length)))); } catch (error) { controller.error(error); } } }); this.#base64Encoder = new Base64Encoder(options); } }