Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"Show Base64 offsets",
"To Base92",
"From Base92",
"To Base91",
"From Base91",
"To Base85",
"From Base85",
"To Base",
Expand Down Expand Up @@ -128,6 +130,8 @@
"From Morse Code",
"Bacon Cipher Encode",
"Bacon Cipher Decode",
"Dancing Men Encode",
"Dancing Men Decode",
"Bifid Cipher Encode",
"Bifid Cipher Decode",
"Caesar Box Cipher",
Expand Down
125 changes: 125 additions & 0 deletions src/core/lib/Base91.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* 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
* @modified-by Izai Alejandro Zalles Merino <[email protected]> (ialejandrozalles)
* @modified-date 2025-10-01
* © 2025 Izai Alejandro Zalles Merino

*/

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
* @modified-by Izai Alejandro Zalles Merino <[email protected]> (ialejandrozalles)

*/
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
* @modified-by Izai Alejandro Zalles Merino <[email protected]> (ialejandrozalles)

*/
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);
}
76 changes: 76 additions & 0 deletions src/core/operations/DancingMenDecode.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @author Agent Mode
* @license Apache-2.0
*/

import Operation from "../Operation.mjs";

/**
* Dancing Men Decode operation
*
* Decodes textual Dancing Men tokens like char(97)..char(122) back to letters a-z.
* If a token is suffixed with '!' (flag), it can be interpreted as a word separator.
*/
class DancingMenDecode extends Operation {

/**
* DancingMenDecode constructor
*/
constructor() {
super();

this.name = "Dancing Men Decode";
this.module = "Ciphers";
this.description = "Decode Dancing Men token format (char(97)..char(122), optional ! for flags) back to text.";
this.infoURL = "https://www.dcode.fr/dancing-men-cipher";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Flags indicate spaces",
type: "boolean",
value: false
}
];
// Magic detection: sequence of 3+ char(ddd) tokens optionally with trailing '!'
this.checks = [
{
pattern: "^(?:\\s*char\\(\\d{2,3}\\)!?\\s*){3,}$",
args: [false],
useful: true
}
];
}

/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [flagsAsSpaces] = args;
const tokenRe = /char\((\d{2,3})\)(!?)/g;
let out = "";
let lastIndex = 0;
let m;
while ((m = tokenRe.exec(input)) !== null) {
// Append any intermediary non-token text unchanged
if (m.index > lastIndex) {
out += input.slice(lastIndex, m.index);
}
const code = parseInt(m[1], 10);
let ch = "";
if (code >= 97 && code <= 122) ch = String.fromCharCode(code);
else if (code >= 65 && code <= 90) ch = String.fromCharCode(code).toLowerCase();
else ch = ""; // Unknown token range -> drop
out += ch;
if (flagsAsSpaces && m[2] === "!") out += " ";
lastIndex = tokenRe.lastIndex;
}
// Append any remainder
if (lastIndex < input.length) out += input.slice(lastIndex);
return out;
}
}

export default DancingMenDecode;
97 changes: 97 additions & 0 deletions src/core/operations/DancingMenEncode.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @author Agent Mode
* @license Apache-2.0
*/

import Operation from "../Operation.mjs";

/**
* Dancing Men Encode operation
*
* Encodes Latin letters a-z into textual Dancing Men tokens of the form char(97)..char(122).
* Optionally, spaces can be represented by a flag marker appended to the previous token ("!")
* to mimic the word-separator flag described in Conan Doyle's short story.
*/
class DancingMenEncode extends Operation {

/**
* DancingMenEncode constructor
*/
constructor() {
super();

this.name = "Dancing Men Encode";
this.module = "Ciphers";
this.description = "Encode plaintext to Dancing Men token format using tokens like char(97)..char(122). Optionally mark word boundaries with a flag (!).";
this.infoURL = "https://www.dcode.fr/dancing-men-cipher";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Use flags as word separators",
type: "boolean",
value: false
},
{
name: "Separator between tokens",
type: "option",
value: ["Space", "None"],
defaultIndex: 0
}
];
}

/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [useFlags, sepChoice] = args;
const sep = sepChoice === "None" ? "" : " ";

const out = [];
let prevIdx = -1;

for (let i = 0; i < input.length; i++) {
const ch = input[i];
const code = ch.toLowerCase().charCodeAt(0);
if (code >= 97 && code <= 122) {
out.push(`char(${code})`);
prevIdx = out.length - 1;
} else if (ch === " ") {
if (useFlags && prevIdx >= 0) {
// Append a flag marker to the previous token to denote word boundary
out[prevIdx] = out[prevIdx] + "!";
} else {
// Represent space explicitly in the stream
out.push(" ");
prevIdx = -1;
}
} else if (ch === "\n" || ch === "\r" || ch === "\t") {
out.push(ch);
prevIdx = -1;
} else {
// Pass-through other characters as-is
out.push(ch);
prevIdx = -1;
}
}

// Join but preserve already injected spaces/newlines
// We only join char(...) tokens using the chosen separator
// Build final by inserting sep between adjacent char(...) tokens (and their optional !)
const tokens = [];
for (let i = 0; i < out.length; i++) {
const cur = out[i];
tokens.push(cur);
const curIsToken = /^char\(\d{2,3}\)!?$/.test(cur);
const next = out[i + 1];
const nextIsToken = typeof next === "string" && /^char\(\d{2,3}\)!?$/.test(next);
if (sep && curIsToken && nextIsToken) tokens.push(sep);
}
return tokens.join("");
}
}

export default DancingMenEncode;
51 changes: 51 additions & 0 deletions src/core/operations/FromBase91.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @author Izai Alejandro Zalles Merino <[email protected]> (ialejandrozalles)
* @copyright © 2025 Izai Alejandro Zalles Merino
* @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";
/* eslint-disable no-useless-escape */
this.checks = [
{
pattern:
"^" +
"[\\s0-9A-Za-z!#$%&()*+,./:;<=>?@\\\[\\\]\\^_`{|}~\"]*" +
"[0-9A-Za-z!#$%&()*+,./:;<=>?@\\\[\\\]\\^_`{|}~\"]{15}" +
"[\\s0-9A-Za-z!#$%&()*+,./:;<=>?@\\\[\\\]\\^_`{|}~\"]*" +
"$",
args: []
}
/* eslint-enable no-useless-escape */
];
}

/**
* @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;
16 changes: 14 additions & 2 deletions src/core/operations/FromMorseCode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,20 @@ class FromMorseCode extends Operation {
pattern: "(?:^[-. \\n]{5,}$|^[_. \\n]{5,}$|^(?:dash|dot| |\\n){5,}$)",
flags: "i",
args: ["Space", "Line feed"]
}
];
},
{
pattern: "^(?=.*/)[-./ \n]{5,}$",
args: ["Space", "Forward slash"]
},
{
pattern: "^(?=.*/)[_. /\n]{5,}$",
args: ["Space", "Forward slash"]
},
{
pattern: "^(?=.*/)(?:dash|dot| |/|\n){5,}$",
flags: "i",
args: ["Space", "Forward slash"]
}];
}

/**
Expand Down
Loading
Loading