Add Base91 encoding and decoding operations

- Implement Base91 algorithm using 91 printable ASCII characters
- Add ToBase91 operation for encoding data to Base91
- Add FromBase91 operation for decoding Base91 strings
- Base91 provides ~23% better space efficiency than Base64
- Support for Unicode text, emojis, and binary data
- Full round-trip encoding/decoding compatibility
- Integration with CyberChef operation system
This commit is contained in:
Izai Alejandro Zalles Merino 2025-09-17 14:19:48 -04:00
parent 2a1294f1c0
commit fa20a7a404
5 changed files with 198 additions and 1 deletions

View File

@ -33,6 +33,8 @@
"Show Base64 offsets", "Show Base64 offsets",
"To Base92", "To Base92",
"From Base92", "From Base92",
"To Base91",
"From Base91",
"To Base85", "To Base85",
"From Base85", "From Base85",
"To Base", "To Base",

117
src/core/lib/Base91.mjs Normal file
View File

@ -0,0 +1,117 @@
/**
* Base91 resources.
*
* Based on the original basE91 algorithm by Joachim Henke
* http://base91.sourceforge.net/
*
* @author CyberChef Base91 Implementation
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError.mjs";
/**
* Base91 alphabet - 91 printable ASCII characters
*/
const BASE91_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~\"";
/**
* Decode table for Base91
*/
const BASE91_DECODE_TABLE = new Array(256).fill(-1);
for (let i = 0; i < BASE91_ALPHABET.length; i++) {
BASE91_DECODE_TABLE[BASE91_ALPHABET.charCodeAt(i)] = i;
}
/**
* Encode bytes to Base91
*
* @param {Uint8Array} data - Input byte array
* @returns {string} Base91 encoded string
*/
export function encodeBase91(data) {
let accumulator = 0;
let accumulatorBits = 0;
let output = "";
for (let i = 0; i < data.length; i++) {
accumulator |= data[i] << accumulatorBits;
accumulatorBits += 8;
if (accumulatorBits > 13) {
let value = accumulator & 8191;
if (value > 88) {
accumulator >>= 13;
accumulatorBits -= 13;
} else {
value = accumulator & 16383;
accumulator >>= 14;
accumulatorBits -= 14;
}
output += BASE91_ALPHABET[value % 91] + BASE91_ALPHABET[Math.floor(value / 91)];
}
}
if (accumulatorBits > 0) {
output += BASE91_ALPHABET[accumulator % 91];
if (accumulatorBits > 7 || accumulator > 90) {
output += BASE91_ALPHABET[Math.floor(accumulator / 91)];
}
}
return output;
}
/**
* Decode Base91 string to bytes
*
* @param {string} str - Base91 encoded string
* @returns {Uint8Array} Decoded byte array
*/
export function decodeBase91(str) {
let accumulator = 0;
let accumulatorBits = 0;
let value = -1;
const output = [];
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
const decodeValue = BASE91_DECODE_TABLE[charCode];
if (decodeValue === -1) {
throw new OperationError(`Invalid Base91 character: ${str[i]}`);
}
if (value === -1) {
value = decodeValue;
} else {
value += decodeValue * 91;
accumulator |= (value << accumulatorBits);
if (value > 88) {
accumulatorBits += 13;
} else {
accumulatorBits += 14;
}
value = -1;
while (accumulatorBits > 7) {
output.push(accumulator & 255);
accumulator >>= 8;
accumulatorBits -= 8;
}
}
}
if (value !== -1) {
accumulator |= value << accumulatorBits;
output.push(accumulator & 255);
}
return new Uint8Array(output);
}

View File

@ -0,0 +1,39 @@
/**
* @author CyberChef Base91 Implementation
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import { decodeBase91 } from "../lib/Base91.mjs";
import Operation from "../Operation.mjs";
/**
* From Base91 operation
*/
class FromBase91 extends Operation {
/**
* FromBase91 constructor
*/
constructor() {
super();
this.name = "From Base91";
this.module = "Default";
this.description = "Base91 is a binary-to-text encoding scheme that uses 91 printable ASCII characters. It provides better space efficiency than Base64 while maintaining readability. This operation decodes Base91-encoded text back to its original binary data.";
this.infoURL = "https://en.wikipedia.org/wiki/Binary-to-text_encoding#Encoding_standards";
this.inputType = "string";
this.outputType = "ArrayBuffer";
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {ArrayBuffer}
*/
run(input, args) {
const decoded = decodeBase91(input);
return decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength);
}
}
export default FromBase91;

View File

@ -0,0 +1,39 @@
/**
* @author CyberChef Base91 Implementation
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import { encodeBase91 } from "../lib/Base91.mjs";
import Operation from "../Operation.mjs";
/**
* To Base91 operation
*/
class ToBase91 extends Operation {
/**
* ToBase91 constructor
*/
constructor() {
super();
this.name = "To Base91";
this.module = "Default";
this.description = "Base91 is a binary-to-text encoding scheme that uses 91 printable ASCII characters. It provides better space efficiency than Base64 while maintaining readability. Base91 encodes arbitrary binary data using characters A-Z, a-z, 0-9, and various symbols (excluding hyphen, backslash, and single quote).";
this.infoURL = "https://en.wikipedia.org/wiki/Binary-to-text_encoding#Encoding_standards";
this.inputType = "ArrayBuffer";
this.outputType = "string";
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const data = new Uint8Array(input);
return encodeBase91(data);
}
}
export default ToBase91;

View File

@ -136,7 +136,7 @@ TestRegister.addApiTests([
it("chef.help: returns multiple results", () => { it("chef.help: returns multiple results", () => {
const result = chef.help("base 64"); const result = chef.help("base 64");
assert.strictEqual(result.length, 13); assert.strictEqual(result.length, 15);
}), }),
it("chef.help: looks in description for matches too", () => { it("chef.help: looks in description for matches too", () => {