Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion core/src/shared/room-share.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,23 @@ export function parseRoomShare(text) {
if (at < 0) return undefined
const start = raw.indexOf('{', at)
if (start < 0) return undefined
// A chat line's text is a JSON string inside the object, so a brace in what someone typed is a
// character, not structure: the scan tracks strings and escapes, or one `{` would end the object
// early and drop the whole envelope (#855 reads the same tail this way).
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 }
Expand Down
39 changes: 39 additions & 0 deletions core/test/room-share-braces.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// A room-share envelope is one JSON object after a marker line, and a chat line's text is a JSON
// string inside it: `{` and `}` in that text are ordinary characters, not structure. The scan that
// finds the object's end must therefore know when it is inside a string (and inside an escape), the
// same way the social plugin's own tail reader does (#855) — otherwise one brace in what someone
// typed drops the whole envelope.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { encodeRoomShare, parseRoomShare } from '../src/shared/room-share.js'

const line = (text) => parseRoomShare(encodeRoomShare({ op: 'line', roomId: 'r1', seq: 7, who: 'Alice', text }))

test('a line whose text opens a brace is read back, not dropped', () => {
const parsed = line('if (x) { y()')
assert.equal(parsed?.op, 'line')
assert.equal(parsed?.roomId, 'r1')
assert.equal(parsed?.text, 'if (x) { y()')
})

test('a line whose text closes a brace is read back, not dropped', () => {
assert.equal(line('so the closing brace is }')?.text, 'so the closing brace is }')
})

test('a brace right after an escaped quote is still inside the string', () => {
assert.equal(line('say "{" now')?.text, 'say "{" now')
})

test('a copied history line keeps braces too', () => {
const wire = encodeRoomShare({ op: 'join', roomId: 'abc123', seq: 3, who: 'Alice', history: [{ who: 'Bob', text: 'map { a: 1 }' }] })
assert.equal(parseRoomShare(wire)?.history?.[0]?.text, 'map { a: 1 }')
})

test('plain text and balanced braces keep their current reading', () => {
assert.equal(line('plain text')?.text, 'plain text')
assert.equal(line('a {b} c')?.text, 'a {b} c')
})

test('a truncated envelope is still no envelope', () => {
assert.equal(parseRoomShare('FLYBOT-ROOM-SHARE\n{"v":1,"op":"line","roomId":"r1"'), undefined)
})
14 changes: 13 additions & 1 deletion patches/ds-bot/files/src/shared/room-share.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,23 @@ export function parseRoomShare(text) {
if (at < 0) return undefined
const start = raw.indexOf('{', at)
if (start < 0) return undefined
// A chat line's text is a JSON string inside the object, so a brace in what someone typed is a
// character, not structure: the scan tracks strings and escapes, or one `{` would end the object
// early and drop the whole envelope (#855 reads the same tail this way).
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 }
Expand Down
39 changes: 39 additions & 0 deletions patches/ds-bot/files/test/room-share-braces.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// A room-share envelope is one JSON object after a marker line, and a chat line's text is a JSON
// string inside it: `{` and `}` in that text are ordinary characters, not structure. The scan that
// finds the object's end must therefore know when it is inside a string (and inside an escape), the
// same way the social plugin's own tail reader does (#855) — otherwise one brace in what someone
// typed drops the whole envelope.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { encodeRoomShare, parseRoomShare } from '../src/shared/room-share.js'

const line = (text) => parseRoomShare(encodeRoomShare({ op: 'line', roomId: 'r1', seq: 7, who: 'Alice', text }))

test('a line whose text opens a brace is read back, not dropped', () => {
const parsed = line('if (x) { y()')
assert.equal(parsed?.op, 'line')
assert.equal(parsed?.roomId, 'r1')
assert.equal(parsed?.text, 'if (x) { y()')
})

test('a line whose text closes a brace is read back, not dropped', () => {
assert.equal(line('so the closing brace is }')?.text, 'so the closing brace is }')
})

test('a brace right after an escaped quote is still inside the string', () => {
assert.equal(line('say "{" now')?.text, 'say "{" now')
})

test('a copied history line keeps braces too', () => {
const wire = encodeRoomShare({ op: 'join', roomId: 'abc123', seq: 3, who: 'Alice', history: [{ who: 'Bob', text: 'map { a: 1 }' }] })
assert.equal(parseRoomShare(wire)?.history?.[0]?.text, 'map { a: 1 }')
})

test('plain text and balanced braces keep their current reading', () => {
assert.equal(line('plain text')?.text, 'plain text')
assert.equal(line('a {b} c')?.text, 'a {b} c')
})

test('a truncated envelope is still no envelope', () => {
assert.equal(parseRoomShare('FLYBOT-ROOM-SHARE\n{"v":1,"op":"line","roomId":"r1"'), undefined)
})
Loading