Skip to content
Merged
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
250 changes: 249 additions & 1 deletion apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import { resolveAttachmentRefs, resolveIngestItems } from '../attachment-ingest.js';
import {
type AttachmentSnapshotInput,
resolveAttachmentRefs,
resolveIngestItems,
resolvePickedAttachments,
sniffPickedAttachmentMimeType,
} from '../attachment-ingest.js';
import { createAttachmentApprovalRegistry } from '../attachment-approval.js';
import { attachmentKindFromMimeType } from '@maka/core/attachments';

describe('resolveIngestItems (pre-read validation)', () => {
test('rejects more than 8 items before touching approvals or stat', async () => {
Expand Down Expand Up @@ -235,6 +242,169 @@ describe('resolveIngestItems (pre-read validation)', () => {
});

describe('resolveAttachmentRefs', () => {
test('uses PDF magic bytes instead of a spoofed PNG extension', async () => {
const dir = await mkdtemp(join(tmpdir(), 'att-sniff-'));
const path = join(dir, 'report.png');
await writeFile(path, Buffer.from('%PDF-1.4\nfixture'));
let resizeCalls = 0;
let captured: AttachmentSnapshotInput | undefined;
try {
await resolveAttachmentRefs({
files: [{ path, size: 16 }],
resizeImage: async (bytes) => {
resizeCalls += 1;
return bytes;
},
snapshot: async (input) => {
captured = input;
return {
kind: input.attachmentKind,
name: input.name,
mimeType: input.mimeType,
bytes: input.content.byteLength,
ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'attachment-1' },
};
},
});
assert.equal(resizeCalls, 0);
assert.equal(captured?.mimeType, 'application/pdf');
assert.equal(captured?.attachmentKind, 'pdf');
assert.equal(captured?.artifactKind, 'pdf');
} finally {
await rm(dir, { recursive: true, force: true });
}
});

test('recognises a PDF whose header sits behind a preamble, not as an ordinary file', async () => {
// A PDF may carry bytes before %PDF-; readers scan the first ~1 KiB for it.
// Missing it here would route the file to `other` and let a downstream text
// read decode binary as UTF-8 — so it must still resolve as a PDF.
const content = Buffer.concat([Buffer.alloc(64), Buffer.from('%PDF-1.7\nfixture')]);
let captured: AttachmentSnapshotInput | undefined;

await resolveAttachmentRefs({
files: [{ name: 'notes.txt', size: content.byteLength, content }],
snapshot: async (input) => {
captured = input;
return {
kind: input.attachmentKind,
name: input.name,
mimeType: input.mimeType,
bytes: input.content.byteLength,
ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'attachment-1' },
};
},
});

assert.equal(captured?.mimeType, 'application/pdf');
assert.equal(captured?.attachmentKind, 'pdf');
assert.equal(captured?.artifactKind, 'pdf');
});

test('uses PNG magic bytes instead of conflicting renderer metadata', async () => {
const content = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
let resizeCalls = 0;
let captured: AttachmentSnapshotInput | undefined;

await resolveAttachmentRefs({
files: [{ name: 'report.pdf', mimeType: 'application/pdf', size: content.byteLength, content }],
resizeImage: async (bytes) => {
resizeCalls += 1;
return bytes;
},
snapshot: async (input) => {
captured = input;
return {
kind: input.attachmentKind,
name: input.name,
mimeType: input.mimeType,
bytes: input.content.byteLength,
ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'attachment-1' },
};
},
});

assert.equal(resizeCalls, 1);
assert.equal(captured?.mimeType, 'image/png');
assert.equal(captured?.attachmentKind, 'image');
assert.equal(captured?.artifactKind, 'image');
});

test('recognises an image with no extension from its bytes', async () => {
const content = Buffer.from([0xff, 0xd8, 0xff, 0xe0]);
let captured: AttachmentSnapshotInput | undefined;

await resolveAttachmentRefs({
files: [{ name: 'clipboard-image', size: content.byteLength, content }],
snapshot: async (input) => {
captured = input;
return {
kind: input.attachmentKind,
name: input.name,
mimeType: input.mimeType,
bytes: input.content.byteLength,
ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'attachment-1' },
};
},
});

assert.equal(captured?.mimeType, 'image/jpeg');
assert.equal(captured?.attachmentKind, 'image');
});

test('updates the MIME when image resizing changes the encoded format', async () => {
const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0]);
const resizedPng = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
let captured: AttachmentSnapshotInput | undefined;

await resolveAttachmentRefs({
files: [{ name: 'photo.jpg', size: jpeg.byteLength, content: jpeg }],
resizeImage: async () => resizedPng,
snapshot: async (input) => {
captured = input;
return {
kind: input.attachmentKind,
name: input.name,
mimeType: input.mimeType,
bytes: input.content.byteLength,
ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'attachment-1' },
};
},
});

assert.equal(captured?.mimeType, 'image/png');
assert.deepEqual(captured?.content, resizedPng);
});

test('does not decode unknown bytes merely because the renderer calls them an image', async () => {
const content = Buffer.from('not an image');
let resizeCalls = 0;
let captured: AttachmentSnapshotInput | undefined;

await resolveAttachmentRefs({
files: [{ name: 'payload.svg', mimeType: 'image/svg+xml', size: content.byteLength, content }],
resizeImage: async (bytes) => {
resizeCalls += 1;
return bytes;
},
snapshot: async (input) => {
captured = input;
return {
kind: input.attachmentKind,
name: input.name,
mimeType: input.mimeType,
bytes: input.content.byteLength,
ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'attachment-1' },
};
},
});

assert.equal(resizeCalls, 0);
assert.equal(captured?.mimeType, 'application/octet-stream');
assert.equal(captured?.attachmentKind, 'other');
assert.equal(captured?.artifactKind, 'file');
});

test('rejects a path grown beyond the cap before creating a Host artifact', async () => {
const dir = await mkdtemp(join(tmpdir(), 'att-cap-'));
const path = join(dir, 'grew.bin');
Expand All @@ -258,3 +428,81 @@ describe('resolveAttachmentRefs', () => {
}
});
});

describe('sniffPickedAttachmentMimeType (pick-time staging kind)', () => {
test('stages a real image named .pdf as an image so it previews and notices', async () => {
const dir = await mkdtemp(join(tmpdir(), 'att-pick-'));
const path = join(dir, 'report.pdf');
await writeFile(path, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
try {
const mimeType = await sniffPickedAttachmentMimeType(path, 'report.pdf');
assert.equal(mimeType, 'image/png');
assert.equal(attachmentKindFromMimeType(mimeType, 'report.pdf'), 'image');
} finally {
await rm(dir, { recursive: true, force: true });
}
});

test('stages a real PDF named .png as a pdf, and unknown bytes as an ordinary file', async () => {
const dir = await mkdtemp(join(tmpdir(), 'att-pick-'));
const pdfPath = join(dir, 'report.png');
const junkPath = join(dir, 'payload.png');
await writeFile(pdfPath, Buffer.from('%PDF-1.4\nfixture'));
await writeFile(junkPath, Buffer.from('not an image'));
try {
assert.equal(await sniffPickedAttachmentMimeType(pdfPath, 'report.png'), 'application/pdf');
const junk = await sniffPickedAttachmentMimeType(junkPath, 'payload.png');
assert.equal(junk, 'application/octet-stream');
assert.equal(attachmentKindFromMimeType(junk, 'payload.png'), 'other');
} finally {
await rm(dir, { recursive: true, force: true });
}
});

test('a read failure downgrades a claimed image/PDF name rather than trusting it', async () => {
// The prefix read failing must not reinstate the extension-based image/PDF
// claim resolveAttachmentMimeType exists to reject: staging stays unblocked
// but the spoofed kind is dropped (the send path re-reads and confirms).
const spoofed = await sniffPickedAttachmentMimeType('/no/such/file.pdf', 'file.pdf');
assert.equal(spoofed, 'application/octet-stream');
assert.equal(attachmentKindFromMimeType(spoofed, 'file.pdf'), 'other');
// A non-image/PDF claim still resolves, so genuine document kinds still stage.
const docx = await sniffPickedAttachmentMimeType('/no/such/file.docx', 'file.docx');
assert.equal(
docx,
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
);
});
});

describe('resolvePickedAttachments (pick dialog owner: stage by content)', () => {
test('stamps each picked path with its sniffed MIME and main-side size, not its extension', async () => {
const dir = await mkdtemp(join(tmpdir(), 'att-pick-plan-'));
const imageAsPdf = join(dir, 'report.pdf'); // real PNG bytes behind a .pdf name
const junkAsPng = join(dir, 'payload.png'); // non-image bytes behind an image name
await writeFile(imageAsPdf, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
await writeFile(junkAsPng, Buffer.from('not an image'));
const statCalls: string[] = [];
try {
const plan = await resolvePickedAttachments([imageAsPdf, junkAsPng], async (path) => {
statCalls.push(path);
return { size: path.endsWith('report.pdf') ? 8 : 12 };
});

// report.pdf carries image bytes → stages as an image (thumbnail + notice).
assert.equal(plan[0].name, 'report.pdf');
assert.equal(plan[0].mimeType, 'image/png');
assert.equal(attachmentKindFromMimeType(plan[0].mimeType, plan[0].name), 'image');
assert.equal(plan[0].size, 8);

// A disguised .png loses its image claim → stages as an ordinary file.
assert.equal(plan[1].mimeType, 'application/octet-stream');
assert.equal(attachmentKindFromMimeType(plan[1].mimeType, plan[1].name), 'other');

// Sizes come from the injected (main-side) stat, one call per path.
assert.deepEqual(statCalls, [imageAsPdf, junkAsPng]);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});
98 changes: 94 additions & 4 deletions apps/desktop/src/main/__tests__/attachment-preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
type AttachmentPreviewResult,
} from '../attachment-preview.js';

const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);

function fakeRenderPreview(bytes: Uint8Array): Promise<{ bytes: Uint8Array; mimeType: string } | null> {
return Promise.resolve({ bytes, mimeType: 'image/png' });
Expand Down Expand Up @@ -82,7 +82,7 @@ describe('staged attachment preview (approval source)', () => {
assert.equal(read, 0);
});

it('refuses non-image approvals without touching the file', async () => {
it('reads non-image approvals once to verify their content before refusing them', async () => {
const approvals = createAttachmentApprovalRegistry();
const [issued] = approvals.issueApprovals(1, [{ path: '/tmp/notes.md', name: 'notes.md', size: 4 }]);
let read = 0;
Expand All @@ -92,12 +92,102 @@ describe('staged attachment preview (approval source)', () => {
approvals,
readFile: async () => {
read += 1;
return PNG;
return Buffer.from('text');
},
renderPreview: fakeRenderPreview,
});
assert.deepEqual(result, { ok: false, reason: 'not_image' });
assert.equal(read, 0);
assert.equal(read, 1);
});

it('previews real image bytes even when the file name claims PDF', async () => {
const approvals = createAttachmentApprovalRegistry();
const [issued] = approvals.issueApprovals(1, [
{ path: '/tmp/report.pdf', name: 'report.pdf', mimeType: 'application/pdf', size: PNG.byteLength },
]);
let renderCalls = 0;
const result = await loadApprovalPreview({
senderId: 1,
approvalId: issued.approvalId,
approvals,
readFile: async () => PNG,
renderPreview: async (bytes) => {
renderCalls += 1;
return { bytes, mimeType: 'image/png' };
},
});

assert.deepEqual(result, {
ok: true,
base64: Buffer.from(PNG).toString('base64'),
mimeType: 'image/png',
});
assert.equal(renderCalls, 1);
});

it('does not send PDF bytes with a spoofed PNG name to the image decoder', async () => {
const pdf = Buffer.from('%PDF-1.4\nfixture');
const approvals = createAttachmentApprovalRegistry();
const [issued] = approvals.issueApprovals(1, [
{ path: '/tmp/report.png', name: 'report.png', size: pdf.byteLength },
]);
let renderCalls = 0;
const result = await loadApprovalPreview({
senderId: 1,
approvalId: issued.approvalId,
approvals,
readFile: async () => pdf,
renderPreview: async () => {
renderCalls += 1;
return null;
},
});

assert.deepEqual(result, { ok: false, reason: 'not_image' });
assert.equal(renderCalls, 0);
});

it('does not send unknown bytes with a spoofed PNG name to the image decoder', async () => {
const unknown = Buffer.from('not an image');
const approvals = createAttachmentApprovalRegistry();
const [issued] = approvals.issueApprovals(1, [
{ path: '/tmp/payload.png', name: 'payload.png', size: unknown.byteLength },
]);
let renderCalls = 0;
const result = await loadApprovalPreview({
senderId: 1,
approvalId: issued.approvalId,
approvals,
readFile: async () => unknown,
renderPreview: async () => {
renderCalls += 1;
return null;
},
});

assert.deepEqual(result, { ok: false, reason: 'not_image' });
assert.equal(renderCalls, 0);
});

it('uses the sniffed MIME when returning original image bytes as the preview', async () => {
const webp = Buffer.from('RIFF0000WEBPVP8 ', 'ascii');
const approvals = createAttachmentApprovalRegistry();
const [issued] = approvals.issueApprovals(1, [
{ path: '/tmp/photo.png', name: 'photo.png', size: webp.byteLength },
]);
const result = await loadApprovalPreview({
senderId: 1,
approvalId: issued.approvalId,
approvals,
readFile: async () => webp,
renderPreview: async () => null,
});

assert.deepEqual(result, {
ok: true,
base64: webp.toString('base64'),
mimeType: 'image/webp',
});
});

it('registers the client-owned attachment preview IPC boundary', async () => {
Expand Down
Loading