From 375b3bed22d1481da6be6aceb85cf01e99bd7b7e Mon Sep 17 00:00:00 2001 From: zy95-12 <135135297+zy95-12@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:39:09 +0000 Subject: [PATCH] fix(room-share): count braces outside JSON strings only parseRoomShare found the end of the envelope by counting every `{` and `}`, so a brace a person typed in a chat line closed the object early, JSON.parse failed and the whole line was dropped. An open brace in `if (x) { y()`, or a lone `}` at the end of a dict, lost the message. Skip string literals while scanning, with backslash escapes honoured, so the braces JSON.stringify left raw inside `text` stay data. Fixes #2447 Docs-not-needed: no owned path or contract changes; the parser now reads the envelope the envelope itself describes. --- .../2447-room-share-brace-in-text.fixed.md | 1 + core/src/shared/room-share.js | 13 ++++++- core/test/room-share-brace-in-text.test.js | 34 +++++++++++++++++++ patches/ds-bot/files/src/shared/room-share.js | 13 ++++++- .../test/room-share-brace-in-text.test.js | 34 +++++++++++++++++++ 5 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2447-room-share-brace-in-text.fixed.md create mode 100644 core/test/room-share-brace-in-text.test.js create mode 100644 patches/ds-bot/files/test/room-share-brace-in-text.test.js 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) +})