fix(patch): cherry-pick 7837194 to release/v0.33.0-preview.5-pr-21487 to patch version v0.33.0-preview.5 and create version 0.33.0-preview.6 (#21720)

Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
This commit is contained in:
gemini-cli-robot
2026-03-09 13:59:58 -04:00
committed by GitHub
parent 508aaeda89
commit 9912ea8686
5 changed files with 55 additions and 56 deletions

View File

@@ -6,7 +6,6 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { inspect } from 'node:util';
import process from 'node:process';
import type {
@@ -135,7 +134,7 @@ import { SkillManager, type SkillDefinition } from '../skills/skillManager.js';
import { startupProfiler } from '../telemetry/startupProfiler.js';
import type { AgentDefinition } from '../agents/types.js';
import { fetchAdminControls } from '../code_assist/admin/admin_controls.js';
import { isSubpath } from '../utils/paths.js';
import { isSubpath, resolveToRealPath } from '../utils/paths.js';
import { UserHintService } from './userHintService.js';
import { WORKSPACE_POLICY_TIER } from '../policy/config.js';
import { loadPoliciesFromToml } from '../policy/toml-loader.js';
@@ -2345,17 +2344,7 @@ export class Config implements McpContext {
* @returns true if the path is allowed, false otherwise.
*/
isPathAllowed(absolutePath: string): boolean {
const realpath = (p: string) => {
let resolved: string;
try {
resolved = fs.realpathSync(p);
} catch {
resolved = path.resolve(p);
}
return os.platform() === 'win32' ? resolved.toLowerCase() : resolved;
};
const resolvedPath = realpath(absolutePath);
const resolvedPath = resolveToRealPath(absolutePath);
const workspaceContext = this.getWorkspaceContext();
if (workspaceContext.isPathWithinWorkspace(resolvedPath)) {
@@ -2363,7 +2352,7 @@ export class Config implements McpContext {
}
const projectTempDir = this.storage.getProjectTempDir();
const resolvedTempDir = realpath(projectTempDir);
const resolvedTempDir = resolveToRealPath(projectTempDir);
return isSubpath(resolvedTempDir, resolvedPath);
}

View File

@@ -24,7 +24,7 @@ vi.mock('fs', async (importOriginal) => {
});
import { Storage } from './storage.js';
import { GEMINI_DIR, homedir } from '../utils/paths.js';
import { GEMINI_DIR, homedir, resolveToRealPath } from '../utils/paths.js';
import { ProjectRegistry } from './projectRegistry.js';
import { StorageMigration } from './storageMigration.js';
@@ -279,8 +279,7 @@ describe('Storage additional helpers', () => {
name: 'custom absolute path outside throws',
customDir: '/absolute/path/to/plans',
expected: '',
expectedError:
"Custom plans directory '/absolute/path/to/plans' resolves to '/absolute/path/to/plans', which is outside the project root '/tmp/project'.",
expectedError: `Custom plans directory '/absolute/path/to/plans' resolves to '/absolute/path/to/plans', which is outside the project root '${resolveToRealPath(projectRoot)}'.`,
},
{
name: 'absolute path that happens to be inside project root',
@@ -306,8 +305,7 @@ describe('Storage additional helpers', () => {
name: 'escaping relative path throws',
customDir: '../escaped-plans',
expected: '',
expectedError:
"Custom plans directory '../escaped-plans' resolves to '/tmp/escaped-plans', which is outside the project root '/tmp/project'.",
expectedError: `Custom plans directory '../escaped-plans' resolves to '${resolveToRealPath(path.resolve(projectRoot, '../escaped-plans'))}', which is outside the project root '${resolveToRealPath(projectRoot)}'.`,
},
{
name: 'hidden directory starting with ..',

View File

@@ -510,9 +510,11 @@ describe('resolveToRealPath', () => {
expect(resolveToRealPath(input)).toBe(expected);
});
it('should return decoded path even if fs.realpathSync fails', () => {
it('should return decoded path even if fs.realpathSync fails with ENOENT', () => {
vi.spyOn(fs, 'realpathSync').mockImplementationOnce(() => {
throw new Error('File not found');
const err = new Error('File not found') as NodeJS.ErrnoException;
err.code = 'ENOENT';
throw err;
});
const p = path.resolve('path', 'to', 'New Project');
@@ -521,6 +523,25 @@ describe('resolveToRealPath', () => {
expect(resolveToRealPath(input)).toBe(expected);
});
it('should recursively resolve symlinks for non-existent child paths', () => {
const parentPath = path.resolve('/some/parent/path');
const resolvedParentPath = path.resolve('/resolved/parent/path');
const childPath = path.resolve(parentPath, 'child', 'file.txt');
const expectedPath = path.resolve(resolvedParentPath, 'child', 'file.txt');
vi.spyOn(fs, 'realpathSync').mockImplementation((p) => {
const pStr = p.toString();
if (pStr === parentPath) {
return resolvedParentPath;
}
const err = new Error('ENOENT') as NodeJS.ErrnoException;
err.code = 'ENOENT';
throw err;
});
expect(resolveToRealPath(childPath)).toBe(expectedPath);
});
});
describe('normalizePath', () => {

View File

@@ -359,8 +359,8 @@ export function isSubpath(parentPath: string, childPath: string): boolean {
* @param pathStr The path string to resolve.
* @returns The resolved real path.
*/
export function resolveToRealPath(path: string): string {
let resolvedPath = path;
export function resolveToRealPath(pathStr: string): string {
let resolvedPath = pathStr;
try {
if (resolvedPath.startsWith('file://')) {
@@ -372,11 +372,28 @@ export function resolveToRealPath(path: string): string {
// Ignore error (e.g. malformed URI), keep path from previous step
}
return robustRealpath(path.resolve(resolvedPath));
}
function robustRealpath(p: string): string {
try {
return fs.realpathSync(resolvedPath);
} catch (_e) {
// If realpathSync fails, it might be because the path doesn't exist.
// In that case, we can fall back to the path processed.
return resolvedPath;
return fs.realpathSync(p);
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
try {
const stat = fs.lstatSync(p);
if (stat.isSymbolicLink()) {
const target = fs.readlinkSync(p);
const resolvedTarget = path.resolve(path.dirname(p), target);
return robustRealpath(resolvedTarget);
}
} catch {
// Not a symlink, or lstat failed. Just resolve parent.
}
const parent = path.dirname(p);
if (parent === p) return p;
return path.join(robustRealpath(parent), path.basename(p));
}
throw e;
}
}

View File

@@ -4,10 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { isNodeError } from '../utils/errors.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { debugLogger } from './debugLogger.js';
import { resolveToRealPath } from './paths.js';
export type Unsubscribe = () => void;
@@ -227,22 +227,7 @@ export class WorkspaceContext {
* if it did exist.
*/
private fullyResolvedPath(pathToCheck: string): string {
try {
return fs.realpathSync(path.resolve(this.targetDir, pathToCheck));
} catch (e: unknown) {
if (
isNodeError(e) &&
e.code === 'ENOENT' &&
e.path &&
// realpathSync does not set e.path correctly for symlinks to
// non-existent files.
!this.isFileSymlink(e.path)
) {
// If it doesn't exist, e.path contains the fully resolved path.
return e.path;
}
throw e;
}
return resolveToRealPath(path.resolve(this.targetDir, pathToCheck));
}
/**
@@ -262,15 +247,4 @@ export class WorkspaceContext {
!path.isAbsolute(relative)
);
}
/**
* Checks if a file path is a symbolic link that points to a file.
*/
private isFileSymlink(filePath: string): boolean {
try {
return !fs.readlinkSync(filePath).endsWith('/');
} catch (_error) {
return false;
}
}
}