• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

javascript-obfuscator / javascript-obfuscator / 29062568267

09 Jul 2026 07:56PM UTC coverage: 96.0% (-0.3%) from 96.311%
29062568267

push

github

web-flow
Fixed unicode (`\uXXXX`, `\u{XXXX}`) and hex (`\xXX`) escape sequences of string literals being un-escaped into their literal characters during obfuscation (#1427)

2009 of 2202 branches covered (91.24%)

Branch coverage included in aggregate %.

84 of 97 new or added lines in 4 files covered. (86.6%)

6103 of 6248 relevant lines covered (97.68%)

30101709.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.25
/src/utils/EscapeSequenceEncoder.ts
1
import { injectable } from 'inversify';
6✔
2

3
import { IEscapeSequenceEncoder } from '../interfaces/utils/IEscapeSequenceEncoder';
4

5
@injectable()
6
export class EscapeSequenceEncoder implements IEscapeSequenceEncoder {
6✔
7
    /**
8
     * https://bytefreaks.net/gnulinux/regular-expression-to-match-any-ascii-character
9
     *
10
     * @type {RegExp}
11
     */
12
    private static readonly ASCIICharactersRegExp: RegExp = /[\x00-\x7F]/;
6✔
13

14
    /**
15
     * https://en.wikipedia.org/wiki/List_of_Unicode_characters
16
     * \x00-\x1F\x7F-\x9F are the control unicode characters
17
     *
18
     * @type {RegExp}
19
     */
20
    private static readonly forceEscapeCharactersRegExp: RegExp = /[\x00-\x1F\x7F-\x9F'"\\\s]/;
6✔
21

22
    /**
23
     * @type {RegExp}
24
     */
25
    private static readonly replaceRegExp: RegExp = /[\s\S]/g;
6✔
26

27
    /**
28
     * @type {RegExp}
29
     */
30
    private static readonly hexDigitRegExp: RegExp = /[0-9a-fA-F]/;
6✔
31

32
    /**
33
     * @type {RegExp}
34
     */
35
    private static readonly octalDigitRegExp: RegExp = /[0-7]/;
6✔
36

37
    /**
38
     * @type {Map<string, string>}
39
     */
40
    private readonly stringsCache: Map<string, string> = new Map();
203,348✔
41

42
    /**
43
     * @param {string} string
44
     * @param {boolean} encodeAllSymbols
45
     * @returns {string}
46
     */
47
    public encode(string: string, encodeAllSymbols: boolean): string {
48
        const cacheKey: string = `${string}-${String(encodeAllSymbols)}`;
27,373,725✔
49

50
        if (this.stringsCache.has(cacheKey)) {
27,373,725✔
51
            return <string>this.stringsCache.get(cacheKey);
19,381,522✔
52
        }
53

54
        const result: string = string.replace(
7,992,203✔
55
            EscapeSequenceEncoder.replaceRegExp,
56
            (character: string): string => this.encodeCharacter(character, encodeAllSymbols)
43,617,652✔
57
        );
58

59
        this.stringsCache.set(cacheKey, result);
7,992,203✔
60
        this.stringsCache.set(`${result}-${String(encodeAllSymbols)}`, result);
7,992,203✔
61

62
        return result;
7,992,203✔
63
    }
64

65
    /**
66
     * @param {string} value - decoded literal value
67
     * @param {string | undefined} rawValue - original source representation of the literal (with quotes)
68
     * @param {boolean} encodeAllSymbols
69
     * @returns {string}
70
     */
71
    public encodeLiteral(value: string, rawValue: string | undefined, encodeAllSymbols: boolean): string {
72
        // when all symbols are being encoded every character becomes an escape sequence anyway,
73
        // so there is nothing to preserve
74
        if (encodeAllSymbols || rawValue === undefined) {
29,796,797✔
75
            return this.encode(value, encodeAllSymbols);
27,371,509✔
76
        }
77

78
        const cacheKey: string = `literal-${value}-${rawValue}-${String(encodeAllSymbols)}`;
2,425,288✔
79

80
        if (this.stringsCache.has(cacheKey)) {
2,425,288✔
81
            return <string>this.stringsCache.get(cacheKey);
1,034,249✔
82
        }
83

84
        const preservedResult: string | null = this.encodePreservingUnicodeEscapes(value, rawValue);
1,391,039✔
85
        const result: string = preservedResult ?? this.encode(value, encodeAllSymbols);
1,391,039✔
86

87
        this.stringsCache.set(cacheKey, result);
1,391,039✔
88

89
        return result;
1,391,039✔
90
    }
91

92
    /**
93
     * @param {string} character
94
     * @param {boolean} encodeAllSymbols
95
     * @returns {string}
96
     */
97
    private encodeCharacter(character: string, encodeAllSymbols: boolean): string {
98
        const shouldEncodeCharacter: boolean =
99
            encodeAllSymbols || EscapeSequenceEncoder.forceEscapeCharactersRegExp.test(character);
56,078,257✔
100

101
        if (!shouldEncodeCharacter) {
56,078,257✔
102
            return character;
12,136,326✔
103
        }
104

105
        const radix: number = 16;
43,941,931✔
106

107
        let prefix: string;
108
        let template: string;
109

110
        if (EscapeSequenceEncoder.ASCIICharactersRegExp.test(character)) {
43,941,931✔
111
            prefix = '\\x';
43,941,804✔
112
            template = '00';
43,941,804✔
113
        } else {
114
            prefix = '\\u';
127✔
115
            template = '0000';
127✔
116
        }
117

118
        return `${prefix}${(template + character.charCodeAt(0).toString(radix)).slice(-template.length)}`;
43,941,931✔
119
    }
120

121
    /**
122
     * @param {string} value
123
     * @param {string} rawValue
124
     * @returns {string | null}
125
     */
126
    private encodePreservingUnicodeEscapes(value: string, rawValue: string): string | null {
127
        const rawBody: string = rawValue.slice(1, -1);
1,391,039✔
128

129
        let result: string = '';
1,391,039✔
130
        let decoded: string = '';
1,391,039✔
131
        let index: number = 0;
1,391,039✔
132

133
        while (index < rawBody.length) {
1,391,039✔
134
            const character: string = rawBody[index];
12,499,467✔
135

136
            if (character !== '\\') {
12,499,467✔
137
                result += this.encodeCharacter(character, false);
12,297,614✔
138
                decoded += character;
12,297,614✔
139
                index++;
12,297,614✔
140

141
                continue;
12,297,614✔
142
            }
143

144
            const nextCharacter: string | undefined = rawBody[index + 1];
201,853✔
145

146
            if (nextCharacter === undefined) {
201,853✔
147
                // dangling backslash - malformed, cannot trust the raw value
148
                return null;
12✔
149
            }
150

151
            if (nextCharacter === 'u' || nextCharacter === 'x') {
201,841✔
152
                const unicodeEscape: { raw: string; decoded: string } | null = this.readUnicodeEscapeSequence(
38,850✔
153
                    rawBody,
154
                    index
155
                );
156

157
                if (!unicodeEscape) {
38,850!
NEW
158
                    return null;
×
159
                }
160

161
                // keep the unicode/hex escape sequence exactly as it was in the source
162
                result += unicodeEscape.raw;
38,850✔
163
                decoded += unicodeEscape.decoded;
38,850✔
164
                index += unicodeEscape.raw.length;
38,850✔
165

166
                continue;
38,850✔
167
            }
168

169
            // any other escape sequence is decoded and re-encoded as a regular character
170
            const simpleEscape: { raw: string; decoded: string } = this.readSimpleEscapeSequence(rawBody, index);
162,991✔
171

172
            for (const decodedCharacter of simpleEscape.decoded) {
162,991✔
173
                result += this.encodeCharacter(decodedCharacter, false);
162,991✔
174
            }
175

176
            decoded += simpleEscape.decoded;
162,991✔
177
            index += simpleEscape.raw.length;
162,991✔
178
        }
179

180
        return decoded === value ? result : null;
1,391,027✔
181
    }
182

183
    /**
184
     * @param {string} rawBody
185
     * @param {number} startIndex - index of the leading backslash
186
     * @returns {{raw: string, decoded: string} | null}
187
     */
188
    private readUnicodeEscapeSequence(
189
        rawBody: string,
190
        startIndex: number
191
    ): { raw: string; decoded: string } | null {
192
        // `\xXX`
193
        if (rawBody[startIndex + 1] === 'x') {
38,850✔
194
            return this.readFixedLengthHexEscapeSequence(rawBody, startIndex, 2);
38,832✔
195
        }
196

197
        // `\u{XXXX}`
198
        if (rawBody[startIndex + 2] === '{') {
18✔
199
            return this.readCodePointEscapeSequence(rawBody, startIndex);
6✔
200
        }
201

202
        // `\uXXXX`
203
        return this.readFixedLengthHexEscapeSequence(rawBody, startIndex, 4);
12✔
204
    }
205

206
    /**
207
     * @param {string} rawBody
208
     * @param {number} startIndex - index of the leading backslash
209
     * @param {number} hexLength - amount of the hex digits (2 for `\xXX`, 4 for `\uXXXX`)
210
     * @returns {{raw: string, decoded: string} | null}
211
     */
212
    private readFixedLengthHexEscapeSequence(
213
        rawBody: string,
214
        startIndex: number,
215
        hexLength: number
216
    ): { raw: string; decoded: string } | null {
217
        const hexDigits: string = rawBody.slice(startIndex + 2, startIndex + 2 + hexLength);
38,844✔
218

219
        if (hexDigits.length !== hexLength || !this.isHexString(hexDigits)) {
38,844!
NEW
220
            return null;
×
221
        }
222

223
        return {
38,844✔
224
            raw: rawBody.slice(startIndex, startIndex + 2 + hexLength),
225
            decoded: String.fromCharCode(parseInt(hexDigits, 16))
226
        };
227
    }
228

229
    /**
230
     * @param {string} rawBody
231
     * @param {number} startIndex - index of the leading backslash
232
     * @returns {{raw: string, decoded: string} | null}
233
     */
234
    private readCodePointEscapeSequence(
235
        rawBody: string,
236
        startIndex: number
237
    ): { raw: string; decoded: string } | null {
238
        const closingBraceIndex: number = rawBody.indexOf('}', startIndex + 3);
6✔
239

240
        if (closingBraceIndex === -1) {
6!
NEW
241
            return null;
×
242
        }
243

244
        const hexDigits: string = rawBody.slice(startIndex + 3, closingBraceIndex);
6✔
245
        const codePoint: number = parseInt(hexDigits, 16);
6✔
246

247
        const maxCodePoint: number = 0x10_ff_ff;
6✔
248

249
        if (!hexDigits.length || !this.isHexString(hexDigits) || codePoint > maxCodePoint) {
6!
NEW
250
            return null;
×
251
        }
252

253
        return {
6✔
254
            raw: rawBody.slice(startIndex, closingBraceIndex + 1),
255
            decoded: String.fromCodePoint(codePoint)
256
        };
257
    }
258

259
    /**
260
     * @param {string} rawBody
261
     * @param {number} startIndex - index of the leading backslash
262
     * @returns {{raw: string, decoded: string}}
263
     */
264
    // eslint-disable-next-line complexity
265
    private readSimpleEscapeSequence(rawBody: string, startIndex: number): { raw: string; decoded: string } {
266
        const escapeCharacter: string = rawBody[startIndex + 1];
162,991✔
267

268
        switch (escapeCharacter) {
162,991✔
269
            case 'n':
162,991!
270
                return { raw: '\\n', decoded: '\n' };
12,354✔
271

272
            case 'r':
273
                return { raw: '\\r', decoded: '\r' };
36✔
274

275
            case 't':
276
                return { raw: '\\t', decoded: '\t' };
20✔
277

278
            case 'b':
279
                return { raw: '\\b', decoded: '\b' };
18✔
280

281
            case 'f':
282
                return { raw: '\\f', decoded: '\f' };
12✔
283

284
            case 'v':
285
                return { raw: '\\v', decoded: '\v' };
12✔
286

287
            case '\r': {
288
                // line continuation (`\` followed by a line terminator)
NEW
289
                const isCarriageReturnLineFeed: boolean = rawBody[startIndex + 2] === '\n';
×
290

NEW
291
                return { raw: isCarriageReturnLineFeed ? '\\\r\n' : '\\\r', decoded: '' };
×
292
            }
293

294
            case '\n':
NEW
295
                return { raw: '\\\n', decoded: '' };
×
296

297
            default:
298
                break;
150,539✔
299
        }
300

301
        // legacy octal escape sequence (e.g. `\0`, `\12`, `\101`)
302
        if (EscapeSequenceEncoder.octalDigitRegExp.test(escapeCharacter)) {
150,539!
NEW
303
            let octalDigits: string = escapeCharacter;
×
NEW
304
            const maxOctalLength: number = escapeCharacter <= '3' ? 3 : 2;
×
305

NEW
306
            while (
×
307
                octalDigits.length < maxOctalLength &&
×
308
                EscapeSequenceEncoder.octalDigitRegExp.test(rawBody[startIndex + 1 + octalDigits.length] ?? '')
×
309
            ) {
NEW
310
                octalDigits += rawBody[startIndex + 1 + octalDigits.length];
×
311
            }
312

NEW
313
            return {
×
314
                raw: `\\${octalDigits}`,
315
                decoded: String.fromCharCode(parseInt(octalDigits, 8))
316
            };
317
        }
318

319
        // identity escape (`\\`, `\'`, `\"`, `` \` ``, `\/`, etc.)
320
        return { raw: `\\${escapeCharacter}`, decoded: escapeCharacter };
150,539✔
321
    }
322

323
    /**
324
     * @param {string} string
325
     * @returns {boolean}
326
     */
327
    private isHexString(string: string): boolean {
328
        for (const character of string) {
38,850✔
329
            if (!EscapeSequenceEncoder.hexDigitRegExp.test(character)) {
77,742!
NEW
330
                return false;
×
331
            }
332
        }
333

334
        return true;
38,850✔
335
    }
336
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc