| 58 | |
| 59 | @classmethod |
| 60 | def from_modified(cls, a_loc: ScanLocation, b_loc: ScanLocation, similarity: Optional[float]=None, content_diff: Optional[str]=None): |
| 61 | data = { |
| 62 | "operation": "M", |
| 63 | "a_scan": a_loc, |
| 64 | "b_scan": b_loc |
| 65 | } |
| 66 | |
| 67 | if content_diff is None and a_loc.metadata["md5"] != b_loc.metadata["md5"]: |
| 68 | # FIXME: add handling for binary and empty files |
| 69 | try: |
| 70 | a_content = a_loc.location.read_text().splitlines(keepends=True) |
| 71 | b_content = b_loc.location.read_text().splitlines(keepends=True) |
| 72 | content_diff = difflib.unified_diff(a_content, b_content, fromfile=str(a_loc), tofile=str(b_loc)) |
| 73 | except UnicodeDecodeError: # FIXME: thrown when file is a binary file |
| 74 | pass |
| 75 | else: |
| 76 | if similarity is None: |
| 77 | similarity = difflib.SequenceMatcher(None, a_content, b_content).ratio() |
| 78 | |
| 79 | # TODO: Use OS line ending instead of hardcoded unix '\n' |
| 80 | data["diff"] = "".join(content_diff) |
| 81 | else: |
| 82 | data["diff"] = content_diff |
| 83 | |
| 84 | data["similarity"] = similarity or 0.0 |
| 85 | return cls(**data) |
| 86 | |
| 87 | def add_detections(self, a_detections: List[Detection], b_detections: List[Detection]): |
| 88 | duplicates = set(x.diff_hash for x in a_detections) & set(x.diff_hash for x in b_detections) |