import { app } from 'electron'; import * as fsp from 'fs/promises'; import * as path from 'path'; export const DEFAULT_USER_DATA_SUBDIRS = [ 'server', 'direct-messages', 'plugins', 'plugin-bundles', 'plugin-cache', 'themes', 'metoyou' ] as const; export function isPathInside(parentPath: string, candidatePath: string): boolean { const relativePath = path.relative(parentPath, candidatePath); return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); } async function realpathOrSelf(filePath: string): Promise { try { return await fsp.realpath(filePath); } catch { return path.resolve(filePath); } } function normalizeAllowedSubdirs(allowedSubdirs: readonly string[]): string[] { return allowedSubdirs.map((entry) => entry.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')).filter(Boolean); } export async function assertPathUnderRoot( rootPath: string, candidatePath: string, allowedSubdirs: readonly string[] = DEFAULT_USER_DATA_SUBDIRS ): Promise { if (typeof candidatePath !== 'string' || !candidatePath.trim()) { throw new Error('Invalid file path'); } const [realRoot, realCandidate] = await Promise.all([realpathOrSelf(rootPath), realpathOrSelf(candidatePath)]); if (!isPathInside(realRoot, realCandidate)) { throw new Error('Path is outside allowed app-data paths'); } const relativePath = path.relative(realRoot, realCandidate).replace(/\\/g, '/'); const [topLevelSegment] = relativePath.split('/'); if (!topLevelSegment || !normalizeAllowedSubdirs(allowedSubdirs).includes(topLevelSegment)) { throw new Error('Path is outside allowed app-data paths'); } return realCandidate; } export async function assertPathUnderUserData( candidatePath: string, allowedSubdirs: readonly string[] = DEFAULT_USER_DATA_SUBDIRS ): Promise { return assertPathUnderRoot(app.getPath('userData'), candidatePath, allowedSubdirs); } const grantedPluginReadRoots = new Set(); export function grantPluginReadRoot(rootPath: string): void { if (typeof rootPath !== 'string' || !rootPath.trim()) { return; } grantedPluginReadRoots.add(path.resolve(rootPath)); } export function clearGrantedPluginReadRoots(): void { grantedPluginReadRoots.clear(); } async function assertPathUnderGrantedPluginRoot(candidatePath: string): Promise { const realCandidate = await realpathOrSelf(candidatePath); for (const rootPath of grantedPluginReadRoots) { const realRoot = await realpathOrSelf(rootPath); if (isPathInside(realRoot, realCandidate)) { return realCandidate; } } throw new Error('Path is outside allowed app-data paths'); } /** Resolves readable paths under app data or user-granted plugin source roots. */ export async function resolveReadablePath(candidatePath: string): Promise { if (typeof candidatePath !== 'string' || !candidatePath.trim()) { return null; } try { return await assertPathUnderUserData(candidatePath); } catch { try { return await assertPathUnderGrantedPluginRoot(candidatePath); } catch { return null; } } }