mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(core): handle malformed projects.json in ProjectRegistry (#26885)
This commit is contained in:
@@ -374,4 +374,65 @@ describe('ProjectRegistry', () => {
|
||||
|
||||
readFileSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('recovers gracefully if registry is an empty object (invalid schema)', async () => {
|
||||
// 1. Write an empty object which is valid JSON but invalid schema
|
||||
fs.writeFileSync(registryPath, '{}');
|
||||
|
||||
const registry = new ProjectRegistry(registryPath);
|
||||
await registry.initialize();
|
||||
|
||||
// 2. It should not crash and should allow adding new projects
|
||||
const projectPath = path.join(tempDir, 'my-project');
|
||||
const shortId = await registry.getShortId(projectPath);
|
||||
|
||||
expect(shortId).toBe('my-project');
|
||||
|
||||
// 3. Verify it healed the file
|
||||
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
||||
expect(data.projects).toBeDefined();
|
||||
expect(data.projects[normalizePath(projectPath)]).toBe('my-project');
|
||||
});
|
||||
|
||||
it('recovers gracefully if registry projects property is an array (invalid schema)', async () => {
|
||||
// 1. Write an object where 'projects' is an array
|
||||
fs.writeFileSync(registryPath, JSON.stringify({ projects: [] }));
|
||||
|
||||
const registry = new ProjectRegistry(registryPath);
|
||||
await registry.initialize();
|
||||
|
||||
// 2. It should reset and allow adding new projects correctly
|
||||
const projectPath = path.join(tempDir, 'my-project');
|
||||
const shortId = await registry.getShortId(projectPath);
|
||||
|
||||
expect(shortId).toBe('my-project');
|
||||
|
||||
// 3. Verify it healed the file to an object
|
||||
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
||||
expect(data.projects).toBeDefined();
|
||||
expect(Array.isArray(data.projects)).toBe(false);
|
||||
expect(data.projects[normalizePath(projectPath)]).toBe('my-project');
|
||||
});
|
||||
|
||||
it('recovers gracefully if registry contains malicious slugs (path traversal)', async () => {
|
||||
// 1. Write a registry with a path traversal slug
|
||||
fs.writeFileSync(
|
||||
registryPath,
|
||||
JSON.stringify({ projects: { '/some/path': '../../etc/passwd' } }),
|
||||
);
|
||||
|
||||
const registry = new ProjectRegistry(registryPath);
|
||||
await registry.initialize();
|
||||
|
||||
// 2. It should identify as invalid and reset
|
||||
const projectPath = path.join(tempDir, 'my-project');
|
||||
const shortId = await registry.getShortId(projectPath);
|
||||
|
||||
expect(shortId).toBe('my-project');
|
||||
|
||||
// 3. Verify it healed the file and didn't preserve the malicious entry
|
||||
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
||||
expect(data.projects[normalizePath(projectPath)]).toBe('my-project');
|
||||
expect(Object.values(data.projects)).not.toContain('../../etc/passwd');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { lock } from 'proper-lockfile';
|
||||
import { z } from 'zod';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { isNodeError } from '../utils/errors.js';
|
||||
|
||||
@@ -16,6 +17,10 @@ export interface RegistryData {
|
||||
projects: Record<string, string>;
|
||||
}
|
||||
|
||||
const registryDataSchema = z.object({
|
||||
projects: z.record(z.string(), z.string().regex(/^[a-z0-9-]+$/)),
|
||||
});
|
||||
|
||||
const PROJECT_ROOT_FILE = '.project_root';
|
||||
const LOCK_TIMEOUT_MS = 10000;
|
||||
const LOCK_RETRY_DELAY_MS = 100;
|
||||
@@ -57,8 +62,16 @@ export class ProjectRegistry {
|
||||
private async loadData(): Promise<RegistryData> {
|
||||
try {
|
||||
const content = await fs.promises.readFile(this.registryPath, 'utf8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(content);
|
||||
const parsed: unknown = JSON.parse(content);
|
||||
|
||||
if (this.isValidRegistryData(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
debugLogger.warn(
|
||||
`Project registry at ${this.registryPath} has an invalid schema, resetting to empty.`,
|
||||
);
|
||||
return { projects: {} };
|
||||
} catch (error: unknown) {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
return { projects: {} }; // Normal first run
|
||||
@@ -407,4 +420,8 @@ export class ProjectRegistry {
|
||||
.replace(/^-|-$/g, '') || 'project'
|
||||
);
|
||||
}
|
||||
|
||||
private isValidRegistryData(data: unknown): data is RegistryData {
|
||||
return registryDataSchema.safeParse(data).success;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user