Files
orca/src/main/git/worktree-symlink-detection.test.ts
T
NeilandNeil ad760c8b92 fix(worktree): reject Windows drive-qualified shared paths in the symlink guard (#17793)
getSafeRelativePath strips leading `/` and `\` before testing absoluteness, so
the only rooted spelling that can still reach the guard is a Windows drive
designator. It tested that with the host `path.isAbsolute`, which left two gaps:
the drive-absolute form `C:/payload` was refused on Windows but admitted as an
ordinary relative filename on macOS/Linux, and the drive-RELATIVE form
`C:payload` was admitted on every host including Windows, where
`win32.resolve(root, 'C:payload')` discards the worktree root and lands under
C:'s current directory. That holds for a drive root (`D:\wt`) and for the
`\\wsl.localhost\<Distro>\...` UNC root a WSL project uses, both verified.

The value reaches the guard from two configs: the per-user Worktree Shared Paths
setting alone on the create path (createWorktreeLinkedPaths, called with
`repo.symlinkPaths` from orca-runtime.ts:27820 and worktree-remote.ts:2636), and
that setting merged with the repo's checked-in `orca.yaml`
`worktree.sharedDirectories` on the removal and detection paths
(getWorktreeSharedLinkPaths). No repo config is required to reach it.

Replace both `isAbsolute` calls with a `/^[a-zA-Z]:/` test, verified by fuzz to
be a strict superset of `posix.isAbsolute || win32.isAbsolute` for every
post-strip input. No filesystem escape is closed on macOS/Linux, where such an
entry resolves to a literal in-worktree filename.

Cost: on POSIX, `:` is a legal filename character, so a shared/linked path whose
first segment is `<letter>:...` is now refused where it previously worked — it
stops being created, and if a worktree already holds an Orca-created symlink
there it stops being excluded from the untracked-file filters in all four
findExistingWorktreeSymlinkPaths callers, which means a refused non-force
worktree removal (remove-registered-local-worktree.ts:91, orca-runtime.ts:30205),
a phantom untracked row in Source Control (status-read.ts:90), and a blocked
hosted-review creation (hosted-review-creation-git-state.ts:290) — and
removeWorktreeLinkedPaths no longer unlinks it, so nothing cleans it up.
Accepted because a per-host verdict would defeat the point of judging the same
config identically on every host it is evaluated on.

Co-authored-by: Neil <neil@example.com>
2026-08-31 20:32:23 -07:00

84 lines
3.9 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { findExistingWorktreeSymlinkPaths, getSafeRelativePath } from './worktree-symlink-detection'
describe('getSafeRelativePath', () => {
// The only case in this file that binds the production change: every one of
// these is admitted by at least one host's `path.isAbsolute` — the
// drive-relative spellings are admitted by *every* host's, Windows included.
it('rejects Windows drive-qualified entries, including drive-relative, on any host', () => {
expect(getSafeRelativePath('C:\\Windows\\Temp')).toEqual({ safe: false })
expect(getSafeRelativePath('C:/Windows/Temp')).toEqual({ safe: false })
expect(getSafeRelativePath(' d:\\payload ')).toEqual({ safe: false })
// Drive-RELATIVE: `win32.resolve('D:\\wt', 'C:payload')` discards the
// worktree root and lands under C:'s current directory.
expect(getSafeRelativePath('C:payload')).toEqual({ safe: false })
expect(getSafeRelativePath('c:')).toEqual({ safe: false })
})
it('keeps colon-bearing paths that are not drive-qualified', () => {
expect(getSafeRelativePath('build:out')).toEqual({ safe: true, rel: 'build:out' })
expect(getSafeRelativePath('logs/2026-08-31T10:00.txt')).toEqual({
safe: true,
rel: 'logs/2026-08-31T10:00.txt'
})
})
// These pin the rest of the guard expression, which this commit rewrote:
// both `isAbsolute` calls were deleted as provably subsumed by the strip
// above plus the drive check, so their inputs must still be judged the same.
it('still strips leading separators of either flavour and keeps the remainder relative', () => {
expect(getSafeRelativePath('node_modules')).toEqual({ safe: true, rel: 'node_modules' })
expect(getSafeRelativePath(' .env ')).toEqual({ safe: true, rel: '.env' })
expect(getSafeRelativePath('/.env')).toEqual({ safe: true, rel: '.env' })
expect(getSafeRelativePath('\\.env')).toEqual({ safe: true, rel: '.env' })
expect(getSafeRelativePath('//srv/share/x')).toEqual({ safe: true, rel: 'srv/share/x' })
})
it('still rejects empty, whitespace-only, and parent-directory entries', () => {
expect(getSafeRelativePath('')).toEqual({ safe: false })
expect(getSafeRelativePath(' ')).toEqual({ safe: false })
expect(getSafeRelativePath('../secrets')).toEqual({ safe: false })
expect(getSafeRelativePath('safe/../../escape')).toEqual({ safe: false })
expect(getSafeRelativePath('..\\escape')).toEqual({ safe: false })
expect(getSafeRelativePath('foo\\..\\..\\escape')).toEqual({ safe: false })
})
})
describe('findExistingWorktreeSymlinkPaths', () => {
let root: string
let primary: string
let worktree: string
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-symlink-detection-'))
primary = join(root, 'primary')
worktree = join(root, 'worktree')
mkdirSync(primary)
mkdirSync(worktree)
})
afterEach(() => {
rmSync(root, { recursive: true, force: true })
})
// Why skipped on Windows rather than made platform-independent: the fixture
// needs a real on-disk directory named `C:`, which only POSIX allows. The
// Windows half of this guard is bound by the unit tests above, which run
// everywhere because they never touch the filesystem.
const posixIt = process.platform === 'win32' ? it.skip : it
posixIt('does not report a drive-qualified entry even when the literal path exists', async () => {
mkdirSync(join(worktree, 'C:'))
writeFileSync(join(primary, 'payload'), 'X=1\n')
symlinkSync(join(primary, 'payload'), join(worktree, 'C:', 'payload'))
symlinkSync(join(primary, 'payload'), join(worktree, 'C:payload'))
await expect(
findExistingWorktreeSymlinkPaths(worktree, ['C:/payload', 'C:\\payload', 'C:payload'])
).resolves.toEqual([])
})
})