From 92b5b935fd853bd774fe7d86da428a551cfe47cf Mon Sep 17 00:00:00 2001 From: CloudCLI UI Contributors Date: Mon, 29 Jun 2026 01:44:15 +0000 Subject: [PATCH] fix(file-viewer): address review feedback on media preview - Never write preview-only or binary files: handleSave is a no-op when previewKind/isBinary, and the text buffer is cleared when switching to a media/binary file, so Cmd/Ctrl+S can no longer post a stale or empty buffer over the file on disk (CodeRabbit: Data Integrity, Critical). - Choose the fallback MIME type per file extension (single source of truth in previewableFile.ts) instead of per preview-kind, and only override when the server Content-Type is missing or generic, so webm/ogv/ogg/flac/svg render correctly (CodeRabbit: Functional Correctness, Major). - Harden the PDF iframe: validate the %PDF- magic bytes and pin the blob MIME to application/pdf so a mislabeled HTML/SVG file can't execute in the app origin. A sandbox attribute is intentionally not used because Chromium's built-in PDF viewer will not load inside any sandboxed frame (CodeRabbit: Security, Critical). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../hooks/useCodeEditorDocument.ts | 12 +++- .../code-editor/utils/previewableFile.ts | 72 +++++++++++++------ .../subcomponents/CodeEditorMediaPreview.tsx | 43 ++++++++--- 3 files changed, 97 insertions(+), 30 deletions(-) diff --git a/src/components/code-editor/hooks/useCodeEditorDocument.ts b/src/components/code-editor/hooks/useCodeEditorDocument.ts index 74f928d6..dda02887 100644 --- a/src/components/code-editor/hooks/useCodeEditorDocument.ts +++ b/src/components/code-editor/hooks/useCodeEditorDocument.ts @@ -44,13 +44,17 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume // Natively previewable media (image/pdf/audio/video) is rendered by // CodeEditorMediaPreview, so there is nothing to read as text here. + // Clear any buffer left over from a previously opened text file so a + // stray save can't write stale content over the binary file. if (getPreviewKind(file.name)) { + setContent(''); setLoading(false); return; } // Check if file is binary by extension if (isBinaryFile(file.name)) { + setContent(''); setIsBinary(true); setLoading(false); return; @@ -87,6 +91,12 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume }, [file.diffInfo, file.name, fileDiffNewString, fileDiffOldString, fileName, filePath, fileProjectId]); const handleSave = useCallback(async () => { + // Preview-only and binary files have no editable text buffer; never write + // them back (e.g. via Cmd/Ctrl+S) or we'd corrupt the file on disk. + if (previewKind || isBinaryFile(fileName)) { + return; + } + setSaving(true); setSaveError(null); @@ -120,7 +130,7 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume } finally { setSaving(false); } - }, [content, filePath, fileProjectId]); + }, [content, filePath, fileProjectId, previewKind, fileName]); const handleDownload = useCallback(() => { const blob = new Blob([content], { type: 'text/plain' }); diff --git a/src/components/code-editor/utils/previewableFile.ts b/src/components/code-editor/utils/previewableFile.ts index 464cad81..0071de4e 100644 --- a/src/components/code-editor/utils/previewableFile.ts +++ b/src/components/code-editor/utils/previewableFile.ts @@ -5,27 +5,59 @@ export type PreviewKind = 'image' | 'pdf' | 'video' | 'audio'; -// Formats browsers can decode in . -const IMAGE_EXTENSIONS = new Set([ - 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp', 'avif', 'apng', -]); +// Single source of truth: every extension the browser can preview, mapped to the +// MIME type we apply when the server response has a missing/generic Content-Type. +// The preview kind is derived from the MIME type so the two never drift apart. +// Formats browsers generally can't play (avi, mkv, flv, wmv) are intentionally +// absent and keep the binary fallback. +const EXTENSION_MIME: Record = { + // Images + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + svg: 'image/svg+xml', + webp: 'image/webp', + ico: 'image/x-icon', + bmp: 'image/bmp', + avif: 'image/avif', + apng: 'image/apng', + // PDF + pdf: 'application/pdf', + // Video + mp4: 'video/mp4', + webm: 'video/webm', + ogv: 'video/ogg', + mov: 'video/quicktime', + m4v: 'video/x-m4v', + // Audio + mp3: 'audio/mpeg', + wav: 'audio/wav', + m4a: 'audio/mp4', + aac: 'audio/aac', + flac: 'audio/flac', + opus: 'audio/opus', + oga: 'audio/ogg', + ogg: 'audio/ogg', + weba: 'audio/webm', +}; -const PDF_EXTENSIONS = new Set(['pdf']); +const extensionOf = (filename: string): string => filename.split('.').pop()?.toLowerCase() ?? ''; -// Container/codec combos broadly playable in