chore: add more tests for allowed folder paths (#9679)

This commit is contained in:
Shelby
2026-03-02 16:11:58 -08:00
committed by GitHub
parent 08fcd55f35
commit 62e4e77d32
3 changed files with 176 additions and 18 deletions

View File

@@ -266,7 +266,33 @@ export function unescapeForwardSlash(str: string): string {
});
}
export const normalizeFolderPath = (p: string) => path.normalize(p).replace(/[/\\]+$/, '');
export const normalizeFolderPath = (p: string) => {
const normalized = path.normalize(p);
// Preserve filesystem roots as-is (e.g. "/" on POSIX, "C:\" on Windows)
if (normalized === path.parse(normalized).root) {
return normalized;
}
return normalized.replace(/[/\\]+$/, '');
};
export type FolderValidationResult =
| { ok: true; normalizedValue: string }
| { ok: false; error: string };
export function validateFolderInput(input: string, existing: string[]): FolderValidationResult {
const trimmed = input.trim();
if (trimmed === '') {
return { ok: false, error: 'Enter a folder path to add.' };
}
const normalized = normalizeFolderPath(trimmed);
if (trimmed !== normalized) {
return { ok: false, error: `Invalid folder path format. Did you mean "${normalized}"?` };
}
if (existing.some(v => normalizeFolderPath(v) === normalized)) {
return { ok: false, error: 'Duplicate folders are not allowed.' };
}
return { ok: true, normalizedValue: normalized };
}
export function cannotAccessPathError(accessingPath: string): string {
return process.type === 'renderer' || process.type === 'browser'

View File

@@ -1,18 +1,29 @@
import { describe, expect, it } from 'vitest';
import { normalizeFolderPath } from '../../../common/misc';
import { normalizeFolderPath, validateFolderInput } from '../../../common/misc';
const isWindows = process.platform === 'win32';
describe('normalizeFolderPath', () => {
describe.skipIf(isWindows)('POSIX paths', () => {
it.each([
// root is preserved as-is
{ input: '/', expected: '/' },
{ input: '///', expected: '/' },
// trailing slash removal
{ input: '/Users/foo/bar', expected: '/Users/foo/bar' },
{ input: '/Users/foo/bar/', expected: '/Users/foo/bar' },
{ input: '/Users/foo/bar///', expected: '/Users/foo/bar' },
// duplicate separator collapse
{ input: '/Users//foo//bar', expected: '/Users/foo/bar' },
// real-world paths
{ input: '/Volumes/External/data/', expected: '/Volumes/External/data' },
{ input: '/Applications/Insomnia.app/Contents/', expected: '/Applications/Insomnia.app/Contents' },
{ input: '/Users/名前/docs', expected: '/Users/名前/docs' },
{ input: '/Users/my folder/docs', expected: '/Users/my folder/docs' },
// ".." and "./" resolution (format error fires in the UI before storing)
{ input: '/Users/foo/../bar', expected: '/Users/bar' },
{ input: './relative/path', expected: 'relative/path' },
])('normalizes "$input" to "$expected"', ({ input, expected }) => {
expect(normalizeFolderPath(input)).toBe(expected);
});
@@ -20,6 +31,10 @@ describe('normalizeFolderPath', () => {
describe.runIf(isWindows)('Windows paths', () => {
it.each([
// root is preserved as-is
{ input: 'C:\\', expected: 'C:\\' },
{ input: 'C:\\\\', expected: 'C:\\' },
// trailing backslash removal
{ input: 'C:\\Users\\foo\\bar', expected: 'C:\\Users\\foo\\bar' },
{ input: 'C:\\Users\\foo\\bar\\', expected: 'C:\\Users\\foo\\bar' },
{ input: 'C:\\Users\\foo\\bar\\\\\\', expected: 'C:\\Users\\foo\\bar' },
@@ -28,4 +43,131 @@ describe('normalizeFolderPath', () => {
expect(normalizeFolderPath(input)).toBe(expected);
});
});
it('does not throw on a path longer than 4096 characters', () => {
expect(() => normalizeFolderPath('/' + 'a'.repeat(4096))).not.toThrow();
});
it('is case sensitive', () => {
expect(normalizeFolderPath('/Users/foo')).not.toBe(normalizeFolderPath('/Users/FOO'));
});
});
describe('validateFolderInput', () => {
describe('empty input', () => {
it('returns error for empty string', () => {
expect(validateFolderInput('', [])).toEqual({ ok: false, error: 'Enter a folder path to add.' });
});
it('returns error for whitespace-only input (trimmed to empty)', () => {
expect(validateFolderInput(' ', [])).toEqual({ ok: false, error: 'Enter a folder path to add.' });
});
});
describe.skipIf(isWindows)('format validation (POSIX)', () => {
it('suggests normalized form when input has a trailing slash', () => {
expect(validateFolderInput('/Users/foo/', [])).toEqual({
ok: false,
error: 'Invalid folder path format. Did you mean "/Users/foo"?',
});
});
it('suggests normalized form when input has ".." segments', () => {
expect(validateFolderInput('/Users/foo/../bar', [])).toEqual({
ok: false,
error: 'Invalid folder path format. Did you mean "/Users/bar"?',
});
});
it('suggests normalized form when input has a "./" prefix', () => {
expect(validateFolderInput('./relative/path', [])).toEqual({
ok: false,
error: 'Invalid folder path format. Did you mean "relative/path"?',
});
});
it('format error takes priority over duplicate error', () => {
// "/Users/foo/" !== "/Users/foo", so format check fires before duplicate check
expect(validateFolderInput('/Users/foo/', ['/Users/foo'])).toEqual({
ok: false,
error: 'Invalid folder path format. Did you mean "/Users/foo"?',
});
});
});
describe.runIf(isWindows)('format validation (Windows)', () => {
it('suggests normalized form when input has a trailing backslash', () => {
expect(validateFolderInput('C:\\Users\\foo\\', [])).toEqual({
ok: false,
error: 'Invalid folder path format. Did you mean "C:\\Users\\foo"?',
});
});
it('suggests normalized form when input has ".." segments', () => {
expect(validateFolderInput('C:\\Users\\foo\\..\\bar', [])).toEqual({
ok: false,
error: 'Invalid folder path format. Did you mean "C:\\Users\\bar"?',
});
});
it('format error takes priority over duplicate error', () => {
expect(validateFolderInput('C:\\Users\\foo\\', ['C:\\Users\\foo'])).toEqual({
ok: false,
error: 'Invalid folder path format. Did you mean "C:\\Users\\foo"?',
});
});
});
describe.skipIf(isWindows)('duplicate detection (POSIX)', () => {
it('returns duplicate error when the exact path is already in the list', () => {
expect(validateFolderInput('/Users/foo', ['/Users/foo'])).toEqual({
ok: false,
error: 'Duplicate folders are not allowed.',
});
});
it('returns no error when the path is not in the list', () => {
expect(validateFolderInput('/Users/bar', ['/Users/foo'])).toEqual({
ok: true,
normalizedValue: '/Users/bar',
});
});
it('returns no error when the list is empty', () => {
expect(validateFolderInput('/Users/foo', [])).toEqual({
ok: true,
normalizedValue: '/Users/foo',
});
});
});
describe.runIf(isWindows)('duplicate detection (Windows)', () => {
it('returns duplicate error when the exact path is already in the list', () => {
expect(validateFolderInput('C:\\Users\\foo', ['C:\\Users\\foo'])).toEqual({
ok: false,
error: 'Duplicate folders are not allowed.',
});
});
it('returns no error when the path is not in the list', () => {
expect(validateFolderInput('C:\\Users\\bar', ['C:\\Users\\foo'])).toEqual({
ok: true,
normalizedValue: 'C:\\Users\\bar',
});
});
it('returns no error when the list is empty', () => {
expect(validateFolderInput('C:\\Users\\foo', [])).toEqual({
ok: true,
normalizedValue: 'C:\\Users\\foo',
});
});
});
it.skipIf(isWindows)('trims leading and trailing whitespace before processing', () => {
expect(validateFolderInput(' /Users/docs ', [])).toEqual({
ok: true,
normalizedValue: '/Users/docs',
});
});
});

View File

@@ -4,7 +4,7 @@ import { ListBox, ListBoxItem } from 'react-aria-components';
import { useRootLoaderData } from '~/root';
import { invariant } from '~/utils/invariant';
import { normalizeFolderPath } from '../../../common/misc';
import { validateFolderInput } from '../../../common/misc';
import type { SettingsOfType } from '../../../common/settings';
import { useSettingsPatcher } from '../../hooks/use-request';
import { PromptButton } from '../base/prompt-button';
@@ -30,22 +30,12 @@ export const TextArraySetting: FC<{
}
const onAddDataFolder = useCallback(async () => {
const validValue = folderToAdd ? folderToAdd.trim() : '';
if (validValue === '') {
setValidationError('Enter a folder path to add.');
const result = validateFolderInput(folderToAdd, currentValue);
if (!result.ok) {
setValidationError(result.error);
return;
}
const normalizedValue = normalizeFolderPath(validValue);
if (validValue !== normalizedValue) {
setValidationError(`Invalid path format. Did you mean "${normalizedValue}"?`);
return;
}
const exists = currentValue.some(v => normalizeFolderPath(v) === normalizedValue);
if (exists) {
setValidationError('Duplicate folders are not allowed.');
return;
}
const updatedValue = [...currentValue, validValue];
const updatedValue = [...currentValue, result.normalizedValue];
patchSettings({ [setting]: updatedValue });
setFolderToAdd('');
setValidationError('');
@@ -115,7 +105,7 @@ export const TextArraySetting: FC<{
confirmMessage=""
doneMessage=""
onClick={() => onDeleteDataFolder(dataFolderPath)}
title="Delete cookie"
title="Delete folder"
>
<i className="fa fa-trash-o" />
</PromptButton>