mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
fix: file viewer opens audio/svg/binary like the attachments viewer
The File Browser preview and Attachments preview share openFilePreview(), but the workspace branch (via /file-content) misclassified several types the attachments viewer handled fine: - SVG was reported as type:image, but file-raw serves SVG as octet-stream + attachment (XSS hardening), so the <img> broke. Now fetched and rendered via a same-origin image/svg+xml blob <img> (safe; <img> never runs SVG scripts). file-raw's SVG hardening is unchanged. - Audio (mp3/wav/ogg/m4a/aac/flac/opus) was type:binary -> "Cannot preview". Now classified as audio and rendered with <audio controls>; file-raw gained the matching audio/video MIME types so playback works. - Binary formats not in the hardcoded list (xlsx/doc/zip/...) were decoded as UTF-8 and dumped as mojibake. Replaced the static list with a NUL-byte content sniff that flags arbitrary binaries; the binary fallback now offers a Download link instead of dead-ending. Adds route tests for audio, known-binary (xlsx), and NUL-sniff classification. Verified end-to-end on an isolated instance + headless browser. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -215,6 +215,50 @@ describe('file-routes', () => {
|
||||
expect(body.data.url).toContain('file-raw');
|
||||
});
|
||||
|
||||
it('returns audio metadata for audio files', async () => {
|
||||
mockedStat.mockResolvedValue({ size: 2048 } as never);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/file-content?path=clip.mp3`,
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.type).toBe('audio');
|
||||
expect(body.data.url).toContain('file-raw');
|
||||
});
|
||||
|
||||
it('flags known-binary extensions (e.g. xlsx) instead of dumping mojibake', async () => {
|
||||
mockedStat.mockResolvedValue({ size: 4096 } as never);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/file-content?path=sheet.xlsx`,
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.type).toBe('binary');
|
||||
expect(body.data.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sniffs NUL bytes and flags binary content for unknown extensions', async () => {
|
||||
const binary = Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x00, 0x01, 0x02]);
|
||||
mockedReadFile.mockResolvedValue(binary as never);
|
||||
mockedStat.mockResolvedValue({ size: binary.length } as never);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/sessions/${harness.ctx._sessionId}/file-content?path=mystery.dat`,
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.data.type).toBe('binary');
|
||||
expect(body.data.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects path traversal attempts', async () => {
|
||||
// realpathSync resolves the symlink to a path outside workingDir
|
||||
mockedRealpathSync.mockReturnValue('/etc/passwd' as never);
|
||||
|
||||
Reference in New Issue
Block a user