mirror of
https://github.com/javascript-obfuscator/javascript-obfuscator
synced 2026-08-09 12:42:29 +00:00
Fixed unicode (\uXXXX, \u{XXXX}) and hex (\xXX) escape sequences of string literals being un-escaped into their literal characters during obfuscation
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "javascript-obfuscator",
|
||||
"version": "5.4.5",
|
||||
"version": "5.4.6",
|
||||
"description": "JavaScript obfuscator",
|
||||
"keywords": [
|
||||
"obfuscator",
|
||||
|
||||
@@ -5,11 +5,14 @@ import { IStringArrayStorageItemData } from './IStringArrayStorageItem';
|
||||
|
||||
export interface ILiteralNodesCacheStorage extends IMapStorage<string, ESTree.Node> {
|
||||
/**
|
||||
* @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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,12 +27,17 @@ export class LiteralNodesCacheStorage extends MapStorage<string, ESTree.Node> 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)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, string>}
|
||||
*/
|
||||
@@ -41,33 +51,286 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder {
|
||||
return <string>this.stringsCache.get(cacheKey);
|
||||
}
|
||||
|
||||
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 (!shouldEncodeCharacter) {
|
||||
return character;
|
||||
}
|
||||
|
||||
if (EscapeSequenceEncoder.ASCIICharactersRegExp.test(character)) {
|
||||
prefix = '\\x';
|
||||
template = '00';
|
||||
} else {
|
||||
prefix = '\\u';
|
||||
template = '0000';
|
||||
}
|
||||
|
||||
return `${prefix}${(template + character.charCodeAt(0).toString(radix)).slice(-template.length)}`;
|
||||
});
|
||||
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 <string>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;
|
||||
|
||||
if (EscapeSequenceEncoder.ASCIICharactersRegExp.test(character)) {
|
||||
prefix = '\\x';
|
||||
template = '00';
|
||||
} else {
|
||||
prefix = '\\u';
|
||||
template = '0000';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const nextCharacter: string | undefined = rawBody[index + 1];
|
||||
|
||||
if (nextCharacter === undefined) {
|
||||
// dangling backslash - malformed, cannot trust the raw value
|
||||
return null;
|
||||
}
|
||||
|
||||
if (nextCharacter === 'u' || nextCharacter === 'x') {
|
||||
const unicodeEscape: { raw: string; decoded: string } | null = this.readUnicodeEscapeSequence(
|
||||
rawBody,
|
||||
index
|
||||
);
|
||||
|
||||
if (!unicodeEscape) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
console.log("\ud83d\ude03");
|
||||
console.log("😃");
|
||||
console.log("\x41\x42\x43");
|
||||
console.log("\u{1F604}");
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
+33
-12
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -81,4 +81,131 @@ describe('EscapeSequenceEncoder', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeLiteral', () => {
|
||||
let escapeSequenceEncoder: IEscapeSequenceEncoder;
|
||||
|
||||
before(() => {
|
||||
const inversifyContainerFacade: IInversifyContainerFacade = new InversifyContainerFacade();
|
||||
|
||||
inversifyContainerFacade.load('', '', {});
|
||||
escapeSequenceEncoder = inversifyContainerFacade.get<IEscapeSequenceEncoder>(
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user