File size: 1,671 Bytes
5bfc214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"""Shared diff-to-patch-lines logic (spec §3.2's `patch_lines_vuln`), used by
every source loader so patch line numbers are always relative to the
extracted function body itself — never to the original file's absolute line
numbers. See DECISIONS.md "SecVulEval patch-line numbers were file-absolute,
not function-relative" for why this needed to be a shared, source-agnostic
computation rather than trusting any one dataset's own line-number field.
"""
from __future__ import annotations

import difflib


def diff_and_patch_lines(func_vuln: str, func_patch: str) -> tuple[str, list[int]]:
    """Unified diff of the function pair + 1-based line numbers in func_vuln
    that were removed/changed by the patch (spec §3.2 `patch_lines_vuln`).

    A pure insertion (e.g. a missing null-check added, CWE-476/CWE-20 style
    fixes) has no line in func_vuln that was literally changed, but the
    patch location is still meaningful for localization: we anchor to the
    line immediately preceding the insertion point (1-based), or line 1 if
    the insertion is at the very top.
    """
    vuln_lines = func_vuln.splitlines(keepends=True)
    patch_lines = func_patch.splitlines(keepends=True)
    diff = "".join(
        difflib.unified_diff(vuln_lines, patch_lines, fromfile="vuln", tofile="patch")
    )
    changed: set[int] = set()
    sm = difflib.SequenceMatcher(a=vuln_lines, b=patch_lines, autojunk=False)
    for tag, i1, i2, _j1, _j2 in sm.get_opcodes():
        if tag in ("replace", "delete"):
            changed.update(range(i1 + 1, i2 + 1))  # 1-based
        elif tag == "insert":
            changed.add(max(1, i1))
    return diff, sorted(changed)