Skip to content

Commit 4bc59a5

Browse files
authored
Merge pull request microsoft#165635 from rwe/fix-deserialize-message
shellIntegrationAddon: fix broken `deserializeMessage()` implementation + add tests
2 parents bacf5eb + 7d99990 commit 4bc59a5

File tree

2 files changed

+42
-11
lines changed

2 files changed

+42
-11
lines changed

src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -513,16 +513,12 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
513513
}
514514

515515
export function deserializeMessage(message: string): string {
516-
let result = message.replace(/\\\\/g, '\\');
517-
const deserializeRegex = /\\x([0-9a-f]{2})/i;
518-
while (true) {
519-
const match = result.match(deserializeRegex);
520-
if (!match?.index || match.length < 2) {
521-
break;
522-
}
523-
result = result.slice(0, match.index) + String.fromCharCode(parseInt(match[1], 16)) + result.slice(match.index + 4);
524-
}
525-
return result;
516+
return message.replaceAll(
517+
// Backslash ('\') followed by an escape operator: either another '\', or 'x' and two hex chars.
518+
/\\(\\|x([0-9a-f]{2}))/gi,
519+
// If it's a hex value, parse it to a character.
520+
// Otherwise the operator is '\', which we return literally, now unescaped.
521+
(_match: string, op: string, hex?: string) => hex ? String.fromCharCode(parseInt(hex, 16)) : op);
526522
}
527523

528524
export function parseKeyValueAssignment(message: string): { key: string; value: string | undefined } {

src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Terminal } from 'xterm';
77
import { strictEqual, deepStrictEqual, deepEqual } from 'assert';
88
import { timeout } from 'vs/base/common/async';
99
import * as sinon from 'sinon';
10-
import { parseKeyValueAssignment, parseMarkSequence, ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon';
10+
import { parseKeyValueAssignment, parseMarkSequence, deserializeMessage, ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon';
1111
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
1212
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
1313
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
@@ -257,6 +257,41 @@ suite('ShellIntegrationAddon', () => {
257257
});
258258
});
259259

260+
suite('deserializeMessage', () => {
261+
// A single literal backslash, in order to avoid confusion about whether we are escaping test data or testing escapes.
262+
const Backslash = '\\' as const;
263+
const Newline = '\n' as const;
264+
const Semicolon = ';' as const;
265+
266+
type TestCase = [title: string, input: string, expected: string];
267+
const cases: TestCase[] = [
268+
['empty', '', ''],
269+
['basic', 'value', 'value'],
270+
['space', 'some thing', 'some thing'],
271+
['escaped backslash', `${Backslash}${Backslash}`, Backslash],
272+
['non-initial escaped backslash', `foo${Backslash}${Backslash}`, `foo${Backslash}`],
273+
['two escaped backslashes', `${Backslash}${Backslash}${Backslash}${Backslash}`, `${Backslash}${Backslash}`],
274+
['escaped backslash amidst text', `Hello${Backslash}${Backslash}there`, `Hello${Backslash}there`],
275+
['backslash escaped literally and as hex', `${Backslash}${Backslash} is same as ${Backslash}x5c`, `${Backslash} is same as ${Backslash}`],
276+
['escaped semicolon', `${Backslash}x3b`, Semicolon],
277+
['non-initial escaped semicolon', `foo${Backslash}x3b`, `foo${Semicolon}`],
278+
['escaped semicolon (upper hex)', `${Backslash}x3B`, Semicolon],
279+
['escaped backslash followed by literal "x3b" is not a semicolon', `${Backslash}${Backslash}x3b`, `${Backslash}x3b`],
280+
['non-initial escaped backslash followed by literal "x3b" is not a semicolon', `foo${Backslash}${Backslash}x3b`, `foo${Backslash}x3b`],
281+
['escaped backslash followed by escaped semicolon', `${Backslash}${Backslash}${Backslash}x3b`, `${Backslash}${Semicolon}`],
282+
['escaped semicolon amidst text', `some${Backslash}x3bthing`, `some${Semicolon}thing`],
283+
['escaped newline', `${Backslash}x0a`, Newline],
284+
['non-initial escaped newline', `foo${Backslash}x0a`, `foo${Newline}`],
285+
['escaped newline (upper hex)', `${Backslash}x0A`, Newline],
286+
['escaped backslash followed by literal "x0a" is not a newline', `${Backslash}${Backslash}x0a`, `${Backslash}x0a`],
287+
['non-initial escaped backslash followed by literal "x0a" is not a newline', `foo${Backslash}${Backslash}x0a`, `foo${Backslash}x0a`],
288+
];
289+
290+
cases.forEach(([title, input, expected]) => {
291+
test(title, () => strictEqual(deserializeMessage(input), expected));
292+
});
293+
});
294+
260295
test('parseKeyValueAssignment', () => {
261296
type TestCase = [title: string, input: string, expected: [key: string, value: string | undefined]];
262297
const cases: TestCase[] = [

0 commit comments

Comments
 (0)