"use strict";
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.rust = void 0;
exports.mapClippyLintSeverity = mapClippyLintSeverity;
exports.extractMinPatchedVersion = extractMinPatchedVersion;
exports.parseCargoAuditOutput = parseCargoAuditOutput;
exports.buildRustTopLevelDepIndex = buildRustTopLevelDepIndex;
exports.extractRustImportsRaw = extractRustImportsRaw;
exports.parseLcov = parseLcov;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const walk_source_files_1 = require("../analyzers/tools/walk-source-files");
const csharp_1 = require("./csharp");
const osv_1 = require("../analyzers/tools/osv");
const runner_1 = require("../analyzers/tools/runner");
const run_tests_helper_1 = require("../analyzers/tools/run-tests-helper");
const semver_bump_1 = require("../analyzers/tools/semver-bump");
const tool_registry_1 = require("../analyzers/tools/tool-registry");
/**
 * Clippy lints that indicate definite undefined behavior or memory
 * corruption. These are denied by default in clippy::correctness.
 */
const CRITICAL_CLIPPY = new Set([
    'clippy::mem_replace_with_uninit',
    'clippy::uninit_assumed_init',
    'clippy::uninit_vec',
    'clippy::transmuting_null',
    'clippy::not_unsafe_ptr_arg_deref',
    'clippy::cast_ref_to_mut',
    'clippy::invalid_atomic_ordering',
    'clippy::mut_from_ref',
    'clippy::size_of_in_element_count',
    'clippy::drop_copy',
    'clippy::drop_ref',
    'clippy::forget_copy',
    'clippy::forget_ref',
    'clippy::undropped_manually_drops',
]);
/**
 * Clippy lints that flag likely bugs / correctness issues (not UB but
 * definitely wrong). Bulk of the clippy::correctness group.
 */
const HIGH_CLIPPY = new Set([
    'clippy::absurd_extreme_comparisons',
    'clippy::bad_bit_mask',
    'clippy::cmp_nan',
    'clippy::deprecated_semver',
    'clippy::erasing_op',
    'clippy::fn_address_comparisons',
    'clippy::if_let_redundant_pattern_matching',
    'clippy::ifs_same_cond',
    'clippy::infinite_iter',
    'clippy::invalid_regex',
    'clippy::iter_next_loop',
    'clippy::iterator_step_by_zero',
    'clippy::let_underscore_lock',
    'clippy::logic_bug',
    'clippy::match_on_vec_items',
    'clippy::match_str_case_mismatch',
    'clippy::min_max',
    'clippy::mismatched_target_os',
    'clippy::modulo_one',
    'clippy::never_loop',
    'clippy::nonsensical_open_options',
    'clippy::option_env_unwrap',
    'clippy::out_of_bounds_indexing',
    'clippy::overly_complex_bool_expr',
    'clippy::panicking_unwrap',
    'clippy::possible_missing_comma',
    'clippy::reversed_empty_ranges',
    'clippy::self_assignment',
    'clippy::serde_api_misuse',
    'clippy::suspicious_splitn',
    'clippy::unit_cmp',
    'clippy::unit_hash',
    'clippy::unit_return_expecting_ord',
    'clippy::unsound_collection_transmute',
    'clippy::vec_resize_to_zero',
    'clippy::vtable_address_comparisons',
    'clippy::while_immutable_condition',
    'clippy::zst_offset',
]);
/**
 * Tier a clippy or rustc lint by its code name.
 *
 * clippy lint codes look like `clippy::needless_pass_by_value`.
 * rustc lint codes look like `unused_variables`, `dead_code`, etc.
 *
 * Most clippy lints are style / best-practice suggestions → low.
 * Only the correctness-group lints (hand-catalogued above) are
 * serious. rustc lints default to medium since `unused_*` and
 * `deprecated` are meaningful but rarely critical.
 */
function mapClippyLintSeverity(code) {
    if (!code)
        return 'low';
    if (CRITICAL_CLIPPY.has(code))
        return 'critical';
    if (HIGH_CLIPPY.has(code))
        return 'high';
    if (!code.startsWith('clippy::'))
        return 'medium'; // rustc-native lint
    return 'low'; // other clippy groups: style, perf, complexity, pedantic, nursery, cargo
}
function tierCargoMessage(msg) {
    if (!msg)
        return 'low';
    const tier = mapClippyLintSeverity(msg.code?.code);
    // rustc compile errors (no code.code) are real errors — bump from low to high.
    if (tier === 'low' && msg.level === 'error')
        return 'high';
    return tier;
}
/**
 * Extract the minimum patched semver from a RUSTSEC patched-range
 * string. cargo-audit emits requirement strings like:
 *
 *   `">=1.8.4"`            — single lower bound
 *   `">=1.8.4, <1.9.0"`    — line-restricted patch range
 *   `">= 1.8.4, < 1.9.0"`  — RUSTSEC's spaced convention
 *   `"^1.0.0"`             — caret operator
 *   `"1.8.4"`              — bare version
 *
 * Earlier revisions of `parseCargoAuditOutput` only stripped the
 * leading comparator, producing `"1.8.4, <1.9.0"` for the second
 * shape — unusable as a `cargo update --precise` argument. This
 * helper extracts the explicit `>=` floor when present (the minimum
 * patched version of that line); otherwise falls back to the first
 * semver-shaped token in the string. Surfaced by tokio@0.1.22 in
 * the cross-ecosystem benchmark fixture (Phase 10h.6.8).
 */
function extractMinPatchedVersion(patchedRange) {
    const semver = /\d+\.\d+\.\d+(?:[-+][\w.]+)?/;
    const gte = patchedRange.match(new RegExp(`>=\\s*(${semver.source})`));
    if (gte)
        return gte[1];
    const any = patchedRange.match(semver);
    return any ? any[0] : patchedRange;
}
/**
 * Map cargo-audit's textual severity to the four-tier `SeverityCounts`
 * domain. RUSTSEC uses the standard critical/high/medium/low set;
 * `informational` advisories (yanked, notice) are treated as low.
 */
function normalizeRustSeverity(s) {
    switch (s?.toLowerCase()) {
        case 'critical':
            return 'critical';
        case 'high':
            return 'high';
        case 'medium':
            return 'medium';
        default:
            return 'low';
    }
}
/**
 * Pure parser for `cargo-audit --json` output. Extracted from the gather
 * function so it can be exhaustively unit-tested without a real Cargo
 * toolchain on the dev machine (10h.5 release-time validation runs the
 * full pipeline). Returns null when the input is malformed or contains
 * no vulnerabilities object; otherwise returns counts + findings ready
 * for downstream alias-fallback enrichment.
 */
function parseCargoAuditOutput(raw) {
    let data;
    try {
        data = JSON.parse(raw);
    }
    catch {
        return null;
    }
    if (!data.vulnerabilities)
        return null;
    let critical = 0;
    let high = 0;
    let medium = 0;
    let low = 0;
    const findings = [];
    for (const v of data.vulnerabilities.list || []) {
        const adv = v.advisory;
        if (!adv?.id)
            continue;
        // Resolve severity + CVSS first so the counts bucket matches the
        // per-finding severity even when CVSS promotes the classification
        // for advisories that ship only a vector.
        let severity = normalizeRustSeverity(adv.severity);
        let cvssScore = null;
        if (adv.cvss) {
            const parsed = (0, osv_1.parseCvssV3BaseScore)(adv.cvss);
            if (parsed !== null) {
                cvssScore = parsed;
                if (!adv.severity) {
                    const tier = (0, osv_1.scoreToTier)(parsed);
                    if (tier !== 'unknown')
                        severity = tier;
                }
            }
        }
        if (severity === 'critical')
            critical++;
        else if (severity === 'high')
            high++;
        else if (severity === 'medium')
            medium++;
        else
            low++;
        const finding = {
            id: adv.id,
            package: v.package?.name ?? adv.package ?? 'unknown',
            installedVersion: v.package?.version,
            tool: 'cargo-audit',
            packId: 'rust',
            severity,
        };
        if (cvssScore !== null)
            finding.cvssScore = cvssScore;
        const patched = v.versions?.patched ?? [];
        if (patched.length > 0) {
            // RUSTSEC patched entries are version *requirement* strings, often
            // ranges like ">=1.8.4, <1.9.0" — they describe an entire patched
            // version line, not a single version. Earlier revisions stripped
            // only the leading `>=`, leaving `"1.8.4, <1.9.0"` in
            // `parentVersion` which then can't be passed to `cargo update
            // --precise <X>`. Surfaced by tokio@0.1.22 in the cross-ecosystem
            // benchmark fixture (Phase 10h.6.8). Now we extract the explicit
            // `>=` floor when present (the minimum patched version), or fall
            // back to the first semver-shaped token in the string.
            const cleanFix = extractMinPatchedVersion(patched[0]);
            finding.fixedVersion = cleanFix;
            // Tier-2 structured plan (10h.6.3): Rust's dep graph is resolved
            // by cargo in one go — there's no "transitive parent" concept like
            // npm's where the fix is at a different package. The upgrade
            // target IS the vulnerable crate itself, so parent == finding
            // package. patches[] carries just this advisory's id (cargo-audit
            // doesn't bundle multi-advisory rollups the way osv-scanner does).
            // `breaking` derives from the semver-major comparison; same pre-
            // 1.x convention as the TS/Python packs.
            finding.upgradePlan = {
                parent: finding.package,
                parentVersion: cleanFix,
                patches: [adv.id],
                breaking: (0, semver_bump_1.isMajorBump)(v.package?.version ?? '', cleanFix),
            };
        }
        const aliases = (adv.aliases ?? []).filter((a) => a && a.length > 0);
        if (aliases.length > 0)
            finding.aliases = aliases;
        if (adv.title)
            finding.summary = adv.title;
        else if (adv.description)
            finding.summary = adv.description;
        const refs = [...(adv.references ?? []), adv.url].filter((u) => typeof u === 'string' && u.length > 0);
        if (refs.length > 0)
            finding.references = refs;
        else
            finding.references = [`https://rustsec.org/advisories/${adv.id}.html`];
        findings.push(finding);
    }
    return { counts: { critical, high, medium, low }, findings };
}
/**
 * Pure parser for `cargo metadata --format-version 1`, returning a
 * per-crate-name index of the top-level manifest deps that transitively
 * pull that crate. Mirrors the TS pack's `buildTsTopLevelDepIndex`
 * shape so bom + HTML renders can use identical grouping logic.
 *
 * Cargo's resolve graph uses opaque package ids (e.g.
 * `openssl 0.10.50 (registry+...)`). We map id→name via `packages[]`
 * and then BFS starting from each direct dep of `resolve.root`
 * (derived from the workspace root's `resolve.nodes[i].dependencies`).
 *
 * Returns an empty map on malformed input rather than throwing — the
 * caller keeps attribution unset so dep-vuln gather still succeeds.
 */
function buildRustTopLevelDepIndex(raw) {
    let data;
    try {
        data = JSON.parse(raw);
    }
    catch {
        return new Map();
    }
    const packages = data.packages ?? [];
    const nodes = data.resolve?.nodes ?? [];
    const rootId = data.resolve?.root;
    if (!rootId || nodes.length === 0)
        return new Map();
    const nameById = new Map();
    for (const pkg of packages) {
        if (pkg.id && pkg.name)
            nameById.set(pkg.id, pkg.name);
    }
    const depsById = new Map();
    for (const node of nodes) {
        if (node.id)
            depsById.set(node.id, node.dependencies ?? []);
    }
    const rootNode = depsById.get(rootId);
    if (!rootNode)
        return new Map();
    const result = new Map();
    for (const topId of rootNode) {
        const topName = nameById.get(topId);
        if (!topName)
            continue;
        const visited = new Set();
        const queue = [topId];
        while (queue.length > 0) {
            const id = queue.shift();
            if (visited.has(id))
                continue;
            visited.add(id);
            const name = nameById.get(id);
            if (name) {
                const bucket = result.get(name) ?? new Set();
                bucket.add(topName);
                result.set(name, bucket);
            }
            for (const childId of depsById.get(id) ?? []) {
                if (!visited.has(childId))
                    queue.push(childId);
            }
        }
    }
    const sorted = new Map();
    for (const [name, parents] of result) {
        sorted.set(name, [...parents].sort());
    }
    return sorted;
}
/**
 * Read + parse `cargo metadata --format-version 1` for the project at
 * `cwd`. Returns an empty index when cargo itself is missing or the
 * command fails — topLevelDep stays unattributed rather than blocking.
 */
function loadRustTopLevelDepIndex(cwd) {
    const raw = (0, runner_1.run)('cargo metadata --format-version 1', cwd, 60000);
    if (!raw)
        return new Map();
    return buildRustTopLevelDepIndex(raw);
}
/**
 * Single source of truth for the rust pack's dep-vuln gathering.
 * Consumed by `rustDepVulnsProvider` (capability dispatcher).
 *
 * Manifest gating: cargo-audit operates on `Cargo.lock` — without one
 * it can't enumerate resolved dependency versions. We return early
 * rather than running the tool against the wrong scope (mirrors the
 * 10h.3.3 fix in the python pack).
 *
 * cvssScore is derived from the advisory's CVSS vector when present
 * (RUSTSEC advisories vary — many ship only textual severity), with
 * resolveCvssScores supplying alias-fallback against CVE OSV records
 * for entries where cargo-audit's bundled vector is missing.
 */
async function gatherRustDepVulnsResult(cwd) {
    if (!(0, runner_1.fileExists)(cwd, 'Cargo.lock')) {
        return { kind: 'no-manifest', reason: 'no Cargo.lock — run cargo generate-lockfile first' };
    }
    const audit = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS['cargo-audit'], cwd);
    if (!audit.available || !audit.path) {
        return { kind: 'unavailable', reason: 'cargo-audit not installed' };
    }
    const raw = (0, runner_1.run)(`${audit.path} audit --json`, cwd, 60000);
    if (!raw)
        return { kind: 'unavailable', reason: 'cargo-audit produced no output' };
    const parsed = parseCargoAuditOutput(raw);
    if (!parsed)
        return { kind: 'unavailable', reason: 'cargo-audit output failed JSON parse' };
    const { counts, findings } = parsed;
    // Attach top-level attribution from cargo's resolve graph. Best-effort:
    // when `cargo metadata` is unavailable the findings keep the Tier-1
    // identity fields only.
    if (findings.length > 0) {
        const topLevelIndex = loadRustTopLevelDepIndex(cwd);
        for (const f of findings) {
            const parents = topLevelIndex.get(f.package);
            if (parents && parents.length > 0)
                f.topLevelDep = parents;
        }
    }
    // Alias-fallback CVSS pass: many RUSTSEC entries ship without a
    // CVSS vector but their CVE alias on OSV.dev carries one.
    if (findings.length > 0) {
        const cvssInputs = findings.map((f) => ({
            primaryId: f.id,
            embeddedCvss: f.cvssScore ?? null,
            aliases: f.aliases ?? [],
        }));
        const resolved = await (0, osv_1.resolveCvssScores)(cvssInputs);
        for (const f of findings) {
            const score = resolved.get(f.id);
            if (score !== null && score !== undefined)
                f.cvssScore = score;
        }
    }
    const envelope = {
        schemaVersion: 1,
        tool: 'cargo-audit',
        enrichment: null,
        counts,
        findings,
    };
    return { kind: 'success', envelope };
}
const rustDepVulnsProvider = {
    source: 'rust',
    async gather(cwd) {
        const outcome = await gatherRustDepVulnsResult(cwd);
        return outcome.kind === 'success' ? outcome.envelope : null;
    },
    async gatherOutcome(cwd) {
        return gatherRustDepVulnsResult(cwd);
    },
};
/**
 * Single source of truth for the rust pack's lint gathering.
 * Consumed by `rustLintProvider` (capability dispatcher).
 *
 * Previously, empty cargo output was silently skipped (nothing pushed
 * to toolsUsed or toolsUnavailable). This helper aligns rust with the
 * other packs: empty output = clean run with zero lint issues. Strict
 * improvement.
 */
function gatherRustLintResult(cwd) {
    const clippy = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS.clippy, cwd);
    if (!clippy.available) {
        return { kind: 'unavailable', reason: 'not installed' };
    }
    const raw = (0, runner_1.run)('cargo clippy --message-format json', cwd, 120000);
    const counts = { critical: 0, high: 0, medium: 0, low: 0 };
    for (const line of raw.split('\n')) {
        if (!line.trim())
            continue;
        try {
            const msg = JSON.parse(line);
            if (msg.reason !== 'compiler-message' || !msg.message)
                continue;
            // Upstream emits multiple levels (error, warning, note, help).
            // Skip note/help — they're context, not findings.
            if (msg.message.level !== 'error' && msg.message.level !== 'warning')
                continue;
            counts[tierCargoMessage(msg.message)]++;
        }
        catch {
            /* skip non-JSON lines */
        }
    }
    const envelope = { schemaVersion: 1, tool: 'clippy', counts };
    return { kind: 'success', envelope };
}
const rustLintProvider = {
    source: 'rust',
    async gather(cwd) {
        const outcome = gatherRustLintResult(cwd);
        return outcome.kind === 'success' ? outcome.envelope : null;
    },
};
/**
 * Single source of truth for the rust pack's coverage gathering.
 * Tries lcov first (cargo llvm-cov --lcov default), falls back to
 * cobertura XML. Consumed by `rustCoverageProvider` (capability
 * dispatcher).
 */
function gatherRustCoverageResult(cwd) {
    for (const file of ['lcov.info', 'coverage/lcov.info']) {
        const abs = path.join(cwd, file);
        let raw;
        try {
            raw = fs.readFileSync(abs, 'utf-8');
        }
        catch {
            continue;
        }
        const coverage = parseLcov(raw, file, cwd);
        if (coverage) {
            return { schemaVersion: 1, tool: `coverage:${coverage.source}`, coverage };
        }
    }
    for (const file of ['coverage.cobertura.xml', 'coverage/coverage.cobertura.xml']) {
        const abs = path.join(cwd, file);
        let raw;
        try {
            raw = fs.readFileSync(abs, 'utf-8');
        }
        catch {
            continue;
        }
        const coverage = (0, csharp_1.parseCoberturaXml)(raw, file, cwd);
        if (coverage) {
            return { schemaVersion: 1, tool: `coverage:${coverage.source}`, coverage };
        }
    }
    return null;
}
/**
 * Run `cargo llvm-cov --lcov --output-path lcov.info` from cwd (D021).
 *
 * cargo-llvm-cov drives the standard `cargo test` runner under LLVM
 * source-based coverage instrumentation, so this is the canonical
 * "test + coverage" command for any Cargo workspace. The output file
 * `lcov.info` is the first artifact `gatherRustCoverageResult` looks
 * for on the next dispatcher pass.
 *
 * Preflight:
 *   - `Cargo.toml` must exist — otherwise this isn't a Rust project
 *     and cargo's own "could not find Cargo.toml" framing is worse than
 *     a fast "skipped" outcome.
 *   - `cargo-llvm-cov` must be installed. Unlike Go's built-in
 *     `go test -cover`, Rust coverage requires the third-party subcommand;
 *     without it `cargo` would exit non-zero with "no such command:
 *     llvm-cov", which we'd classify as `failed` — misleading framing for
 *     what is really a missing tool. Route the user to the install path
 *     instead (per CLAUDE.md rule #1: tools go through the registry).
 */
function runRustTestsWithCoverage(cwd) {
    return Promise.resolve((0, run_tests_helper_1.runTestsWithCoverage)({
        pack: 'rust',
        cmd: 'cargo llvm-cov --lcov --output-path lcov.info',
        cwd,
        artifact: 'lcov.info',
        preflight: (cwd) => {
            if (!(0, runner_1.fileExists)(cwd, 'Cargo.toml')) {
                return 'no Cargo.toml in this directory — not a Rust project';
            }
            if (!(0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS['cargo-llvm-cov'], cwd).available) {
                return 'cargo-llvm-cov not installed — run `vyuh-dxkit tools install`';
            }
            return null;
        },
    }));
}
const rustCoverageProvider = {
    source: 'rust',
    async gather(cwd) {
        return gatherRustCoverageResult(cwd);
    },
    async runTests(cwd) {
        return runRustTestsWithCoverage(cwd);
    },
};
/**
 * Capture Rust `use` path specifiers from source text. Handles simple
 * paths (`use std::io;`) and grouped (`use std::{io, fs};`). Rust has
 * no file-level resolver — see note on the pack's capabilities slot —
 * so this is the only raw helper the imports capability needs.
 * Exported for unit tests.
 */
function extractRustImportsRaw(content) {
    const out = [];
    const re = /^\s*use\s+([a-zA-Z_][\w:]*(?:::\{[^}]+\})?)\s*;/gm;
    let m;
    while ((m = re.exec(content)) !== null) {
        out.push(m[1]);
    }
    return out;
}
/**
 * Enumerate .rs source files and capture the pack's per-file imports.
 * Rust has no resolveImport (the module/crate hierarchy requires parsing
 * Cargo.toml + mod declarations — out of scope), so `edges` is always
 * empty and the envelope carries only `extracted` for downstream
 * consumers that want package-level import analysis.
 */
function gatherRustImportsResult(cwd) {
    const files = (0, walk_source_files_1.walkSourceFiles)(cwd, {
        extensions: ['.rs'],
        includeTests: true,
        includeAutogen: true,
    });
    if (files.length === 0)
        return null;
    const extracted = new Map();
    for (const rel of files) {
        let content;
        try {
            content = fs.readFileSync(path.join(cwd, rel), 'utf-8');
        }
        catch {
            continue;
        }
        extracted.set(rel, extractRustImportsRaw(content));
    }
    if (extracted.size === 0)
        return null;
    return {
        schemaVersion: 1,
        tool: 'rust-imports',
        sourceExtensions: ['.rs'],
        extracted,
        edges: new Map(),
    };
}
const rustImportsProvider = {
    source: 'rust',
    async gather(cwd) {
        return gatherRustImportsResult(cwd);
    },
};
/**
 * Rust's canonical test runner is `cargo test`; any crate with a
 * `Cargo.toml` has it available by default. No deeper detection needed.
 */
function gatherRustTestFrameworkResult(cwd) {
    if (!(0, runner_1.fileExists)(cwd, 'Cargo.toml'))
        return null;
    return { schemaVersion: 1, tool: 'rust', name: 'cargo-test' };
}
const rustTestFrameworkProvider = {
    source: 'rust',
    async gather(cwd) {
        return gatherRustTestFrameworkResult(cwd);
    },
};
/**
 * Single source of truth for the rust pack's license gathering.
 * Consumed by `rustLicensesProvider` (capability dispatcher).
 *
 * cargo-license walks the Cargo.toml-resolved dep graph and emits every
 * crate's license metadata in one pass — richer than go-licenses (has
 * description + authors + version natively), simpler than the node path
 * (no per-package disk read needed). Returns null cleanly when the repo
 * isn't a Cargo workspace or the tool isn't installed.
 */
function gatherRustLicensesResult(cwd) {
    if (!(0, runner_1.fileExists)(cwd, 'Cargo.toml')) {
        return { kind: 'no-manifest', reason: 'no Cargo.toml' };
    }
    const status = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS['cargo-license'], cwd);
    if (!status.available || !status.path) {
        return { kind: 'unavailable', reason: 'cargo-license not installed' };
    }
    const raw = (0, runner_1.run)(`${status.path} --json`, cwd, 120000);
    if (!raw)
        return { kind: 'unavailable', reason: 'cargo-license produced no output' };
    let data;
    try {
        data = JSON.parse(raw);
    }
    catch (err) {
        return { kind: 'unavailable', reason: `cargo-license parse error: ${err.message}` };
    }
    if (!Array.isArray(data)) {
        return { kind: 'unavailable', reason: 'cargo-license output was not a JSON array' };
    }
    // Same self-parent invariant as the other packs: cargo metadata's
    // resolve graph seeds BFS from each direct dep, so `index[top]`
    // always contains `top`. Empty index (cargo missing, not a
    // workspace) leaves isTopLevel unset — the bom filter will pass
    // the row through rather than guess.
    const topLevelIndex = loadRustTopLevelDepIndex(cwd);
    const hasIndex = topLevelIndex.size > 0;
    const findings = [];
    for (const entry of data) {
        if (!entry.name || !entry.version)
            continue;
        const parents = hasIndex ? topLevelIndex.get(entry.name) : undefined;
        findings.push({
            package: entry.name,
            version: entry.version,
            licenseType: entry.license && entry.license.length > 0 ? entry.license : 'UNKNOWN',
            sourceUrl: entry.repository || undefined,
            description: entry.description || undefined,
            supplier: entry.authors || undefined,
            isTopLevel: hasIndex ? (parents?.includes(entry.name) ?? false) : undefined,
        });
    }
    const envelope = {
        schemaVersion: 1,
        tool: 'cargo-license',
        findings,
    };
    return { kind: 'success', envelope };
}
const rustLicensesProvider = {
    source: 'rust',
    async gather(cwd) {
        const outcome = gatherRustLicensesResult(cwd);
        return outcome.kind === 'success' ? outcome.envelope : null;
    },
    async gatherOutcome(cwd) {
        return gatherRustLicensesResult(cwd);
    },
};
function round1(n) {
    return Math.round(n * 10) / 10;
}
function parseLcov(raw, sourceFile, cwd) {
    const files = new Map();
    let totalHit = 0;
    let totalFound = 0;
    let currentFile = null;
    let fileHit = 0;
    let fileFound = 0;
    for (const line of raw.split('\n')) {
        const trimmed = line.trim();
        if (trimmed.startsWith('SF:')) {
            currentFile = trimmed.slice(3);
            fileHit = 0;
            fileFound = 0;
        }
        else if (trimmed.startsWith('LH:')) {
            fileHit = parseInt(trimmed.slice(3), 10) || 0;
        }
        else if (trimmed.startsWith('LF:')) {
            fileFound = parseInt(trimmed.slice(3), 10) || 0;
        }
        else if (trimmed === 'end_of_record' && currentFile) {
            const rel = path.isAbsolute(currentFile)
                ? path.relative(cwd, currentFile).split(path.sep).join('/')
                : currentFile;
            files.set(rel, {
                path: rel,
                covered: fileHit,
                total: fileFound,
                pct: round1(fileFound > 0 ? (fileHit / fileFound) * 100 : 0),
            });
            totalHit += fileHit;
            totalFound += fileFound;
            currentFile = null;
        }
    }
    if (files.size === 0)
        return null;
    return {
        source: 'lcov',
        sourceFile,
        linePercent: round1(totalFound > 0 ? (totalHit / totalFound) * 100 : 0),
        files,
    };
}
exports.rust = {
    id: 'rust',
    displayName: 'Rust',
    commentSyntax: { lineComment: '//', blockCommentStart: '/*', blockCommentEnd: '*/' },
    sourceExtensions: ['.rs'],
    // Rust convention: tests live in the same file via #[cfg(test)] / #[test],
    // or in a dedicated tests/ directory. Filename patterns cover the latter.
    testFilePatterns: ['*_test.rs', 'tests/*.rs'],
    extraExcludes: ['target'],
    exportDetection: {
        reliability: 'full',
        strategy: '`pub`, `pub(crate)`, `pub(super)` visibility modifiers on items',
    },
    // D027 (2.4.7): rustdoc uses outer (`///`) and inner (`//!`)
    // doc-comment markers. Both at the start of a (possibly indented)
    // line.
    docCommentPatterns: ['^[[:space:]]*///', '^[[:space:]]*//!'],
    // D034 (2.4.7): `reqwest`'s permissive opt-outs are the dominant
    // Rust TLS-bypass idiom (`Client::builder().danger_accept_invalid_certs(true)`).
    // Function names are explicitly `danger_*` — high-signal greppable
    // tokens with near-zero false-positive rate.
    tlsBypassPatterns: ['danger_accept_invalid_certs', 'danger_accept_invalid_hostnames'],
    upgradeCommand(name, version) {
        return `cargo update -p ${name} --precise ${version}`;
    },
    // Rust's build layout (`src/bin/`, `src/lib.rs`, `src/main.rs`)
    // is too generic to call "primary architecture" — every Rust
    // project has it. axum/actix/rocket web frameworks organize HTTP
    // surface under `handlers/`, `routes/`, `api/`; CLI tools and
    // standalone binaries don't, and degrade cleanly to "no primary
    // architecture detected."
    architecturalShape: {
        primaryComponentPaths: ['/handlers/', '/routes/', '/api/', '/services/'],
        routePaths: ['/handlers/', '/routes/', '/api/'],
        modelPaths: ['/models/'],
        vocabulary: {
            components: 'handlers/services',
            models: 'models',
            routes: 'routes',
        },
        testGapPriority: {
            high: ['/handlers/', '/routes/', '/services/'],
        },
    },
    clocLanguageNames: ['Rust'],
    detect(cwd) {
        return (0, runner_1.fileExists)(cwd, 'Cargo.toml');
    },
    tools: ['clippy', 'cargo-audit', 'cargo-llvm-cov', 'cargo-license'],
    // No dedicated semgrep Rust ruleset; covered by p/security-audit.
    semgrepRulesets: [],
    capabilities: {
        depVulns: rustDepVulnsProvider,
        lint: rustLintProvider,
        coverage: rustCoverageProvider,
        imports: rustImportsProvider,
        testFramework: rustTestFrameworkProvider,
        licenses: rustLicensesProvider,
    },
    mapLintSeverity: mapClippyLintSeverity,
    permissions: ['Bash(cargo test:*)', 'Bash(cargo build:*)', 'Bash(cargo clippy:*)'],
    ruleFile: 'rust.md',
    defaultVersion: 'stable',
    cliBinaries: ['rustc', 'cargo'],
    devcontainerFeature: {
        name: 'ghcr.io/devcontainers/features/rust:1',
        opts: { version: 'stable', profile: 'default' },
    },
    devcontainerExtensions: ['rust-lang.rust-analyzer'],
};
//# sourceMappingURL=rust.js.map