fix(files): let context panel open binaries into FilesView safely
Skip UTF-8 pre-read for known binary/image/PDF paths so the shared editor can show preview or cannot-preview instead of failing open or decoding binaries as text. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
co-authored by
Serhii Dziupin
parent
d79908cc9e
commit
4f30a00814
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import type { FilesAPI } from '@/lib/api/types';
|
||||||
|
import { validateContextFileOpen } from './contextFileOpenGuard';
|
||||||
|
|
||||||
|
const filesApi = (content: string): FilesAPI => ({
|
||||||
|
listDirectory: async () => ({ path: '/', entries: [] }),
|
||||||
|
readFile: async () => ({ content, path: '/x' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateContextFileOpen', () => {
|
||||||
|
test('allows known binaries through without reading text', async () => {
|
||||||
|
const files: FilesAPI = {
|
||||||
|
listDirectory: async () => ({ path: '/', entries: [] }),
|
||||||
|
readFile: async () => {
|
||||||
|
throw new Error('should not read binary as text');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(validateContextFileOpen(files, '/repo/docs/report.pdf')).resolves.toEqual({ ok: true });
|
||||||
|
await expect(validateContextFileOpen(files, '/repo/docs/report.docx')).resolves.toEqual({ ok: true });
|
||||||
|
await expect(validateContextFileOpen(files, '/repo/docs/pixel.png')).resolves.toEqual({ ok: true });
|
||||||
|
await expect(validateContextFileOpen(files, '/repo/bin/archive.zip')).resolves.toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects text payloads that look binary', async () => {
|
||||||
|
await expect(validateContextFileOpen(filesApi('%PDF-1.7\nbinary'), '/repo/mystery.bin.bak')).resolves.toEqual({
|
||||||
|
ok: false,
|
||||||
|
reason: 'binary',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows ordinary text files', async () => {
|
||||||
|
await expect(validateContextFileOpen(filesApi('hello\nworld\n'), '/repo/notes.txt')).resolves.toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,8 +6,9 @@ import { formatMessage, useI18nStore } from '@/lib/i18n/store';
|
|||||||
const t = (key: Parameters<typeof formatMessage>[1], params?: Parameters<typeof formatMessage>[2]) =>
|
const t = (key: Parameters<typeof formatMessage>[1], params?: Parameters<typeof formatMessage>[2]) =>
|
||||||
formatMessage(useI18nStore.getState().dictionary, key, params);
|
formatMessage(useI18nStore.getState().dictionary, key, params);
|
||||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||||
|
import { isBinaryFile, isImageFile, isPdfFile, looksLikeBinaryText } from '@/lib/toolHelpers';
|
||||||
|
|
||||||
export type ContextFileOpenFailureReason = 'too-large' | 'missing' | 'unreadable';
|
export type ContextFileOpenFailureReason = 'too-large' | 'missing' | 'unreadable' | 'binary';
|
||||||
|
|
||||||
export type ContextFileOpenValidationResult =
|
export type ContextFileOpenValidationResult =
|
||||||
| { ok: true }
|
| { ok: true }
|
||||||
@@ -49,9 +50,21 @@ const readFileContent = async (files: FilesAPI, path: string): Promise<string> =
|
|||||||
return response.text();
|
return response.text();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate whether a context-panel click may open a path in the shared file editor.
|
||||||
|
* Previewable/non-text binaries are allowed through so FilesView can show image/PDF
|
||||||
|
* preview or the cannot-preview empty state — never by decoding them as editable text here.
|
||||||
|
*/
|
||||||
export const validateContextFileOpen = async (files: FilesAPI, path: string): Promise<ContextFileOpenValidationResult> => {
|
export const validateContextFileOpen = async (files: FilesAPI, path: string): Promise<ContextFileOpenValidationResult> => {
|
||||||
|
if (isBinaryFile(path) || isPdfFile(path) || isImageFile(path)) {
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await readFileContent(files, path);
|
const content = await readFileContent(files, path);
|
||||||
|
if (looksLikeBinaryText(content)) {
|
||||||
|
return { ok: false, reason: 'binary' };
|
||||||
|
}
|
||||||
const lineCount = countLinesWithLimit(content, MAX_OPEN_FILE_LINES);
|
const lineCount = countLinesWithLimit(content, MAX_OPEN_FILE_LINES);
|
||||||
if (lineCount > MAX_OPEN_FILE_LINES) {
|
if (lineCount > MAX_OPEN_FILE_LINES) {
|
||||||
return { ok: false, reason: 'too-large' };
|
return { ok: false, reason: 'too-large' };
|
||||||
@@ -73,5 +86,9 @@ export const getContextFileOpenFailureMessage = (reason: ContextFileOpenFailureR
|
|||||||
return t('contextFileOpen.failure.missing');
|
return t('contextFileOpen.failure.missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reason === 'binary') {
|
||||||
|
return t('filesView.editor.cannotPreviewBinary');
|
||||||
|
}
|
||||||
|
|
||||||
return t('contextFileOpen.failure.unreadable');
|
return t('contextFileOpen.failure.unreadable');
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user