"""Loads SecVulEval (HF: arag0rn/SecVulEval) and normalizes to the unified corpus schema (spec §3.2). SecVulEval ships statement-level `changed_statements` (line, code) pairs, but **its line numbers are relative to the original source FILE, not the extracted function body** (verified by inspection: a sample with an 80-line func_body had a changed_statements line number of 877). Silently trusting them would map every patch line to nothing inside func_vuln. We therefore recompute `patch_lines_vuln` ourselves via the same diff-based approach as PrimeVul (`src/data/diff_utils.py`), using `changed_statements` only to sanity-check CWE/pairing — not for line numbers. See DECISIONS.md "SecVulEval patch-line numbers were file-absolute, not function-relative". Pairing is via `fixed_func_idx`, which points from a vulnerable row's `idx` to its patched counterpart's `idx` in the same table. `commit_date` is not provided by this dataset directly; recovering it would require one GitHub API call per of its 6,050 distinct commits, which the unauthenticated API's 60 req/hour limit makes impractical in one session (~100+ hours). Left as None here, consistent with PrimeVul rows that didn't match Big-Vul; see DECISIONS.md. Revisit with a GitHub token if the chronological split's date coverage needs improving. """ from __future__ import annotations import hashlib from pathlib import Path import pandas as pd from src.data.diff_utils import diff_and_patch_lines PARQUET_PATH = Path("data/raw/SecVulEval/secvuleval.parquet") def _func_hash(text: str) -> str: return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()[:16] def _first_or_none(arr) -> str | None: try: return arr[0] if arr is not None and len(arr) > 0 else None except TypeError: return None def normalize_secvuleval(path: Path = PARQUET_PATH) -> tuple[list[dict], dict]: df = pd.read_parquet(path) by_idx = {int(row.idx): row for row in df.itertuples()} rows: list[dict] = [] n_pairs = 0 seen_pairs: set[frozenset[int]] = set() for row in df.itertuples(): if not row.is_vulnerable: continue fixed_idx = row.fixed_func_idx if fixed_idx is None or pd.isna(fixed_idx): continue fixed_idx = int(fixed_idx) if fixed_idx not in by_idx: continue patch_row = by_idx[fixed_idx] pair_key = frozenset((int(row.idx), fixed_idx)) if pair_key in seen_pairs: continue seen_pairs.add(pair_key) n_pairs += 1 cve = _first_or_none(row.cve_list) cwe = _first_or_none(row.cwe_list) pair_id = f"secvuleval:{row.commit_id}:{row.idx}" diff_text, patch_lines_vuln = diff_and_patch_lines(row.func_body, patch_row.func_body) # sample_id includes the row's own idx, not just a content hash: two # different pairs from the same commit can converge to an identical # func text (verified in PrimeVul; guarding against it here too). for target, func_text in ((1, row.func_body), (0, patch_row.func_body)): sample_id = f"secvuleval:{cve or 'NOCVE'}:{row.commit_id}:{row.idx}:{_func_hash(func_text)}" rows.append({ "sample_id": sample_id, "source": "secvuleval", "project": row.project, "cve": cve, "cwe": cwe, "commit_id": row.commit_id, "func_vuln": row.func_body, "func_patch": patch_row.func_body, "diff": diff_text, "patch_lines_vuln": patch_lines_vuln if target == 1 else [], "commit_date": None, "target": target, "pair_id": pair_id, }) stats = { "n_pairs": n_pairs, "n_rows": len(rows), "n_with_date": 0, "date_coverage": 0.0, } return rows, stats