diff --git a/lib/utils.js b/lib/utils.js index 5627bec..6a28827 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,13 +7,24 @@ const fs = require("fs"); * whose name matches one of the entries in `name`. Stops at `parent`. * * @param {string} parent - Root of the workspace (walk stops here). - * @param {string} directory - Relative directory to start from. + * @param {string} directory - Relative directory to start from (must be within parent). * @param {string|string[]} name - File name(s) to look for. * @returns {string|null} Absolute path to the first matching file, or null. */ function findFiles(parent, directory, name) { const names = [].concat(name); - const chunks = path.resolve(parent, directory).split(path.sep); + const resolvedParent = path.resolve(parent); + const resolvedStart = path.resolve(parent, directory); + + // Guard: if the resolved start path is outside parent (e.g. cross-drive path + // on Windows produced by path.relative between different drives), there is + // nothing to find within the workspace boundary — return null immediately. + if (resolvedStart !== resolvedParent && + !resolvedStart.startsWith(resolvedParent + path.sep)) { + return null; + } + + const chunks = resolvedStart.split(path.sep); while (chunks.length) { let currentDir = chunks.join(path.sep); @@ -23,7 +34,7 @@ function findFiles(parent, directory, name) { return filePath; } } - if (parent === currentDir) { + if (resolvedParent === currentDir) { break; } chunks.pop(); diff --git a/test/unit.test.js b/test/unit.test.js index 0fb759e..8eb7b19 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -98,6 +98,19 @@ describe("findFiles", () => { assert.equal(result, null); }); + test("returns null when directory is outside parent (simulated cross-drive path)", () => { + // Simulate what happens when path.relative() returns an absolute path + // (e.g. Windows cross-drive: path.relative("C:\\ws", "D:\\other") -> "D:\\other"). + // findFiles should return null immediately without walking outside parent. + mkFile("outside-guard", "phpcs.xml"); + // Pass an absolute path as `directory` that is not within parent. + const outsideDir = path.join(tmpRoot, "outside-guard"); + const unrelatedParent = path.join(tmpRoot, "unrelated-parent"); + fs.mkdirSync(unrelatedParent, { recursive: true }); + const result = findFiles(unrelatedParent, outsideDir, "phpcs.xml"); + assert.equal(result, null); + }); + test("handles a single-segment directory (file at root)", () => { const expected = mkFile("single", "phpcs.xml"); const result = findFiles(path.join(tmpRoot, "single"), ".", "phpcs.xml");