import { app, ipcMain, shell } from 'electron' import * as fs from 'fs/promises' import * as path from 'path' import { createLogger } from '../services/logger' import { IPC_CHANNELS } from '../../shared/ipc-channels' import { ValidationError } from '../types/errors' import { withErrorHandling, type IpcResult } from './index' const log = createLogger('FileHandler') function getAllowedRoots(): string[] { return [path.resolve(app.getAppPath()), path.resolve(app.getPath('userData'))] } export function isPathWithinAllowedRoots(inputPath: string, roots: string[]): boolean { const normalized = path.resolve(inputPath) return roots.some((root) => { const rel = path.relative(root, normalized) return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel)) }) } function normalizeAndValidatePath(inputPath: string): string { const normalized = path.resolve(inputPath) const isAllowed = isPathWithinAllowedRoots(normalized, getAllowedRoots()) if (!isAllowed) { throw new ValidationError('Path is outside allowed roots', 'VAL_INVALID_INPUT') } return normalized } export function registerFileHandlers(): void { ipcMain.handle( IPC_CHANNELS.FILE_READ, async (_event, filePath: string): Promise> => { return withErrorHandling(async () => { const safePath = normalizeAndValidatePath(filePath) log.debug('Reading file', { filePath: safePath }) return await fs.readFile(safePath, 'utf-8') }, 'file:read') } ) ipcMain.handle( IPC_CHANNELS.FILE_WRITE, async (_event, filePath: string, content: string): Promise> => { return withErrorHandling(async () => { const safePath = normalizeAndValidatePath(filePath) log.debug('Writing file', { filePath: safePath }) const dir = path.dirname(safePath) await fs.mkdir(dir, { recursive: true }) await fs.writeFile(safePath, content, 'utf-8') }, 'file:write') } ) ipcMain.handle( IPC_CHANNELS.FILE_EXISTS, async (_event, filePath: string): Promise> => { return withErrorHandling(async () => { const safePath = normalizeAndValidatePath(filePath) try { await fs.access(safePath) return true } catch { return false } }, 'file:exists') } ) ipcMain.handle( IPC_CHANNELS.FILE_LIST, async (_event, dirPath: string): Promise> => { return withErrorHandling(async () => { const safePath = normalizeAndValidatePath(dirPath) log.debug('Listing directory', { dirPath: safePath }) const entries = await fs.readdir(safePath, { withFileTypes: true }) return entries .filter((entry) => entry.isFile()) .map((entry) => entry.name) .sort() }, 'file:list') } ) ipcMain.handle( IPC_CHANNELS.FILE_OPEN_PATH, async (_event, filePath: string): Promise> => { return withErrorHandling(async () => { const safePath = normalizeAndValidatePath(filePath) log.debug('Opening path in explorer', { filePath: safePath }) await fs.access(safePath) await shell.openPath(safePath) }, 'file:openPath') } ) }