mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 19:49:52 +00:00
fix(core): reject binary files before reading
This commit is contained in:
parent
9a1199770b
commit
cc801089d7
3 changed files with 75 additions and 6 deletions
|
|
@ -48,6 +48,47 @@ export class BinaryFileError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
const BINARY_EXTENSIONS = new Set([
|
||||
".zip",
|
||||
".tar",
|
||||
".gz",
|
||||
".exe",
|
||||
".dll",
|
||||
".so",
|
||||
".class",
|
||||
".jar",
|
||||
".war",
|
||||
".7z",
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".odt",
|
||||
".ods",
|
||||
".odp",
|
||||
".bin",
|
||||
".dat",
|
||||
".obj",
|
||||
".o",
|
||||
".a",
|
||||
".lib",
|
||||
".wasm",
|
||||
".pyc",
|
||||
".pyo",
|
||||
])
|
||||
|
||||
export const isBinary = (resource: string, bytes: Uint8Array) => {
|
||||
if (BINARY_EXTENSIONS.has(path.extname(resource).toLowerCase())) return true
|
||||
if (bytes.length === 0) return false
|
||||
const nonPrintable = bytes.reduce(
|
||||
(count, byte) => count + (byte === 0 || byte < 9 || (byte > 13 && byte < 32) ? 1 : 0),
|
||||
0,
|
||||
)
|
||||
return bytes.includes(0) || nonPrintable / bytes.length > 0.3
|
||||
}
|
||||
|
||||
export class TextContent extends Schema.Class<TextContent>("FileSystem.TextContent")({
|
||||
type: Schema.Literal("text"),
|
||||
content: Schema.String,
|
||||
|
|
|
|||
|
|
@ -110,11 +110,11 @@ export const layer = Layer.effectDiscard(
|
|||
const final = yield* filesystem.resolveReadPath(input)
|
||||
if (final.type !== "file" || final.target.resource !== target.resource || final.target.real !== target.real)
|
||||
return yield* Effect.die(new Error("File changed after permission approval"))
|
||||
const mime = imageMime(
|
||||
yield* filesystem.readSampleResolved(final.target, FileSystem.READ_SAMPLE_BYTES),
|
||||
FSUtil.mimeType(final.target.real),
|
||||
)
|
||||
const sample = yield* filesystem.readSampleResolved(final.target, FileSystem.READ_SAMPLE_BYTES)
|
||||
const mime = imageMime(sample, FSUtil.mimeType(final.target.real))
|
||||
if (!SUPPORTED_IMAGE_MIMES.has(mime)) {
|
||||
if (FileSystem.isBinary(final.target.resource, sample))
|
||||
return yield* Effect.die(new FileSystem.BinaryFileError(final.target.resource))
|
||||
if (
|
||||
final.target.size > FileSystem.MAX_READ_BYTES ||
|
||||
input.offset !== undefined ||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const filesystem = Layer.succeed(
|
|||
type: "file" as const,
|
||||
target: new FileSystem.ReadTarget({
|
||||
real,
|
||||
resource: input.reference === undefined ? "README.md" : `${input.reference}:README.md`,
|
||||
resource: input.reference === undefined ? input.path : `${input.reference}:${input.path}`,
|
||||
size,
|
||||
dev: 1,
|
||||
}),
|
||||
|
|
@ -69,7 +69,7 @@ const filesystem = Layer.succeed(
|
|||
? Effect.succeed(
|
||||
new FileSystem.ReadTarget({
|
||||
real,
|
||||
resource: input.reference === undefined ? "README.md" : `${input.reference}:README.md`,
|
||||
resource: input.reference === undefined ? input.path : `${input.reference}:${input.path}`,
|
||||
size,
|
||||
dev: 1,
|
||||
}),
|
||||
|
|
@ -511,6 +511,34 @@ describe("ReadTool", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects unsupported binary files before reading or paging them", () =>
|
||||
Effect.gen(function* () {
|
||||
reads.length = 0
|
||||
textPageInputs.length = 0
|
||||
samples.length = 0
|
||||
allow = true
|
||||
resolveFailure = undefined
|
||||
listResolveFailure = new Error("not a directory")
|
||||
size = 4
|
||||
real = "/project/archive.dat"
|
||||
afterApproval = () => {}
|
||||
readFailure = undefined
|
||||
sample = new Uint8Array([0, 1, 2, 3])
|
||||
configEntries = []
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
expect(
|
||||
yield* registry.execute({
|
||||
sessionID,
|
||||
call: { type: "tool-call", id: "call-small-binary", name: "read", input: { path: "archive.dat" } },
|
||||
}),
|
||||
).toEqual({ type: "error", value: "Cannot read binary file: archive.dat" })
|
||||
expect(samples).toEqual([FileSystem.READ_SAMPLE_BYTES])
|
||||
expect(reads).toEqual([])
|
||||
expect(textPageInputs).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not read when the file changes after permission approval", () =>
|
||||
Effect.gen(function* () {
|
||||
assertions.length = 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue