mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 03:08:20 +08:00
fix(paths): a project at a Windows drive root indexes its files (#2931)
A project at the root of a drive, such as a mapped network drive Z:\, a
subst drive or a UNC share root, indexed nothing on Windows: every file
was refused as "Path traversal blocked", and code reads under the root
were refused too. The containment check appended a separator to the root
before comparing, and a root such as C:\ already ends with one, so the
prefix became C:\\ and no path under the root matched. The dead code
report's default file reader had its own copy of the same check, so it
treated every file of such a project as unreadable.
The check now adds a separator only when the root doesn't already end
with one. The dead code reader uses the shared lexical containment check
instead of its own copy.
Validation on Windows 11:
- subst R: onto a two-file fixture, then codegraph init -y --force R:\:
main indexes 0 files ("all 2 files had errors", 2 "Path traversal
blocked"), and validatePathWithinRoot(R:\, a.ts) is null. The fix
indexes both files (6 nodes, 6 edges) and getCode returns both
functions' source. The mapping was removed after each run.
- Dead code report against one R:\ index: main's reader drops the unused
function as unreadable; the fix reports it.
- Tests: 3 new tests fail on main's source and pass with the fix: a file
under the filesystem root (this drive's root here, / on POSIX), path
math under Q:\ and \\server\share\, and read-only realpaths under C:\.
Reported in PR #415 by @Kamilslaoui93; this reimplements the fix
independently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0187366697
commit
f93e89a82d
@@ -100,10 +100,10 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type CodeGraph from '../index';
|
||||
import type { EdgeKind, Node, NodeKind } from '../types';
|
||||
import { isTestFile } from '../search/query-utils';
|
||||
import { lexicalPathWithinRoot } from '../utils';
|
||||
|
||||
// =============================================================================
|
||||
// Caps and defaults
|
||||
@@ -706,11 +706,13 @@ export function buildDeadCodeReport(cg: CodeGraph, query: DeadCodeQuery = {}): D
|
||||
* on disk and this module should not be the thing that trusts it.
|
||||
*/
|
||||
function defaultSourceReader(cg: CodeGraph): (filePath: string) => string | null {
|
||||
const root = path.resolve(cg.getProjectRoot());
|
||||
const root = cg.getProjectRoot();
|
||||
return (filePath: string): string | null => {
|
||||
try {
|
||||
const absolute = path.resolve(root, filePath);
|
||||
if (absolute !== root && !absolute.startsWith(root + path.sep)) return null;
|
||||
// The shared containment check, which also holds for a project at a
|
||||
// drive root: appending a separator to `C:\` refused every file (#415).
|
||||
const absolute = lexicalPathWithinRoot(root, filePath);
|
||||
if (absolute === null) return null;
|
||||
const stat = fs.statSync(absolute);
|
||||
if (!stat.isFile() || stat.size > MAX_CORROBORATION_BYTES) return null;
|
||||
return fs.readFileSync(absolute, 'utf8');
|
||||
|
||||
+14
-12
@@ -82,19 +82,28 @@ export function isConfigLeafNode(node: { kind: string; language?: string }): boo
|
||||
return node.kind === 'constant' && !!node.language && CONFIG_LEAF_LANGUAGES.has(node.language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `child` is `parent` or lies under it, compared exactly as given.
|
||||
*
|
||||
* A root (`C:\`, `\\server\share\`, `/`) already ends with its separator, so
|
||||
* none is added to it. Adding one made `C:\\` the prefix, which no path under
|
||||
* the root starts with, so a project at a drive root indexed nothing (#415).
|
||||
*/
|
||||
function isSameOrUnder(child: string, parent: string): boolean {
|
||||
if (child === parent) return true;
|
||||
return child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `child` is `parent` itself or sits underneath it. Case-insensitive on
|
||||
* Windows — NTFS is case-insensitive, and realpathSync can hand back a different
|
||||
* case than the lexical root, which would otherwise false-reject a valid file.
|
||||
*/
|
||||
function isWithinDir(child: string, parent: string): boolean {
|
||||
let c = child;
|
||||
let p = parent;
|
||||
if (process.platform === 'win32') {
|
||||
c = c.toLowerCase();
|
||||
p = p.toLowerCase();
|
||||
return isSameOrUnder(child.toLowerCase(), parent.toLowerCase());
|
||||
}
|
||||
return c === p || c.startsWith(p + path.sep);
|
||||
return isSameOrUnder(child, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,13 +217,6 @@ function foldPathCase(p: string): string {
|
||||
return process.platform === 'win32' || process.platform === 'darwin' ? p.toLowerCase() : p;
|
||||
}
|
||||
|
||||
/** Whether `child` is `parent` or lies under it. Both are already folded. */
|
||||
function isSameOrUnder(child: string, parent: string): boolean {
|
||||
if (child === parent) return true;
|
||||
// A root (`C:\`, `/`) already ends with its separator.
|
||||
return child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* The plain spelling of a Win32 namespace path: `\\?\C:\x` and `\\.\C:\x` are
|
||||
* `C:\x`, and `\\?\UNC\host\share` is `\\host\share`. `path.resolve` keeps the
|
||||
|
||||
Reference in New Issue
Block a user