Skip to content

Commit 9a2df1f

Browse files
committed
Reject all Unicode control characters in v1 paths and commands
1 parent f223089 commit 9a2df1f

2 files changed

Lines changed: 9 additions & 3 deletions

File tree

‎core/plan.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ export function assertEditReply(value: unknown): asserts value is EditReply {
7272
}
7373
/** Canonical paths make duplicate checks reliable and exclude Git metadata. */
7474
export function isRepoPath(path: string): boolean {
75-
return path.length > 0 && !/[\\:\x00-\x1f\x7f]/u.test(path) &&
75+
return path.length > 0 && !/[\\:\p{Cc}]/u.test(path) &&
7676
path.split('/').every(part => part !== '' && part !== '.' && part !== '..' &&
7777
part.toLowerCase() !== '.git');
7878
}
7979

8080
/** Small literal-argv grammar, deliberately not a shell parser. Never executes. */
8181
export function commandArgv(command: string): string[] {
82-
if (/[\\\x00-\x1f\x7f]/u.test(command))
82+
if (/[\\\p{Cc}]/u.test(command))
8383
fail('command-syntax', 'Commands must contain literal arguments, not shell syntax.');
8484
const argv: string[] = [];
8585
let word = '', quote = '', started = false;
@@ -109,7 +109,7 @@ function parents(path: string): string[] {
109109
const parts = path.split('/'); return parts.slice(0, -1).map((_, i) => parts.slice(0, i + 1).join('/'));
110110
}
111111
function linkTarget(path: string, target: string, key: (path: string) => string, entries: ReadonlyMap<string, BaseEntry>): string | null {
112-
if (!target || target.startsWith('/') || /[\\:\x00-\x1f\x7f]/u.test(target)) return null;
112+
if (!target || target.startsWith('/') || /[\\:\p{Cc}]/u.test(target)) return null;
113113
const parts = path.split('/').slice(0, -1);
114114
for (const component of target.split('/')) {
115115
if (component === '' || component === '.') continue;

‎test/plan-v1.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,9 @@ it('rejects directories, colliding base leaves, and retained links through hidde
8989
p.items[0]!.files[0] = { path: 'b', kind: 'rename', renamed_from: 'a', change: 'Move' };
9090
expect(validatePlan(p, { ...context, baseEntries: [{ path: 'a', kind: 'symlink', target: 'other/../target' }, { path: 'other', kind: 'symlink', target: 'dir' }] }).errors.some(e => e.code === 'symlink-target')).toBe(true);
9191
});
92+
93+
it('rejects C1 control characters in commands and paths', () => {
94+
expect(() => commandArgv("test '\u0085'")).toThrow();
95+
const p = plan(); p.items[0]!.files[0]!.path = 'a\u0085';
96+
expect(validatePlan(p, { ...context, baseEntries: [{ path: 'a\u0085', kind: 'file' }] }).errors.length).toBeGreaterThan(0);
97+
});

0 commit comments

Comments
 (0)