// src/detect/adapters/query-helpers.ts
import { Query } from "web-tree-sitter";
var InvalidQueryError = class extends Error {
  queryName;
  querySource;
  cause;
  constructor(queryName, querySource, cause) {
    const causeMsg = cause instanceof Error ? cause.message : String(cause);
    super(
      `[query-helpers] Invalid Tree-sitter query "${queryName}": ${causeMsg}
Query source:
${querySource}`
    );
    this.name = "InvalidQueryError";
    this.queryName = queryName;
    this.querySource = querySource;
    this.cause = cause;
  }
};
var queryCache = /* @__PURE__ */ new WeakMap();
function compileQuery(language, source, queryName) {
  let perLang = queryCache.get(language);
  if (!perLang) {
    perLang = /* @__PURE__ */ new Map();
    queryCache.set(language, perLang);
  }
  const cached = perLang.get(source);
  if (cached) return cached;
  let q;
  try {
    q = new Query(language, source);
  } catch (e) {
    throw new InvalidQueryError(queryName, source, e);
  }
  perLang.set(source, q);
  return q;
}
function runQuery(parser, source, queryText, queryName, filePath) {
  const language = parser.language;
  if (!language) {
    throw new InvalidQueryError(
      queryName,
      queryText,
      new Error("Parser has no language assigned")
    );
  }
  const query = compileQuery(language, queryText, queryName);
  const tree = parser.parse(source);
  if (!tree) return [];
  let matches;
  try {
    matches = query.matches(tree.rootNode);
  } catch (e) {
    throw new InvalidQueryError(queryName, queryText, e);
  }
  const out = [];
  for (const match of matches) {
    if (!match.captures || match.captures.length === 0) continue;
    const captures = {};
    let earliestLine = Number.POSITIVE_INFINITY;
    for (const cap of match.captures) {
      const node = cap.node;
      captures[cap.name] = node.text;
      if (node.startPosition.row + 1 < earliestLine) {
        earliestLine = node.startPosition.row + 1;
      }
    }
    out.push({
      captures,
      file: filePath,
      line: Number.isFinite(earliestLine) ? earliestLine : 1,
      queryName
    });
  }
  try {
    tree.delete();
  } catch {
  }
  return out;
}

// src/detect/adapters/tree-sitter-loader.ts
import { createHash } from "crypto";
import {
  mkdirSync,
  readdirSync,
  readFileSync,
  writeFileSync,
  renameSync,
  unlinkSync,
  lstatSync,
  chmodSync,
  utimesSync
} from "fs";
import { homedir } from "os";
import { dirname, join } from "path";
import { Language, Parser } from "web-tree-sitter";
var GrammarSHAMismatchError = class extends Error {
  language;
  expected;
  actual;
  constructor(language, expected, actual) {
    super(
      `[tree-sitter-loader] SHA-256 mismatch for grammar "${language}". Expected ${expected}, got ${actual}. REFUSING to load \u2014 see Phase 3.5 audit attack vector #3.`
    );
    this.name = "GrammarSHAMismatchError";
    this.language = language;
    this.expected = expected;
    this.actual = actual;
  }
};
var GrammarUnavailableError = class extends Error {
  language;
  cause;
  constructor(language, cause) {
    const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause) : "no cached grammar and download failed";
    super(
      `[tree-sitter-loader] Grammar for "${language}" is unavailable: ${causeMsg}. Falling back to regex introspection for files in ${language}.`
    );
    this.name = "GrammarUnavailableError";
    this.language = language;
    this.cause = cause;
  }
};
var GrammarCacheSymlinkError = class extends Error {
  cachePath;
  constructor(cachePath) {
    super(
      `[tree-sitter-loader] Refusing to load grammar \u2014 cache path "${cachePath}" is a symlink or non-regular file. (Phase 3.5 finding #3 \u2014 symlink attack vector.)`
    );
    this.name = "GrammarCacheSymlinkError";
    this.cachePath = cachePath;
  }
};
var GrammarUrlNotHttpsError = class extends Error {
  url;
  constructor(url) {
    super(
      `[tree-sitter-loader] Refusing to download grammar from non-HTTPS URL: ${url}. Only https:// URLs are accepted. (Phase 3.5 finding #3.)`
    );
    this.name = "GrammarUrlNotHttpsError";
    this.url = url;
  }
};
var GRAMMAR_MANIFEST = {
  python: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-python.wasm",
    sha256: "9056d0fb0c337810d019fae350e8167786119da98f0f282aceae7ab89ee8253b",
    version: "0.1.13"
  },
  typescript: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-typescript.wasm",
    sha256: "8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f",
    version: "0.1.13"
  },
  javascript: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-javascript.wasm",
    sha256: "63812b9e275d26851264734868d27a1656bd44a2ef6eb3e85e6b03728c595ab5",
    version: "0.1.13"
  },
  swift: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-swift.wasm",
    sha256: "41c4fdb2249a3aa6d87eed0d383081ff09725c2248b4977043a43825980ffcc7",
    version: "0.1.13"
  },
  // ----------------------------------------------------------------
  // Plan 3c Phase 7 expansion (2026-05-07):
  //
  // Six additional grammars to support the registry-verified framework
  // adapters (go-chi, rails, aspnet, spring, ktor, phoenix) plus the
  // bundled adapters in the same language families (gin/echo/fiber,
  // sinatra, etc.). All entries use the SAME pinned tree-sitter-wasms
  // version (0.1.13) as the v1 four to keep the dependency surface
  // single-source.
  //
  // SHA-256s computed 2026-05-07 via:
  //   curl -fsSL <url> | shasum -a 256
  //
  // The unpkg filename for C# uses an underscore (`c_sharp`) while the
  // TreeSitterLanguage identifier uses no separator (`csharp`); the map
  // key is the type identifier, the URL is the storage path — they do
  // NOT need to match, the same as how `python` maps to `tree-sitter-
  // python.wasm`. This is intentional and validated by the manifest
  // shape test in tree-sitter-loader-manifest.test.ts.
  // ----------------------------------------------------------------
  go: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-go.wasm",
    sha256: "9963ca89b616eaf04b08a43bc1fb0f07b85395bec313330851f1f1ead2f755b6",
    version: "0.1.13"
  },
  ruby: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-ruby.wasm",
    sha256: "93a5022855314cdb45458c7bb026a24a0ebc3a5ff6439e542e881f14dfa13a39",
    version: "0.1.13"
  },
  csharp: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-c_sharp.wasm",
    sha256: "6266a7e32d68a3459104d994dc848df15d5672b0ea8e86d327274b694f8e6991",
    version: "0.1.13"
  },
  java: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-java.wasm",
    sha256: "637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f",
    version: "0.1.13"
  },
  kotlin: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-kotlin.wasm",
    sha256: "b5cb00c8d06ed0f10f1dbe497205b437809d7e87db1f638721a8cfb30e044449",
    version: "0.1.13"
  },
  elixir: {
    url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-elixir.wasm",
    sha256: "82e91b9759ddca30d8978ebbfa8e347b4451b64c931f9ae62112e6db9b8fac20",
    version: "0.1.13"
  }
};
function getCacheDir() {
  return process.env.MASSU_WASM_CACHE_DIR ?? join(homedir(), ".massu", "wasm-cache");
}
function getCachedPath(language, sha) {
  return join(getCacheDir(), `${language}-${sha}.wasm`);
}
var DEFAULT_CACHE_RETAIN_COUNT = 16;
function getCacheRetainCount() {
  const env = process.env.MASSU_WASM_CACHE_RETAIN;
  if (env) {
    const n = Number(env);
    if (Number.isFinite(n) && n >= 1 && n <= 1024) return Math.floor(n);
  }
  return DEFAULT_CACHE_RETAIN_COUNT;
}
function touchCacheFile(path) {
  try {
    const now = /* @__PURE__ */ new Date();
    utimesSync(path, now, now);
  } catch {
  }
}
function evictBeyondRetainCount(retain = getCacheRetainCount()) {
  const dir = getCacheDir();
  let entries;
  try {
    entries = readdirSync(dir);
  } catch {
    return;
  }
  const candidates = [];
  for (const name of entries) {
    if (!name.endsWith(".wasm")) continue;
    const path = join(dir, name);
    let stat;
    try {
      stat = lstatSync(path);
    } catch {
      continue;
    }
    if (stat.isSymbolicLink() || !stat.isFile()) {
      process.stderr.write(
        `[tree-sitter-loader] cache eviction skipped non-regular file: ${path} (possible symlink attack \u2014 see Phase 3.5 finding F-008).
`
      );
      continue;
    }
    candidates.push({ path, mtimeMs: stat.mtimeMs });
  }
  if (candidates.length <= retain) return;
  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
  for (const victim of candidates.slice(retain)) {
    try {
      unlinkSync(victim.path);
    } catch {
    }
  }
}
function sha256(bytes) {
  return createHash("sha256").update(bytes).digest("hex");
}
var parserInitPromise = null;
async function ensureParserInitialized() {
  if (parserInitPromise) return parserInitPromise;
  parserInitPromise = Parser.init();
  return parserInitPromise;
}
var loadedGrammars = /* @__PURE__ */ new Map();
async function loadGrammar(language, options = {}) {
  await ensureParserInitialized();
  const cached = loadedGrammars.get(language);
  if (cached) return cached;
  const manifest = options.manifestOverride?.[language] ?? GRAMMAR_MANIFEST[language];
  if (!manifest) {
    throw new GrammarUnavailableError(
      language,
      new Error(`No manifest entry for language "${language}". v1 supports: ${Object.keys(GRAMMAR_MANIFEST).join(", ")}.`)
    );
  }
  const cachePath = getCachedPath(language, manifest.sha256);
  let cacheLstat;
  try {
    cacheLstat = lstatSync(cachePath);
  } catch {
    cacheLstat = null;
  }
  if (cacheLstat) {
    if (cacheLstat.isSymbolicLink() || !cacheLstat.isFile()) {
      throw new GrammarCacheSymlinkError(cachePath);
    }
    let bytes;
    try {
      bytes = readFileSync(cachePath);
    } catch (e) {
      bytes = new Uint8Array(0);
    }
    if (bytes.byteLength > 0) {
      const actualSha = sha256(bytes);
      if (actualSha !== manifest.sha256) {
        throw new GrammarSHAMismatchError(language, manifest.sha256, actualSha);
      }
      const lang2 = await Language.load(bytes);
      loadedGrammars.set(language, lang2);
      touchCacheFile(cachePath);
      return lang2;
    }
  }
  if (!/^https:\/\//i.test(manifest.url)) {
    throw new GrammarUrlNotHttpsError(manifest.url);
  }
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
  if (!fetchImpl) {
    throw new GrammarUnavailableError(
      language,
      new Error("No fetch implementation available (Node < 18?)")
    );
  }
  let body;
  try {
    const res = await fetchImpl(manifest.url);
    if (!res.ok) {
      throw new Error(`HTTP ${res.status ?? "unknown"} from ${manifest.url}`);
    }
    body = new Uint8Array(await res.arrayBuffer());
  } catch (e) {
    throw new GrammarUnavailableError(language, e);
  }
  const downloadedSha = sha256(body);
  if (downloadedSha !== manifest.sha256) {
    throw new GrammarSHAMismatchError(language, manifest.sha256, downloadedSha);
  }
  try {
    mkdirSync(dirname(cachePath), { recursive: true, mode: 448 });
    try {
      chmodSync(dirname(cachePath), 448);
    } catch {
    }
    const tmpPath = `${cachePath}.tmp.${process.pid}`;
    writeFileSync(tmpPath, body, { mode: 384 });
    try {
      chmodSync(tmpPath, 384);
    } catch {
    }
    try {
      renameSync(tmpPath, cachePath);
      try {
        chmodSync(cachePath, 384);
      } catch {
      }
    } catch (e) {
      try {
        unlinkSync(tmpPath);
      } catch {
      }
      throw e;
    }
    evictBeyondRetainCount();
  } catch (e) {
    process.stderr.write(
      `[tree-sitter-loader] cache write failed for ${language}: ${e instanceof Error ? e.message : String(e)} \u2014 loading directly from memory.
`
    );
  }
  const lang = await Language.load(body);
  loadedGrammars.set(language, lang);
  return lang;
}

// src/detect/adapters/parse-guard.ts
var MAX_AST_FILE_BYTES = 1 * 1024 * 1024;
var MAX_AST_PARSE_DEPTH = 5e3;
var MAX_AST_PARSE_MS = 2e3;
function isParsableSource(source, sizeBytes) {
  const bytes = sizeBytes ?? Buffer.byteLength(source, "utf-8");
  if (bytes > MAX_AST_FILE_BYTES) {
    return {
      reason: "size-cap",
      detail: `${bytes} bytes > ${MAX_AST_FILE_BYTES} cap`
    };
  }
  let depth = 0;
  let maxDepth = 0;
  for (let i = 0; i < source.length; i++) {
    const c = source.charCodeAt(i);
    if (c === 0) {
      return { reason: "control-bytes", detail: "NUL byte at offset " + i };
    }
    if (c === 40 || c === 91 || c === 123) {
      depth++;
      if (depth > maxDepth) maxDepth = depth;
      if (depth > MAX_AST_PARSE_DEPTH) {
        return {
          reason: "depth-cap",
          detail: `nesting depth exceeded ${MAX_AST_PARSE_DEPTH}`
        };
      }
    } else if (c === 41 || c === 93 || c === 125) {
      depth = depth > 0 ? depth - 1 : 0;
    }
  }
  return null;
}

// src/adapter.ts
function defineAdapter(spec) {
  return spec;
}
export {
  InvalidQueryError,
  MAX_AST_FILE_BYTES,
  MAX_AST_PARSE_DEPTH,
  MAX_AST_PARSE_MS,
  defineAdapter,
  isParsableSource,
  loadGrammar,
  runQuery
};
