From c9298191890e46b77479a079523728f261e59653 Mon Sep 17 00:00:00 2001 From: Harel Katz <34490550+HarelKatz@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:44:20 +0300 Subject: [PATCH 1/2] feat: add Escape Smart Characters operation (#2391) --- src/core/config/Categories.json | 1 + src/core/operations/EscapeSmartCharacters.mjs | 129 +++++++++++++++++ tests/operations/index.mjs | 1 + .../tests/EscapeSmartCharacters.mjs | 132 ++++++++++++++++++ 4 files changed, 263 insertions(+) create mode 100644 src/core/operations/EscapeSmartCharacters.mjs create mode 100644 tests/operations/tests/EscapeSmartCharacters.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index dd6918dc..d3e7648a 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -49,6 +49,7 @@ "Escape Unicode Characters", "Unescape Unicode Characters", "Normalise Unicode", + "Escape Smart Characters", "To Quoted Printable", "From Quoted Printable", "To Punycode", diff --git a/src/core/operations/EscapeSmartCharacters.mjs b/src/core/operations/EscapeSmartCharacters.mjs new file mode 100644 index 00000000..39444e94 --- /dev/null +++ b/src/core/operations/EscapeSmartCharacters.mjs @@ -0,0 +1,129 @@ +/** + * @author HarelKatz [github.com/HarelKatz] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Escape Smart Characters operation + */ +class EscapeSmartCharacters extends Operation { + + /** + * EscapeSmartCharacters constructor + */ + constructor() { + super(); + + this.name = "Escape Smart Characters"; + this.module = "Default"; + this.description = "Converts smart (typographic) Unicode characters — e.g. smart quotes, em/en dashes, ellipses, ©, ®, ™, arrows — into their plain ASCII equivalents.

Characters with no ASCII mapping (e.g. ) are handled according to the 'Unmappable characters' option.

e.g. “Hello” — world… becomes \"Hello\" -- world..."; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Unmappable characters", + type: "option", + value: ["Include", "Remove", "Replace with '.'"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [unmappable] = args; + let result = ""; + for (const ch of input) { + if (ch.codePointAt(0) < 128) { + result += ch; + } else if (Object.prototype.hasOwnProperty.call(SMART_MAP, ch)) { + result += SMART_MAP[ch]; + } else { + switch (unmappable) { + case "Remove": + break; + case "Replace with '.'": + result += "."; + break; + case "Include": + default: + result += ch; + break; + } + } + } + return result; + } + +} + +const SMART_MAP = { + // Smart double quotes + "“": "\"", // “ left double quotation mark + "”": "\"", // ” right double quotation mark + "„": "\"", // „ double low-9 quotation mark + "‟": "\"", // ‟ double high-reversed-9 quotation mark + "″": "\"", // ″ double prime + + // Smart single quotes / apostrophes + "‘": "'", // ‘ left single quotation mark + "’": "'", // ’ right single quotation mark / apostrophe + "‚": "'", // ‚ single low-9 quotation mark + "‛": "'", // ‛ single high-reversed-9 quotation mark + "′": "'", // ′ prime + + // Dashes & hyphens + "‐": "-", // ‐ hyphen + "‑": "-", // ‑ non-breaking hyphen + "‒": "-", // ‒ figure dash + "–": "-", // – en dash + "—": "--", // — em dash + "―": "--", // ― horizontal bar + + // Ellipsis + "…": "...", // … + + // Trademark / copyright symbols + "©": "(c)", // © + "®": "(r)", // ® + "™": "(tm)", // ™ + + // Arrows + "←": "<--", // ← + "→": "-->", // → + "↑": "^", // ↑ + "↓": "v", // ↓ + "↔": "<->", // ↔ + "⇐": "<==", // ⇐ + "⇒": "==>", // ⇒ + "⇔": "<=>", // ⇔ + + // Guillemets + "«": "<<", // « + "»": ">>", // » + "‹": "<", // ‹ + "›": ">", // › + + // Math & misc symbols + "×": "x", // × + "÷": "/", // ÷ + "±": "+/-", // ± + "•": "*", // • + "·": ".", // · + + // Non-ASCII spaces + "\u00A0": " ", // NBSP + "\u2002": " ", // en space + "\u2003": " ", // em space + "\u2009": " ", // thin space + "\u200A": " " // hair space +}; + +export default EscapeSmartCharacters; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index c44270fd..44792560 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -69,6 +69,7 @@ import "./tests/DropNthBytes.mjs"; import "./tests/ECDSA.mjs"; import "./tests/ELFInfo.mjs"; import "./tests/Enigma.mjs"; +import "./tests/EscapeSmartCharacters.mjs"; import "./tests/ExtractAudioMetadata.mjs"; import "./tests/ExtractEmailAddresses.mjs"; import "./tests/ExtractHashes.mjs"; diff --git a/tests/operations/tests/EscapeSmartCharacters.mjs b/tests/operations/tests/EscapeSmartCharacters.mjs new file mode 100644 index 00000000..428c3ad8 --- /dev/null +++ b/tests/operations/tests/EscapeSmartCharacters.mjs @@ -0,0 +1,132 @@ +/** + * Escape Smart Characters tests + * + * @author HarelKatz [github.com/HarelKatz] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + "name": "Escape Smart Characters: smart quotes and apostrophes", + "input": "“Hello,” she said, ‘yes.’", + "expectedOutput": "\"Hello,\" she said, 'yes.'", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + }, + { + "name": "Escape Smart Characters: em dash, en dash and ellipsis", + "input": "page 1–3 — wait…", + "expectedOutput": "page 1-3 -- wait...", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + }, + { + "name": "Escape Smart Characters: trademark symbols", + "input": "Foo© Bar® Baz™", + "expectedOutput": "Foo(c) Bar(r) Baz(tm)", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + }, + { + "name": "Escape Smart Characters: arrows and guillemets", + "input": "← → ↔ ⇒ « »", + "expectedOutput": "<-- --> <-> ==> << >>", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + }, + { + "name": "Escape Smart Characters: math and misc", + "input": "3 × 4 ÷ 2 = 6, ±0.5 • item", + "expectedOutput": "3 x 4 / 2 = 6, +/-0.5 * item", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + }, + { + "name": "Escape Smart Characters: NBSP becomes regular space", + "input": "a b c", + "expectedOutput": "a b c", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + }, + { + "name": "Escape Smart Characters: unmappable Include preserves char", + "input": "warning: ☣ hazard", + "expectedOutput": "warning: ☣ hazard", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + }, + { + "name": "Escape Smart Characters: unmappable Remove drops char", + "input": "warning: ☣ hazard", + "expectedOutput": "warning: hazard", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Remove"] + } + ] + }, + { + "name": "Escape Smart Characters: unmappable Replace substitutes dot", + "input": "warning: ☣ hazard", + "expectedOutput": "warning: . hazard", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Replace with '.'"] + } + ] + }, + { + "name": "Escape Smart Characters: pure ASCII passes through", + "input": "hello world! 123", + "expectedOutput": "hello world! 123", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + }, + { + "name": "Escape Smart Characters: empty input", + "input": "", + "expectedOutput": "", + "recipeConfig": [ + { + "op": "Escape Smart Characters", + "args": ["Include"] + } + ] + } +]); From d735496641493725fbdf64f1c0064296c7f3fdac Mon Sep 17 00:00:00 2001 From: andreas Date: Fri, 5 Jun 2026 21:19:24 +0200 Subject: [PATCH 2/2] Update README to reflect AES Decrypt changes (#2502) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6da7d3ec..50b1a44d 100755 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ You can use as many operations as you like in simple or complex ways. Some examp - [Carry out different operations on data of different types][8] - [Use parts of the input as arguments to operations][9] - [Perform AES decryption, extracting the IV from the beginning of the cipher stream][10] + - [A simpler way to perform the same AES Decryption][13] - [Automagically detect several layers of nested encoding][12] @@ -147,6 +148,7 @@ CyberChef is released under the [Apache 2.0 Licence](https://www.apache.org/lice [7]: https://gchq.github.io/CyberChef/#recipe=Fork('%5C%5Cn','%5C%5Cn',false)From_UNIX_Timestamp('Seconds%20(s)')&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA [8]: https://gchq.github.io/CyberChef/#recipe=Fork('%5C%5Cn','%5C%5Cn',false)Conditional_Jump('1',false,'base64',10)To_Hex('Space')Return()Label('base64')To_Base64('A-Za-z0-9%2B/%3D')&input=U29tZSBkYXRhIHdpdGggYSAxIGluIGl0ClNvbWUgZGF0YSB3aXRoIGEgMiBpbiBpdA [9]: https://gchq.github.io/CyberChef/#recipe=Register('key%3D(%5B%5C%5Cda-f%5D*)',true,false)Find_/_Replace(%7B'option':'Regex','string':'.*data%3D(.*)'%7D,'$1',true,false,true)RC4(%7B'option':'Hex','string':'$R0'%7D,'Hex','Latin1')&input=aHR0cDovL21hbHdhcmV6LmJpei9iZWFjb24ucGhwP2tleT0wZTkzMmE1YyZkYXRhPThkYjdkNWViZTM4NjYzYTU0ZWNiYjMzNGUzZGIxMQ - [10]: https://gchq.github.io/CyberChef/#recipe=Register('(.%7B32%7D)',true,false)Drop_bytes(0,32,false)AES_Decrypt(%7B'option':'Hex','string':'1748e7179bd56570d51fa4ba287cc3e5'%7D,%7B'option':'Hex','string':'$R0'%7D,'CTR','Hex','Raw',%7B'option':'Hex','string':''%7D)&input=NTFlMjAxZDQ2MzY5OGVmNWY3MTdmNzFmNWI0NzEyYWYyMGJlNjc0YjNiZmY1M2QzODU0NjM5NmVlNjFkYWFjNDkwOGUzMTljYTNmY2Y3MDg5YmZiNmIzOGVhOTllNzgxZDI2ZTU3N2JhOWRkNmYzMTFhMzk0MjBiODk3OGU5MzAxNGIwNDJkNDQ3MjZjYWVkZjU0MzZlYWY2NTI0MjljMGRmOTRiNTIxNjc2YzdjMmNlODEyMDk3YzI3NzI3M2M3YzcyY2Q4OWFlYzhkOWZiNGEyNzU4NmNjZjZhYTBhZWUyMjRjMzRiYTNiZmRmN2FlYjFkZGQ0Nzc2MjJiOTFlNzJjOWU3MDlhYjYwZjhkYWY3MzFlYzBjYzg1Y2UwZjc0NmZmMTU1NGE1YTNlYzI5MWNhNDBmOWU2MjlhODcyNTkyZDk4OGZkZDgzNDUzNGFiYTc5YzFhZDE2NzY3NjlhN2MwMTBiZjA0NzM5ZWNkYjY1ZDk1MzAyMzcxZDYyOWQ5ZTM3ZTdiNGEzNjFkYTQ2OGYxZWQ1MzU4OTIyZDJlYTc1MmRkMTFjMzY2ZjMwMTdiMTRhYTAxMWQyYWYwM2M0NGY5NTU3OTA5OGExNWUzY2Y5YjQ0ODZmOGZmZTljMjM5ZjM0ZGU3MTUxZjZjYTY1MDBmZTRiODUwYzNmMWMwMmU4MDFjYWYzYTI0NDY0NjE0ZTQyODAxNjE1YjhmZmFhMDdhYzgyNTE0OTNmZmRhN2RlNWRkZjMzNjg4ODBjMmI5NWIwMzBmNDFmOGYxNTA2NmFkZDA3MWE2NmNmNjBlNWY0NmYzYTIzMGQzOTdiNjUyOTYzYTIxYTUzZg + [10]: https://gchq.github.io/CyberChef/#recipe=Register('(.%7B32%7D)',true,false,false)Drop_bytes(0,32,false)AES_Decrypt(%7B'option':'Hex','string':'1748e7179bd56570d51fa4ba287cc3e5'%7D,%7B'option':'Hex','string':'$R0'%7D,16,'CTR','Hex','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D,'Off')&input=NTFlMjAxZDQ2MzY5OGVmNWY3MTdmNzFmNWI0NzEyYWYyMGJlNjc0YjNiZmY1M2QzODU0NjM5NmVlNjFkYWFjNDkwOGUzMTljYTNmY2Y3MDg5YmZiNmIzOGVhOTllNzgxZDI2ZTU3N2JhOWRkNmYzMTFhMzk0MjBiODk3OGU5MzAxNGIwNDJkNDQ3MjZjYWVkZjU0MzZlYWY2NTI0MjljMGRmOTRiNTIxNjc2YzdjMmNlODEyMDk3YzI3NzI3M2M3YzcyY2Q4OWFlYzhkOWZiNGEyNzU4NmNjZjZhYTBhZWUyMjRjMzRiYTNiZmRmN2FlYjFkZGQ0Nzc2MjJiOTFlNzJjOWU3MDlhYjYwZjhkYWY3MzFlYzBjYzg1Y2UwZjc0NmZmMTU1NGE1YTNlYzI5MWNhNDBmOWU2MjlhODcyNTkyZDk4OGZkZDgzNDUzNGFiYTc5YzFhZDE2NzY3NjlhN2MwMTBiZjA0NzM5ZWNkYjY1ZDk1MzAyMzcxZDYyOWQ5ZTM3ZTdiNGEzNjFkYTQ2OGYxZWQ1MzU4OTIyZDJlYTc1MmRkMTFjMzY2ZjMwMTdiMTRhYTAxMWQyYWYwM2M0NGY5NTU3OTA5OGExNWUzY2Y5YjQ0ODZmOGZmZTljMjM5ZjM0ZGU3MTUxZjZjYTY1MDBmZTRiODUwYzNmMWMwMmU4MDFjYWYzYTI0NDY0NjE0ZTQyODAxNjE1YjhmZmFhMDdhYzgyNTE0OTNmZmRhN2RlNWRkZjMzNjg4ODBjMmI5NWIwMzBmNDFmOGYxNTA2NmFkZDA3MWE2NmNmNjBlNWY0NmYzYTIzMGQzOTdiNjUyOTYzYTIxYTUzZg [11]: https://gchq.github.io/CyberChef/#recipe=XOR(%7B'option':'Hex','string':'3a'%7D,'Standard',false)To_Hexdump(16,false,false)&input=VGhlIGFuc3dlciB0byB0aGUgdWx0aW1hdGUgcXVlc3Rpb24gb2YgbGlmZSwgdGhlIFVuaXZlcnNlLCBhbmQgZXZlcnl0aGluZyBpcyA0Mi4 [12]: https://gchq.github.io/CyberChef/#recipe=Magic(3,false,false)&input=V1VhZ3dzaWFlNm1QOGdOdENDTFVGcENwQ0IyNlJtQkRvREQ4UGFjZEFtekF6QlZqa0syUXN0RlhhS2hwQzZpVVM3UkhxWHJKdEZpc29SU2dvSjR3aGptMWFybTg2NHFhTnE0UmNmVW1MSHJjc0FhWmM1VFhDWWlmTmRnUzgzZ0RlZWpHWDQ2Z2FpTXl1QlY2RXNrSHQxc2NnSjg4eDJ0TlNvdFFEd2JHWTFtbUNvYjJBUkdGdkNLWU5xaU45aXBNcTFaVTFtZ2tkYk51R2NiNzZhUnRZV2hDR1VjOGc5M1VKdWRoYjhodHNoZVpud1RwZ3FoeDgzU1ZKU1pYTVhVakpUMnptcEM3dVhXdHVtcW9rYmRTaTg4WXRrV0RBYzFUb291aDJvSDRENGRkbU5LSldVRHBNd21uZ1VtSzE0eHdtb21jY1BRRTloTTE3MkFQblNxd3hkS1ExNzJSa2NBc3lzbm1qNWdHdFJtVk5OaDJzMzU5d3I2bVMyUVJQ + [13]: https://gchq.github.io/CyberChef/#recipe=AES_Decrypt(%7B'option':'Hex','string':'1748e7179bd56570d51fa4ba287cc3e5'%7D,%7B'option':'Hex','string':'$R0'%7D,16,'CTR','Hex','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D,'From%20start')&input=NTFlMjAxZDQ2MzY5OGVmNWY3MTdmNzFmNWI0NzEyYWYyMGJlNjc0YjNiZmY1M2QzODU0NjM5NmVlNjFkYWFjNDkwOGUzMTljYTNmY2Y3MDg5YmZiNmIzOGVhOTllNzgxZDI2ZTU3N2JhOWRkNmYzMTFhMzk0MjBiODk3OGU5MzAxNGIwNDJkNDQ3MjZjYWVkZjU0MzZlYWY2NTI0MjljMGRmOTRiNTIxNjc2YzdjMmNlODEyMDk3YzI3NzI3M2M3YzcyY2Q4OWFlYzhkOWZiNGEyNzU4NmNjZjZhYTBhZWUyMjRjMzRiYTNiZmRmN2FlYjFkZGQ0Nzc2MjJiOTFlNzJjOWU3MDlhYjYwZjhkYWY3MzFlYzBjYzg1Y2UwZjc0NmZmMTU1NGE1YTNlYzI5MWNhNDBmOWU2MjlhODcyNTkyZDk4OGZkZDgzNDUzNGFiYTc5YzFhZDE2NzY3NjlhN2MwMTBiZjA0NzM5ZWNkYjY1ZDk1MzAyMzcxZDYyOWQ5ZTM3ZTdiNGEzNjFkYTQ2OGYxZWQ1MzU4OTIyZDJlYTc1MmRkMTFjMzY2ZjMwMTdiMTRhYTAxMWQyYWYwM2M0NGY5NTU3OTA5OGExNWUzY2Y5YjQ0ODZmOGZmZTljMjM5ZjM0ZGU3MTUxZjZjYTY1MDBmZTRiODUwYzNmMWMwMmU4MDFjYWYzYTI0NDY0NjE0ZTQyODAxNjE1YjhmZmFhMDdhYzgyNTE0OTNmZmRhN2RlNWRkZjMzNjg4ODBjMmI5NWIwMzBmNDFmOGYxNTA2NmFkZDA3MWE2NmNmNjBlNWY0NmYzYTIzMGQzOTdiNjUyOTYzYTIxYTUzZg