Skip to content

Commit 797b42c

Browse files
committed
fix: prevent binary attachment corruption (LF to CRLF conversion)
When storing binary attachments via session.saveChanges(), Buffer data passed directly to FormData.append() was being treated as text, causing LF (0x0A) bytes to be converted to CRLF (0x0D 0x0A) on Windows. The fix wraps the payload in a Blob with 'application/octet-stream' content type, consistent with how other binary data is handled elsewhere in the codebase (e.g., DatabaseSmuggler.ts:270). Fixes binary file corruption affecting ZIP, images, PDFs, and any file containing 0x0A bytes. Added regression test to verify all 256 byte values (0x00-0xFF) are preserved correctly through attachment storage and retrieval.
1 parent bf80fc7 commit 797b42c

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

src/Documents/Commands/Batches/SingleNodeBatchCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export class SingleNodeBatchCommand extends RavenCommand<BatchCommandResult> imp
9393
for (let i = 0; i < attachments.length; i++) {
9494
const part = attachments[i].body;
9595
const payload = part instanceof Readable ? await readToBuffer(part) : part;
96-
body.append("attachment_" + i, payload);
96+
body.append("attachment_" + i, new Blob([payload], { type: "application/octet-stream" }));
9797
}
9898
}
9999

test/Ported/Attachments/AttachmentsSessionTest.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,66 @@ describe("Attachments Session", function () {
319319
}
320320
});
321321

322+
it("binary attachment preserves all byte values including LF (0x0A)", async () => {
323+
// This test verifies that binary attachments are not corrupted.
324+
// Previously, LF bytes (0x0A) were being converted to CRLF (0x0D 0x0A)
325+
// when FormData.append() received a Buffer without Blob wrapper.
326+
327+
// Create binary data with all possible byte values (0x00 to 0xFF)
328+
const binaryData = new Uint8Array(256);
329+
for (let i = 0; i < 256; i++) {
330+
binaryData[i] = i;
331+
}
332+
const originalBuffer = Buffer.from(binaryData);
333+
334+
{
335+
const session = store.openSession();
336+
const user = new User();
337+
user.name = "BinaryTest";
338+
await session.store(user, "users/binary");
339+
session.advanced.attachments.store(
340+
user,
341+
"allbytes.bin",
342+
originalBuffer,
343+
"application/octet-stream"
344+
);
345+
await session.saveChanges();
346+
}
347+
348+
{
349+
const session = store.openSession();
350+
const result = await session.advanced.attachments.get("users/binary", "allbytes.bin");
351+
352+
let retrievedBuffer = Buffer.from([]);
353+
result.data.pipe(new Writable({
354+
write(chunk, enc, cb) {
355+
retrievedBuffer = Buffer.concat([retrievedBuffer, chunk]);
356+
cb();
357+
}
358+
}));
359+
360+
await finishedAsync(result.data);
361+
result.dispose();
362+
363+
// Size should be exactly 256 bytes
364+
assert.strictEqual(
365+
retrievedBuffer.length,
366+
originalBuffer.length,
367+
`Binary attachment corrupted: expected ${originalBuffer.length} bytes, got ${retrievedBuffer.length} bytes`
368+
);
369+
370+
// Content should be identical
371+
assert.ok(
372+
Buffer.compare(retrievedBuffer, originalBuffer) === 0,
373+
"Binary attachment content was modified during storage/retrieval"
374+
);
375+
376+
// Specifically verify byte 0x0A (LF) was not converted to 0x0D 0x0A (CRLF)
377+
assert.strictEqual(retrievedBuffer[10], 0x0A, "Byte at index 10 should be 0x0A (LF)");
378+
assert.strictEqual(retrievedBuffer[11], 0x0B, "Byte at index 11 should be 0x0B, not 0x0A from CRLF expansion");
379+
}
380+
});
381+
322382
it("attachment exists", async () => {
323383
{
324384
const session = store.openSession();

0 commit comments

Comments
 (0)