From b40e58bc7709c0d36122c2d78eece11c603b81ef Mon Sep 17 00:00:00 2001 From: Josiah Hudson <108340950+josiahhudson@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:40:21 -0400 Subject: [PATCH 1/3] Fix https://github.com/xtermjs/xterm.js/issues/4944 by only splitting on the first ";" in InputHandler.setHyperlink(). --- src/common/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index a4b8c64b02..3482abc7d1 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2972,7 +2972,7 @@ export class InputHandler extends Disposable implements IInputHandler { * feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink. */ public setHyperlink(data: string): boolean { - const args = data.split(';'); + const args = data.match(/^([^;]*);(.*)$/)?.slice(1) ?? []; if (args.length < 2) { return false; } From 0e77f839ff904d030319e8514af827247faadb05 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 21 Apr 2024 08:30:45 -0700 Subject: [PATCH 2/3] Use indexOf(';') approach --- src/common/InputHandler.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index c50c39606a..6db8751ea6 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2972,14 +2972,18 @@ export class InputHandler extends Disposable implements IInputHandler { * feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink. */ public setHyperlink(data: string): boolean { - const args = data.match(/^([^;]*);(.*)$/)?.slice(1) ?? []; - if (args.length < 2) { - return false; + // Arg parsing is special cases to support unencoded semi-colons in the URIs (#4944) + const idx = data.indexOf(';'); + if (idx === -1) { + // malformed sequence, just return as handled + return true; } - if (args[1]) { - return this._createHyperlink(args[0], args[1]); + const id = data.slice(0, idx).trim(); + const uri = data.slice(idx + 1); + if (uri) { + return this._createHyperlink(id, uri); } - if (args[0].trim()) { + if (id.trim()) { return false; } return this._finishHyperlink(); From d77b41ed256f1a8410d307476a791bd1433457e7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 21 Apr 2024 08:38:12 -0700 Subject: [PATCH 3/3] Add tests to cover OSC 8 hyperlinks --- src/common/InputHandler.test.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 8f9a988f14..0f17a3e69a 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -12,11 +12,12 @@ import { Attributes, BgFlags, UnderlineStyle } from 'common/buffer/Constants'; import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test'; -import { IBufferService, ICoreService } from 'common/services/Services'; +import { IBufferService, ICoreService, type IOscLinkService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; import { BufferService } from 'common/services/BufferService'; import { CoreService } from 'common/services/CoreService'; +import { OscLinkService } from 'common/services/OscLinkService'; function getCursor(bufferService: IBufferService): number[] { @@ -59,6 +60,7 @@ describe('InputHandler', () => { let bufferService: IBufferService; let coreService: ICoreService; let optionsService: MockOptionsService; + let oscLinkService: IOscLinkService; let inputHandler: TestInputHandler; beforeEach(() => { @@ -66,8 +68,9 @@ describe('InputHandler', () => { bufferService = new BufferService(optionsService); bufferService.resize(80, 30); coreService = new CoreService(bufferService, new MockLogService(), optionsService); + oscLinkService = new OscLinkService(bufferService); - inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService()); + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockCoreMouseService(), new MockUnicodeService()); }); describe('SL/SR/DECIC/DECDC', () => { @@ -1982,6 +1985,32 @@ describe('InputHandler', () => { assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]); stack.length = 0; }); + it('8: hyperlink with id', async () => { + await inputHandler.parseP('\x1b]8;id=100;http://localhost:3000\x07'); + assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0); + assert.deepStrictEqual( + oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId), + { + id: '100', + uri: 'http://localhost:3000' + } + ); + await inputHandler.parseP('\x1b]8;;\x07'); + assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0); + }); + it('8: hyperlink with semi-colon', async () => { + await inputHandler.parseP('\x1b]8;;http://localhost:3000;abc=def\x07'); + assert.notStrictEqual(inputHandler.curAttrData.extended.urlId, 0); + assert.deepStrictEqual( + oscLinkService.getLinkData(inputHandler.curAttrData.extended.urlId), + { + id: undefined, + uri: 'http://localhost:3000;abc=def' + } + ); + await inputHandler.parseP('\x1b]8;;\x07'); + assert.strictEqual(inputHandler.curAttrData.extended.urlId, 0); + }); it('104: restore events', async () => { const stack: IColorEvent[] = []; inputHandler.onColor(ev => stack.push(ev));