Skip to content
Open
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
30 changes: 30 additions & 0 deletions packages/studio-server/src/routes/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,36 @@ describe("registerFileRoutes", () => {
expect(readFileSync(join(projectDir, "index.html"), "utf-8")).toBe("after");
});

it("preserves binary bodies byte-for-byte on PUT and POST", async () => {
const projectDir = createProjectDir();
const app = new Hono();
registerFileRoutes(app, createAdapter(projectDir));

// Bytes that are invalid UTF-8 (e.g. font/image data): a UTF-8 text decode
// would replace them with U+FFFD and corrupt the file.
const binary = Uint8Array.from([0x4f, 0x54, 0x54, 0x4f, 0x00, 0x98, 0xff, 0xfe, 0x80, 0x01]);

const created = await app.request("http://localhost/projects/demo/files/assets/font.otf", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: binary,
});
expect(created.status).toBe(201);
expect(new Uint8Array(readFileSync(join(projectDir, "assets/font.otf")))).toEqual(binary);

const reversed = Uint8Array.from([...binary].reverse());
const overwritten = await app.request(
"http://localhost/projects/demo/files/assets/font.otf",
{
method: "PUT",
headers: { "Content-Type": "application/octet-stream" },
body: reversed,
},
);
expect(overwritten.status).toBe(200);
expect(new Uint8Array(readFileSync(join(projectDir, "assets/font.otf")))).toEqual(reversed);
});

it("backs up the previous file content before delete", async () => {
const projectDir = createProjectDir();
writeFileSync(join(projectDir, "index.html"), "before delete");
Expand Down
14 changes: 10 additions & 4 deletions packages/studio-server/src/routes/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1604,10 +1604,13 @@
if ("error" in res) return res.error;

ensureDir(res.absPath);
const body = await c.req.text();
// Read raw bytes: c.req.text() decodes the body as UTF-8, which corrupts
// binary uploads (fonts, images) by replacing non-UTF-8 bytes. Writing the
// raw buffer is byte-exact for text bodies too.
const body = Buffer.from(await c.req.arrayBuffer());
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
writeFileSync(res.absPath, body, "utf-8");
writeFileSync(res.absPath, body);

return c.json({
ok: true,
Expand All @@ -1627,8 +1630,11 @@
}

ensureDir(res.absPath);
const body = await c.req.text().catch(() => "");
writeFileSync(res.absPath, body, "utf-8");
const body = await c.req
.arrayBuffer()
.then((b) => Buffer.from(b))
.catch(() => Buffer.alloc(0));
writeFileSync(res.absPath, body);

Check failure

Code scanning / CodeQL

Potential file system race condition High

The file may have changed since it
was checked
.

return c.json({ ok: true, path: res.filePath }, 201);
});
Expand Down
Loading