diff --git a/changelog.d/2447-room-share-brace-in-text.fixed.md b/changelog.d/2447-room-share-brace-in-text.fixed.md new file mode 100644 index 000000000..cb8d77e11 --- /dev/null +++ b/changelog.d/2447-room-share-brace-in-text.fixed.md @@ -0,0 +1 @@ +A chat line that carries a brace — `if (x) { y()` or a lone `}` — reaches the room again instead of being dropped: the room-share parser counts braces outside JSON strings only, so one unbalanced brace no longer ends the envelope early. diff --git a/core/src/shared/room-share.js b/core/src/shared/room-share.js index 0f21da824..3433de674 100644 --- a/core/src/shared/room-share.js +++ b/core/src/shared/room-share.js @@ -255,11 +255,22 @@ export function parseRoomShare(text) { if (at < 0) return undefined const start = raw.indexOf('{', at) if (start < 0) return undefined + // #2447: braces inside a JSON string are the sender's characters, not structure. One unbalanced + // brace in a chat line used to close the envelope early and drop the whole line. let depth = 0 let end = -1 + let inString = false + let escaped = false for (let i = start; i < raw.length; i += 1) { const ch = raw[i] - if (ch === '{') depth += 1 + if (inString) { + if (escaped) escaped = false + else if (ch === '\\') escaped = true + else if (ch === '"') inString = false + continue + } + if (ch === '"') inString = true + else if (ch === '{') depth += 1 else if (ch === '}') { depth -= 1 if (depth === 0) { end = i; break } diff --git a/core/test/room-share-brace-in-text.test.js b/core/test/room-share-brace-in-text.test.js new file mode 100644 index 000000000..119580f7a --- /dev/null +++ b/core/test/room-share-brace-in-text.test.js @@ -0,0 +1,34 @@ +// A brace in a chat line is a character someone typed, not structure (#2447): `parseRoomShare` +// finds the end of the JSON envelope by counting braces, and it must not count the ones inside the +// strings `encodeRoomShare` writes — otherwise a single unbalanced brace drops the whole line. +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { encodeRoomShare, parseRoomShare } from '../src/shared/room-share.js' + +const line = (text) => encodeRoomShare({ op: 'line', roomId: 'r1', seq: 7, who: 'Alice', text }) + +test('an open brace in a line does not end the envelope early', () => { + const text = 'if (x) { y()' + const parsed = parseRoomShare(line(text)) + assert.ok(parsed, 'the envelope was dropped') + assert.equal(parsed.text, text) +}) + +test('a close brace in a line does not end the envelope early', () => { + const text = 'so the closing brace is }' + const parsed = parseRoomShare(line(text)) + assert.ok(parsed, 'the envelope was dropped') + assert.equal(parsed.text, text) +}) + +test('a brace inside a quoted word, and an escaped quote, both stay text', () => { + const text = 'he said "{" and then "}" and \\ back' + const parsed = parseRoomShare(line(text)) + assert.ok(parsed, 'the envelope was dropped') + assert.equal(parsed.text, text) +}) + +test('an envelope that really is cut short is still refused (control)', () => { + const text = line('hello there') + assert.equal(parseRoomShare(text.slice(0, text.length - 3)), undefined) +}) diff --git a/patches/ds-bot/files/src/shared/room-share.js b/patches/ds-bot/files/src/shared/room-share.js index 0f21da824..3433de674 100644 --- a/patches/ds-bot/files/src/shared/room-share.js +++ b/patches/ds-bot/files/src/shared/room-share.js @@ -255,11 +255,22 @@ export function parseRoomShare(text) { if (at < 0) return undefined const start = raw.indexOf('{', at) if (start < 0) return undefined + // #2447: braces inside a JSON string are the sender's characters, not structure. One unbalanced + // brace in a chat line used to close the envelope early and drop the whole line. let depth = 0 let end = -1 + let inString = false + let escaped = false for (let i = start; i < raw.length; i += 1) { const ch = raw[i] - if (ch === '{') depth += 1 + if (inString) { + if (escaped) escaped = false + else if (ch === '\\') escaped = true + else if (ch === '"') inString = false + continue + } + if (ch === '"') inString = true + else if (ch === '{') depth += 1 else if (ch === '}') { depth -= 1 if (depth === 0) { end = i; break } diff --git a/patches/ds-bot/files/test/room-share-brace-in-text.test.js b/patches/ds-bot/files/test/room-share-brace-in-text.test.js new file mode 100644 index 000000000..119580f7a --- /dev/null +++ b/patches/ds-bot/files/test/room-share-brace-in-text.test.js @@ -0,0 +1,34 @@ +// A brace in a chat line is a character someone typed, not structure (#2447): `parseRoomShare` +// finds the end of the JSON envelope by counting braces, and it must not count the ones inside the +// strings `encodeRoomShare` writes — otherwise a single unbalanced brace drops the whole line. +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { encodeRoomShare, parseRoomShare } from '../src/shared/room-share.js' + +const line = (text) => encodeRoomShare({ op: 'line', roomId: 'r1', seq: 7, who: 'Alice', text }) + +test('an open brace in a line does not end the envelope early', () => { + const text = 'if (x) { y()' + const parsed = parseRoomShare(line(text)) + assert.ok(parsed, 'the envelope was dropped') + assert.equal(parsed.text, text) +}) + +test('a close brace in a line does not end the envelope early', () => { + const text = 'so the closing brace is }' + const parsed = parseRoomShare(line(text)) + assert.ok(parsed, 'the envelope was dropped') + assert.equal(parsed.text, text) +}) + +test('a brace inside a quoted word, and an escaped quote, both stay text', () => { + const text = 'he said "{" and then "}" and \\ back' + const parsed = parseRoomShare(line(text)) + assert.ok(parsed, 'the envelope was dropped') + assert.equal(parsed.text, text) +}) + +test('an envelope that really is cut short is still refused (control)', () => { + const text = line('hello there') + assert.equal(parseRoomShare(text.slice(0, text.length - 3)), undefined) +})