diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f5d4c4e..d31d585d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.4.6 +--- +* Fixed unicode (`\uXXXX`, `\u{XXXX}`) and hex (`\xXX`) escape sequences of string literals being un-escaped into their literal characters during obfuscation. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/345 + v5.4.5 --- * Fixed `controlFlowFlattening` intermittently dropping arguments of a spread call (e.g. `foo(...args)`) when it reused a control flow wrapper of a same-arity plain call. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1423 diff --git a/package.json b/package.json index 107c8d02e..ca70734b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.4.5", + "version": "5.4.6", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage.ts b/src/interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage.ts index 4c505cb25..5cc8c24f8 100644 --- a/src/interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage.ts +++ b/src/interfaces/storages/string-array-transformers/ILiteralNodesCacheStorage.ts @@ -5,11 +5,14 @@ import { IStringArrayStorageItemData } from './IStringArrayStorageItem'; export interface ILiteralNodesCacheStorage extends IMapStorage { /** - * @param {string} literalValue + * @param {Literal} literalNode * @param {IStringArrayStorageItemData | undefined} stringArrayStorageItemData * @returns {string} */ - buildKey(literalValue: string, stringArrayStorageItemData: IStringArrayStorageItemData | undefined): string; + buildKey( + literalNode: ESTree.Literal, + stringArrayStorageItemData: IStringArrayStorageItemData | undefined + ): string; /** * @param {string} key diff --git a/src/interfaces/utils/IEscapeSequenceEncoder.ts b/src/interfaces/utils/IEscapeSequenceEncoder.ts index e8cc0aec7..182d9561f 100644 --- a/src/interfaces/utils/IEscapeSequenceEncoder.ts +++ b/src/interfaces/utils/IEscapeSequenceEncoder.ts @@ -5,4 +5,12 @@ export interface IEscapeSequenceEncoder { * @returns {string} */ encode(string: string, encodeAllSymbols: boolean): string; + + /** + * @param {string} value + * @param {string | undefined} rawValue + * @param {boolean} encodeAllSymbols + * @returns {string} + */ + encodeLiteral(value: string, rawValue: string | undefined, encodeAllSymbols: boolean): string; } diff --git a/src/node-transformers/finalizing-transformers/EscapeSequenceTransformer.ts b/src/node-transformers/finalizing-transformers/EscapeSequenceTransformer.ts index d4ffd593b..0a10adeee 100644 --- a/src/node-transformers/finalizing-transformers/EscapeSequenceTransformer.ts +++ b/src/node-transformers/finalizing-transformers/EscapeSequenceTransformer.ts @@ -75,8 +75,9 @@ export class EscapeSequenceTransformer extends AbstractNodeTransformer { return literalNode; } - const encodedValue: string = this.escapeSequenceEncoder.encode( + const encodedValue: string = this.escapeSequenceEncoder.encodeLiteral( literalNode.value, + literalNode.raw, this.options.unicodeEscapeSequence ); const newLiteralNode: ESTree.Literal = NodeFactory.literalNode(encodedValue); diff --git a/src/node-transformers/string-array-transformers/StringArrayTransformer.ts b/src/node-transformers/string-array-transformers/StringArrayTransformer.ts index dbc487fbc..05d1f2302 100644 --- a/src/node-transformers/string-array-transformers/StringArrayTransformer.ts +++ b/src/node-transformers/string-array-transformers/StringArrayTransformer.ts @@ -184,11 +184,9 @@ export class StringArrayTransformer extends AbstractNodeTransformer { return literalNode; } - const literalValue: ESTree.SimpleLiteral['value'] = literalNode.value; - const stringArrayStorageItemData: IStringArrayStorageItemData | undefined = this.stringArrayStorageAnalyzer.getItemDataForLiteralNode(literalNode); - const cacheKey: string = this.literalNodesCacheStorage.buildKey(literalValue, stringArrayStorageItemData); + const cacheKey: string = this.literalNodesCacheStorage.buildKey(literalNode, stringArrayStorageItemData); const useCachedValue: boolean = this.literalNodesCacheStorage.shouldUseCachedValue( cacheKey, stringArrayStorageItemData diff --git a/src/storages/string-array-transformers/LiteralNodesCacheStorage.ts b/src/storages/string-array-transformers/LiteralNodesCacheStorage.ts index 31d3e3c6e..e6afe057d 100644 --- a/src/storages/string-array-transformers/LiteralNodesCacheStorage.ts +++ b/src/storages/string-array-transformers/LiteralNodesCacheStorage.ts @@ -27,12 +27,17 @@ export class LiteralNodesCacheStorage extends MapStorage im } /** - * @param {string} literalValue + * @param {Literal} literalNode * @param {IStringArrayStorageItemData | undefined} stringArrayStorageItemData * @returns {string} */ - public buildKey(literalValue: string, stringArrayStorageItemData: IStringArrayStorageItemData | undefined): string { - return `${literalValue}-${Boolean(stringArrayStorageItemData)}`; + public buildKey( + literalNode: ESTree.Literal, + stringArrayStorageItemData: IStringArrayStorageItemData | undefined + ): string { + // `raw` value is a part of the key to keep literals that share the same decoded value but + // have a different source representation (e.g. `'😃'` and `'😃'`) separate + return `${String(literalNode.value)}-${String(literalNode.raw)}-${Boolean(stringArrayStorageItemData)}`; } /** diff --git a/src/utils/EscapeSequenceEncoder.ts b/src/utils/EscapeSequenceEncoder.ts index 75511d7c3..d30f62be4 100644 --- a/src/utils/EscapeSequenceEncoder.ts +++ b/src/utils/EscapeSequenceEncoder.ts @@ -24,6 +24,16 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder { */ private static readonly replaceRegExp: RegExp = /[\s\S]/g; + /** + * @type {RegExp} + */ + private static readonly hexDigitRegExp: RegExp = /[0-9a-fA-F]/; + + /** + * @type {RegExp} + */ + private static readonly octalDigitRegExp: RegExp = /[0-7]/; + /** * @type {Map} */ @@ -41,33 +51,286 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder { return this.stringsCache.get(cacheKey); } + const result: string = string.replace( + EscapeSequenceEncoder.replaceRegExp, + (character: string): string => this.encodeCharacter(character, encodeAllSymbols) + ); + + this.stringsCache.set(cacheKey, result); + this.stringsCache.set(`${result}-${String(encodeAllSymbols)}`, result); + + return result; + } + + /** + * @param {string} value - decoded literal value + * @param {string | undefined} rawValue - original source representation of the literal (with quotes) + * @param {boolean} encodeAllSymbols + * @returns {string} + */ + public encodeLiteral(value: string, rawValue: string | undefined, encodeAllSymbols: boolean): string { + // when all symbols are being encoded every character becomes an escape sequence anyway, + // so there is nothing to preserve + if (encodeAllSymbols || rawValue === undefined) { + return this.encode(value, encodeAllSymbols); + } + + const cacheKey: string = `literal-${value}-${rawValue}-${String(encodeAllSymbols)}`; + + if (this.stringsCache.has(cacheKey)) { + return this.stringsCache.get(cacheKey); + } + + const preservedResult: string | null = this.encodePreservingUnicodeEscapes(value, rawValue); + const result: string = preservedResult ?? this.encode(value, encodeAllSymbols); + + this.stringsCache.set(cacheKey, result); + + return result; + } + + /** + * @param {string} character + * @param {boolean} encodeAllSymbols + * @returns {string} + */ + private encodeCharacter(character: string, encodeAllSymbols: boolean): string { + const shouldEncodeCharacter: boolean = + encodeAllSymbols || EscapeSequenceEncoder.forceEscapeCharactersRegExp.test(character); + + if (!shouldEncodeCharacter) { + return character; + } + const radix: number = 16; let prefix: string; let template: string; - const result: string = string.replace(EscapeSequenceEncoder.replaceRegExp, (character: string): string => { - const shouldEncodeCharacter: boolean = - encodeAllSymbols || EscapeSequenceEncoder.forceEscapeCharactersRegExp.test(character); + if (EscapeSequenceEncoder.ASCIICharactersRegExp.test(character)) { + prefix = '\\x'; + template = '00'; + } else { + prefix = '\\u'; + template = '0000'; + } - if (!shouldEncodeCharacter) { - return character; + return `${prefix}${(template + character.charCodeAt(0).toString(radix)).slice(-template.length)}`; + } + + /** + * @param {string} value + * @param {string} rawValue + * @returns {string | null} + */ + private encodePreservingUnicodeEscapes(value: string, rawValue: string): string | null { + const rawBody: string = rawValue.slice(1, -1); + + let result: string = ''; + let decoded: string = ''; + let index: number = 0; + + while (index < rawBody.length) { + const character: string = rawBody[index]; + + if (character !== '\\') { + result += this.encodeCharacter(character, false); + decoded += character; + index++; + + continue; } - if (EscapeSequenceEncoder.ASCIICharactersRegExp.test(character)) { - prefix = '\\x'; - template = '00'; - } else { - prefix = '\\u'; - template = '0000'; + const nextCharacter: string | undefined = rawBody[index + 1]; + + if (nextCharacter === undefined) { + // dangling backslash - malformed, cannot trust the raw value + return null; } - return `${prefix}${(template + character.charCodeAt(0).toString(radix)).slice(-template.length)}`; - }); + if (nextCharacter === 'u' || nextCharacter === 'x') { + const unicodeEscape: { raw: string; decoded: string } | null = this.readUnicodeEscapeSequence( + rawBody, + index + ); - this.stringsCache.set(cacheKey, result); - this.stringsCache.set(`${result}-${String(encodeAllSymbols)}`, result); + if (!unicodeEscape) { + return null; + } - return result; + // keep the unicode/hex escape sequence exactly as it was in the source + result += unicodeEscape.raw; + decoded += unicodeEscape.decoded; + index += unicodeEscape.raw.length; + + continue; + } + + // any other escape sequence is decoded and re-encoded as a regular character + const simpleEscape: { raw: string; decoded: string } = this.readSimpleEscapeSequence(rawBody, index); + + for (const decodedCharacter of simpleEscape.decoded) { + result += this.encodeCharacter(decodedCharacter, false); + } + + decoded += simpleEscape.decoded; + index += simpleEscape.raw.length; + } + + return decoded === value ? result : null; + } + + /** + * @param {string} rawBody + * @param {number} startIndex - index of the leading backslash + * @returns {{raw: string, decoded: string} | null} + */ + private readUnicodeEscapeSequence( + rawBody: string, + startIndex: number + ): { raw: string; decoded: string } | null { + // `\xXX` + if (rawBody[startIndex + 1] === 'x') { + return this.readFixedLengthHexEscapeSequence(rawBody, startIndex, 2); + } + + // `\u{XXXX}` + if (rawBody[startIndex + 2] === '{') { + return this.readCodePointEscapeSequence(rawBody, startIndex); + } + + // `\uXXXX` + return this.readFixedLengthHexEscapeSequence(rawBody, startIndex, 4); + } + + /** + * @param {string} rawBody + * @param {number} startIndex - index of the leading backslash + * @param {number} hexLength - amount of the hex digits (2 for `\xXX`, 4 for `\uXXXX`) + * @returns {{raw: string, decoded: string} | null} + */ + private readFixedLengthHexEscapeSequence( + rawBody: string, + startIndex: number, + hexLength: number + ): { raw: string; decoded: string } | null { + const hexDigits: string = rawBody.slice(startIndex + 2, startIndex + 2 + hexLength); + + if (hexDigits.length !== hexLength || !this.isHexString(hexDigits)) { + return null; + } + + return { + raw: rawBody.slice(startIndex, startIndex + 2 + hexLength), + decoded: String.fromCharCode(parseInt(hexDigits, 16)) + }; + } + + /** + * @param {string} rawBody + * @param {number} startIndex - index of the leading backslash + * @returns {{raw: string, decoded: string} | null} + */ + private readCodePointEscapeSequence( + rawBody: string, + startIndex: number + ): { raw: string; decoded: string } | null { + const closingBraceIndex: number = rawBody.indexOf('}', startIndex + 3); + + if (closingBraceIndex === -1) { + return null; + } + + const hexDigits: string = rawBody.slice(startIndex + 3, closingBraceIndex); + const codePoint: number = parseInt(hexDigits, 16); + + const maxCodePoint: number = 0x10_ff_ff; + + if (!hexDigits.length || !this.isHexString(hexDigits) || codePoint > maxCodePoint) { + return null; + } + + return { + raw: rawBody.slice(startIndex, closingBraceIndex + 1), + decoded: String.fromCodePoint(codePoint) + }; + } + + /** + * @param {string} rawBody + * @param {number} startIndex - index of the leading backslash + * @returns {{raw: string, decoded: string}} + */ + // eslint-disable-next-line complexity + private readSimpleEscapeSequence(rawBody: string, startIndex: number): { raw: string; decoded: string } { + const escapeCharacter: string = rawBody[startIndex + 1]; + + switch (escapeCharacter) { + case 'n': + return { raw: '\\n', decoded: '\n' }; + + case 'r': + return { raw: '\\r', decoded: '\r' }; + + case 't': + return { raw: '\\t', decoded: '\t' }; + + case 'b': + return { raw: '\\b', decoded: '\b' }; + + case 'f': + return { raw: '\\f', decoded: '\f' }; + + case 'v': + return { raw: '\\v', decoded: '\v' }; + + case '\r': { + // line continuation (`\` followed by a line terminator) + const isCarriageReturnLineFeed: boolean = rawBody[startIndex + 2] === '\n'; + + return { raw: isCarriageReturnLineFeed ? '\\\r\n' : '\\\r', decoded: '' }; + } + + case '\n': + return { raw: '\\\n', decoded: '' }; + + default: + break; + } + + // legacy octal escape sequence (e.g. `\0`, `\12`, `\101`) + if (EscapeSequenceEncoder.octalDigitRegExp.test(escapeCharacter)) { + let octalDigits: string = escapeCharacter; + const maxOctalLength: number = escapeCharacter <= '3' ? 3 : 2; + + while ( + octalDigits.length < maxOctalLength && + EscapeSequenceEncoder.octalDigitRegExp.test(rawBody[startIndex + 1 + octalDigits.length] ?? '') + ) { + octalDigits += rawBody[startIndex + 1 + octalDigits.length]; + } + + return { + raw: `\\${octalDigits}`, + decoded: String.fromCharCode(parseInt(octalDigits, 8)) + }; + } + + // identity escape (`\\`, `\'`, `\"`, `` \` ``, `\/`, etc.) + return { raw: `\\${escapeCharacter}`, decoded: escapeCharacter }; + } + + /** + * @param {string} string + * @returns {boolean} + */ + private isHexString(string: string): boolean { + for (const character of string) { + if (!EscapeSequenceEncoder.hexDigitRegExp.test(character)) { + return false; + } + } + + return true; } } diff --git a/test/functional-tests/issues/fixtures/issue345.js b/test/functional-tests/issues/fixtures/issue345.js new file mode 100644 index 000000000..5372449be --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue345.js @@ -0,0 +1,4 @@ +console.log("\ud83d\ude03"); +console.log("😃"); +console.log("\x41\x42\x43"); +console.log("\u{1F604}"); diff --git a/test/functional-tests/issues/issue345.spec.ts b/test/functional-tests/issues/issue345.spec.ts new file mode 100644 index 000000000..2bb2bfc5e --- /dev/null +++ b/test/functional-tests/issues/issue345.spec.ts @@ -0,0 +1,78 @@ +import { assert } from 'chai'; + +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; + +import { readFileAsString } from '../../helpers/readFileAsString'; + +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/345 +// +describe('Issue #345', () => { + describe('Escape sequences of string literals should be preserved', () => { + let obfuscatedCode: string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue345.js'); + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('match #1: should keep the unicode escape sequence untouched', () => { + assert.match(obfuscatedCode, /console\['log'\]\('\\ud83d\\ude03'\);/); + }); + + it('match #2: should keep the un-escaped character untouched', () => { + assert.match(obfuscatedCode, /console\['log'\]\('😃'\);/); + }); + + it('match #3: should keep the hex escape sequence untouched', () => { + assert.match(obfuscatedCode, /console\['log'\]\('\\x41\\x42\\x43'\);/); + }); + + it('match #4: should keep the unicode code point escape sequence untouched', () => { + assert.match(obfuscatedCode, /console\['log'\]\('\\u{1F604}'\);/); + }); + }); + + describe('Escaped and un-escaped variants of the same value should stay distinguishable', () => { + let obfuscatedCode: string; + + before(() => { + const code: string = 'var a = "\\ud83d\\ude03"; var b = "😃";'; + + obfuscatedCode = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + }); + + it('should keep the escaped variant escaped', () => { + assert.match(obfuscatedCode, /var a *= *'\\ud83d\\ude03';/); + }); + + it('should keep the un-escaped variant un-escaped', () => { + assert.match(obfuscatedCode, /var b *= *'😃';/); + }); + }); + + describe('Obfuscated code should be runnable and produce the original values', () => { + let result: string; + + before(() => { + const code: string = 'return "\\ud83d\\ude03" + "😃" + "\\x41";'; + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET + }).getObfuscatedCode(); + + result = new Function(obfuscatedCode)(); + }); + + it('should evaluate to the correct string', () => { + assert.equal(result, '😃😃A'); + }); + }); +}); diff --git a/test/unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec.ts b/test/unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec.ts index ef8d2b8f4..05bb1f680 100644 --- a/test/unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec.ts +++ b/test/unit-tests/storages/string-array-transformers/literal-nodes-cache/LiteralNodesCacheStorage.spec.ts @@ -39,24 +39,45 @@ const getStorageInstance = (options: TInputOptions = {}): ILiteralNodesCacheStor describe('LiteralNodesCacheStorage', () => { describe('buildKey', () => { - const expectedCacheKey: string = 'foo-true'; + describe('Variant #1: base', () => { + const expectedCacheKey: string = "foo-'foo'-true"; - let cacheKey: string; + let cacheKey: string; - before(() => { - const literalNodesCacheStorage: ILiteralNodesCacheStorage = getStorageInstance(); + before(() => { + const literalNodesCacheStorage: ILiteralNodesCacheStorage = getStorageInstance(); - cacheKey = literalNodesCacheStorage.buildKey('foo', { - index: 1, - value: '_0x123abc', - encoding: StringArrayEncoding.Rc4, - encodedValue: 'encoded_value', - decodeKey: 'key' + cacheKey = literalNodesCacheStorage.buildKey(NodeFactory.literalNode('foo'), { + index: 1, + value: '_0x123abc', + encoding: StringArrayEncoding.Rc4, + encodedValue: 'encoded_value', + decodeKey: 'key' + }); + }); + + it('should build a key for the storage', () => { + assert.equal(cacheKey, expectedCacheKey); }); }); - it('should build a key for the storage', () => { - assert.equal(cacheKey, expectedCacheKey); + describe('Variant #2: literals with the same value but different `raw` representation', () => { + let firstCacheKey: string; + let secondCacheKey: string; + + before(() => { + const literalNodesCacheStorage: ILiteralNodesCacheStorage = getStorageInstance(); + + firstCacheKey = literalNodesCacheStorage.buildKey( + NodeFactory.literalNode('😃', '"\\ud83d\\ude03"'), + undefined + ); + secondCacheKey = literalNodesCacheStorage.buildKey(NodeFactory.literalNode('😃', '"😃"'), undefined); + }); + + it('should build different keys for literals with a different `raw` representation', () => { + assert.notEqual(firstCacheKey, secondCacheKey); + }); }); }); diff --git a/test/unit-tests/utils/EscapeSequenceEncoder.spec.ts b/test/unit-tests/utils/EscapeSequenceEncoder.spec.ts index 73624cc1c..9348d3261 100644 --- a/test/unit-tests/utils/EscapeSequenceEncoder.spec.ts +++ b/test/unit-tests/utils/EscapeSequenceEncoder.spec.ts @@ -81,4 +81,131 @@ describe('EscapeSequenceEncoder', () => { }); }); }); + + describe('encodeLiteral', () => { + let escapeSequenceEncoder: IEscapeSequenceEncoder; + + before(() => { + const inversifyContainerFacade: IInversifyContainerFacade = new InversifyContainerFacade(); + + inversifyContainerFacade.load('', '', {}); + escapeSequenceEncoder = inversifyContainerFacade.get( + ServiceIdentifiers.IEscapeSequenceEncoder + ); + }); + + describe('Variant #1: preserve unicode escape sequences from the raw value', () => { + // eslint-disable-next-line no-useless-escape + const value: string = '😃'; + const rawValue: string = '"\\ud83d\\ude03"'; + const expectedString: string = '\\ud83d\\ude03'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encodeLiteral(value, rawValue, false); + }); + + it('should keep the escape sequence untouched', () => { + assert.equal(actualString, expectedString); + }); + }); + + describe('Variant #2: preserve hex escape sequences from the raw value', () => { + const value: string = 'ABC'; + const rawValue: string = '"\\x41\\x42\\x43"'; + const expectedString: string = '\\x41\\x42\\x43'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encodeLiteral(value, rawValue, false); + }); + + it('should keep the hex escape sequence untouched', () => { + assert.equal(actualString, expectedString); + }); + }); + + describe('Variant #3: preserve unicode code point escape sequences from the raw value', () => { + const value: string = '\u{1F604}'; + const rawValue: string = '"\\u{1F604}"'; + const expectedString: string = '\\u{1F604}'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encodeLiteral(value, rawValue, false); + }); + + it('should keep the unicode code point escape sequence untouched', () => { + assert.equal(actualString, expectedString); + }); + }); + + describe('Variant #4: keep an un-escaped character un-escaped', () => { + const value: string = '😃'; + const rawValue: string = '"😃"'; + const expectedString: string = '😃'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encodeLiteral(value, rawValue, false); + }); + + it('should keep the character un-escaped', () => { + assert.equal(actualString, expectedString); + }); + }); + + describe('Variant #5: non unicode escape sequences are encoded as regular characters', () => { + const value: string = 'a\tb'; + const rawValue: string = '"a\\tb"'; + const expectedString: string = 'a\\x09b'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encodeLiteral(value, rawValue, false); + }); + + it('should encode the escape sequence as a regular character', () => { + assert.equal(actualString, expectedString); + }); + }); + + describe('Variant #6: untrusted raw value falls back to value-based encoding', () => { + // synthesized `raw` (e.g. `'a\b'`) does not faithfully represent the value `a\b` + const value: string = 'a\\b'; + const rawValue: string = "'a\\b'"; + const expectedString: string = 'a\\x5cb'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encodeLiteral(value, rawValue, false); + }); + + it('should encode the value with the safe value-based encoding', () => { + assert.equal(actualString, expectedString); + }); + }); + + describe('Variant #7: `encodeAllSymbols` escapes everything regardless of the raw value', () => { + const value: string = '😃'; + const rawValue: string = '"😃"'; + const expectedString: string = '\\ud83d\\ude03'; + + let actualString: string; + + before(() => { + actualString = escapeSequenceEncoder.encodeLiteral(value, rawValue, true); + }); + + it('should encode all symbols', () => { + assert.equal(actualString, expectedString); + }); + }); + }); });