"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PathSecurityGate = void 0;
exports.isWithinAllowlist = isWithinAllowlist;
const fs = __importStar(require("fs"));
const fsPromises = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const DEFAULT_EXCLUDES = [
    '.git',
    '.gradle',
    '.idea',
    'node_modules',
    'build',
    'out',
    'target',
    'secrets',
];
const DEFAULT_EXTENSIONS = new Set([
    '.java',
    '.kt',
    '.c',
    '.cc',
    '.cpp',
    '.cxx',
    '.h',
    '.hpp',
    '.rs',
    '.go',
    '.py',
    '.kts',
    '.gradle',
    '.mk',
    '.bp',
    '.rc',
    '.te',
    '.conf',
    '.properties',
    '.aidl',
    '.proto',
    '.xml',
]);
function parsePathList(value) {
    if (!value)
        return [];
    return value
        .split(path.delimiter)
        .map(entry => entry.trim())
        .filter(Boolean);
}
function configuredAllowlistRoots() {
    const roots = parsePathList(process.env.SMARTPERFETTO_CODEBASE_ROOTS);
    const unsafeDevRoot = process.env.SMARTPERFETTO_DEV_UNSAFE_CODEBASE_ROOT;
    if (unsafeDevRoot && process.env.NODE_ENV !== 'production') {
        roots.push(unsafeDevRoot);
    }
    return roots;
}
function safeRealpath(target) {
    try {
        return fs.realpathSync(target);
    }
    catch {
        return null;
    }
}
async function safeRealpathAsync(target) {
    try {
        return await fsPromises.realpath(target);
    }
    catch {
        return null;
    }
}
function isWithinAllowlist(target, allowlist) {
    const realTarget = safeRealpath(target);
    if (!realTarget)
        return false;
    for (const root of allowlist) {
        const realRoot = safeRealpath(root);
        if (!realRoot)
            continue;
        const rel = path.relative(realRoot, realTarget);
        if (!rel.startsWith('..') && !path.isAbsolute(rel))
            return true;
    }
    return false;
}
function shouldExclude(relativePath, basename, excludeNames) {
    if (basename.startsWith('.env'))
        return true;
    if (basename.endsWith('.log') || basename.endsWith('.bak'))
        return true;
    const parts = relativePath.split(path.sep);
    return parts.some(part => excludeNames.includes(part));
}
class PathSecurityGate {
    constructor(options = {}) {
        this.allowlistRootsOverride = options.allowlistRoots ?? null;
        this.excludeNames = options.excludeNames ?? DEFAULT_EXCLUDES;
        this.allowedExtensions = new Set(options.allowedExtensions ?? DEFAULT_EXTENSIONS);
        this.maxFileBytes = options.maxFileBytes ?? 200 * 1024;
        this.maxFiles = options.maxFiles ?? 50000;
    }
    async preview(rootPath) {
        // Read env lazily so SMARTPERFETTO_CODEBASE_ROOTS set via dotenv (loaded after
        // module init in ESM) is visible on the first real request.
        const allowlistRoots = this.allowlistRootsOverride ?? configuredAllowlistRoots();
        const rootRealpath = await safeRealpathAsync(rootPath);
        if (!rootRealpath) {
            return {
                rootPath,
                rootRealpath: '',
                acceptedFiles: [],
                skippedFiles: [],
                blocked: true,
                blockedReason: 'root_not_found',
            };
        }
        if (allowlistRoots.length === 0 || !isWithinAllowlist(rootRealpath, allowlistRoots)) {
            return {
                rootPath,
                rootRealpath,
                acceptedFiles: [],
                skippedFiles: [],
                blocked: true,
                blockedReason: 'root_outside_allowlist',
            };
        }
        const acceptedFiles = [];
        const skippedFiles = [];
        const stack = [rootRealpath];
        while (stack.length > 0) {
            const current = stack.pop();
            const entries = await fsPromises.readdir(current, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(current, entry.name);
                const realPath = await safeRealpathAsync(fullPath);
                // Detect symlinks that escape the root without calling isWithinAllowlist
                // (realPath is already resolved, so a simple relative-path check suffices).
                if (!realPath) {
                    skippedFiles.push({
                        relativePath: path.relative(rootRealpath, fullPath),
                        reason: 'symlink_outside_root',
                    });
                    continue;
                }
                const rel = path.relative(rootRealpath, realPath);
                if (rel.startsWith('..') || path.isAbsolute(rel)) {
                    skippedFiles.push({
                        relativePath: path.relative(rootRealpath, fullPath),
                        reason: 'symlink_outside_root',
                    });
                    continue;
                }
                const relativePath = rel;
                if (shouldExclude(relativePath, entry.name, this.excludeNames)) {
                    if (entry.isFile())
                        skippedFiles.push({ relativePath, reason: 'excluded' });
                    continue;
                }
                if (entry.isDirectory()) {
                    stack.push(realPath);
                    continue;
                }
                if (!entry.isFile())
                    continue;
                const ext = path.extname(entry.name);
                if (!this.allowedExtensions.has(ext)) {
                    skippedFiles.push({ relativePath, reason: 'extension_not_allowed' });
                    continue;
                }
                const stat = await fsPromises.stat(realPath);
                if (stat.size > this.maxFileBytes) {
                    skippedFiles.push({ relativePath, reason: 'file_too_large' });
                    continue;
                }
                acceptedFiles.push({ relativePath, sizeBytes: stat.size });
                if (acceptedFiles.length > this.maxFiles) {
                    return {
                        rootPath,
                        rootRealpath,
                        acceptedFiles,
                        skippedFiles,
                        blocked: true,
                        blockedReason: 'too_many_files',
                    };
                }
            }
        }
        acceptedFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
        skippedFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
        return {
            rootPath,
            rootRealpath,
            acceptedFiles,
            skippedFiles,
            blocked: false,
        };
    }
}
exports.PathSecurityGate = PathSecurityGate;
//# sourceMappingURL=pathSecurityGate.js.map