Files
gbrain/test/sync-path-containment.test.ts
T

62 lines
2.5 KiB
TypeScript

/**
* #774 NAV-1/NAV-2 path-containment guard (`isWithinRoot` in sync.ts).
*
* The guard decides whether a realpath-resolved file lives inside the
* realpath-resolved git root. It backs BOTH the `--src-subpath` scope-entry
* check and the per-file TOCTOU re-check in the incremental import drain.
*
* It used to be a string prefix test (`real.startsWith(rootReal + '/')`). That
* hardcoded separator makes the guard fail CLOSED for every path on Windows,
* where `realpathSync` returns `C:\repo\file.md` and the `C:\repo/` prefix can
* never match: every file in the drain is recorded as a symlink escape and the
* sync imports nothing. These cases pin the containment semantics against the
* platform's own separator, so a future prefix-based rewrite is caught.
*
* The Windows regression itself is only OBSERVABLE on win32 — on POSIX the old
* prefix form and the current `relative()` form agree on every case here.
*/
import { describe, test, expect } from 'bun:test';
import { join, sep, parse } from 'path';
import { isWithinRoot } from '../src/commands/sync.ts';
describe('isWithinRoot path containment (#774 NAV-1/NAV-2)', () => {
const root = join(parse(process.cwd()).root, 'repo');
test('accepts the root itself', () => {
expect(isWithinRoot(root, root)).toBe(true);
});
test('accepts a direct child', () => {
expect(isWithinRoot(join(root, 'page.md'), root)).toBe(true);
});
test('accepts a nested descendant (the case a hardcoded "/" broke on win32)', () => {
expect(isWithinRoot(join(root, 'wiki', 'notes', 'page.md'), root)).toBe(true);
});
test('rejects the parent directory', () => {
expect(isWithinRoot(parse(root).dir, root)).toBe(false);
});
test('rejects a path escaping upward', () => {
expect(isWithinRoot(join(root, '..', 'outside', 'page.md'), root)).toBe(false);
});
test('rejects a sibling sharing the root as a string prefix', () => {
expect(isWithinRoot(root + '-evil', root)).toBe(false);
expect(isWithinRoot(join(root + '-evil', 'page.md'), root)).toBe(false);
});
test('rejects a sibling whose name extends the last segment', () => {
expect(isWithinRoot(join(parse(root).dir, 'repository', 'page.md'), root)).toBe(false);
});
test('containment does not depend on a hardcoded forward slash', () => {
// On win32 `sep` is '\\'; the child below is genuinely inside `root` and a
// `root + '/'` prefix test would have rejected it.
const child = `${root}${sep}sub${sep}page.md`;
expect(isWithinRoot(child, root)).toBe(true);
});
});