@hugoalh/base64
Version:
A module for Base64 encode and decode.
228 lines (227 loc) • 7.29 kB
JavaScript
if (typeof Uint8Array.fromBase64 === "undefined") {
//deno-lint-ignore hugoalh/no-import-dynamic -- Polyfill.
await import("es-arraybuffer-base64/Uint8Array.fromBase64/auto");
}
if (typeof Uint8Array.prototype.toBase64 === "undefined") {
//deno-lint-ignore hugoalh/no-import-dynamic -- Polyfill.
await import("es-arraybuffer-base64/Uint8Array.prototype.toBase64/auto");
}
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.fromBase64(itemFmt, {
alphabet: "base64",
lastChunkHandling: "loose"
});
}
/**
* 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 = itemFmt.toBase64({
alphabet: "base64",
omitPadding: false
});
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);
}
}