Skip to content

Commit 061652e

Browse files
committed
fix: prevent crashes from corrupted JSON files in storage
- Add null byte/control character detection in storage read/update - Wrap JSON.parse in try-catch with descriptive error messages - Make stats command resilient to corrupted files by catching errors - Fixes 'JSON Parse error: Unterminated string' crashes in stats command
1 parent 23848ed commit 061652e

File tree

2 files changed

+26
-8
lines changed

2 files changed

+26
-8
lines changed

packages/opencode/src/cli/cmd/stats.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,13 @@ async function getAllSessions(): Promise<Session.Info[]> {
8686
const sessions: Session.Info[] = []
8787

8888
const projectKeys = await Storage.list(["project"])
89-
const projects = await Promise.all(projectKeys.map((key) => Storage.read<Project.Info>(key)))
89+
const projects = await Promise.all(projectKeys.map((key) => Storage.read<Project.Info>(key).catch(() => undefined)))
9090

9191
for (const project of projects) {
9292
if (!project) continue
9393

9494
const sessionKeys = await Storage.list(["session", project.id])
95-
const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key)))
95+
const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key).catch(() => undefined)))
9696

9797
for (const session of projectSessions) {
9898
if (session) {

packages/opencode/src/storage/storage.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,17 @@ export namespace Storage {
174174
if (!content.trim()) {
175175
throw new NotFoundError({ message: `Empty file: ${target}` })
176176
}
177-
const result = JSON.parse(content)
178-
return result as T
177+
const hasControlCharacters = /[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(content)
178+
if (hasControlCharacters) {
179+
throw new NotFoundError({ message: `Corrupted file detected: ${target}` })
180+
}
181+
try {
182+
const result = JSON.parse(content)
183+
return result as T
184+
} catch (e) {
185+
const message = e instanceof Error ? e.message : String(e)
186+
throw new NotFoundError({ message: `Failed to parse JSON from ${target}: ${message}` })
187+
}
179188
})
180189
}
181190

@@ -188,10 +197,19 @@ export namespace Storage {
188197
if (!content.trim()) {
189198
throw new NotFoundError({ message: `Empty file: ${target}` })
190199
}
191-
const parsed = JSON.parse(content)
192-
fn(parsed)
193-
await Bun.write(target, JSON.stringify(parsed, null, 2))
194-
return parsed as T
200+
const hasControlCharacters = /[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(content)
201+
if (hasControlCharacters) {
202+
throw new NotFoundError({ message: `Corrupted file detected: ${target}` })
203+
}
204+
try {
205+
const parsed = JSON.parse(content)
206+
fn(parsed)
207+
await Bun.write(target, JSON.stringify(parsed, null, 2))
208+
return parsed as T
209+
} catch (e) {
210+
const message = e instanceof Error ? e.message : String(e)
211+
throw new NotFoundError({ message: `Failed to parse JSON from ${target}: ${message}` })
212+
}
195213
})
196214
}
197215

0 commit comments

Comments
 (0)