diff --git a/MethodenAnalyser3.py b/MethodenAnalyser3.py index 847be23..a459af6 100644 --- a/MethodenAnalyser3.py +++ b/MethodenAnalyser3.py @@ -1,6 +1,7 @@ import argparse import ast import builtins +import codecs import collections import datetime import difflib @@ -1092,7 +1093,7 @@ def analyze_file(path: str) -> AnalysisResult: # Datei lesen try: - with open(path, "r", encoding="utf-8") as f: + with open(path, "r", encoding="utf-8-sig") as f: code = f.read() except UnicodeDecodeError: try: @@ -1611,8 +1612,10 @@ def auto_fix_unused_imports(output_widget: scrolledtext.ScrolledText, status_wid # Datei lesen mit Encoding-Fallback — erkanntes Encoding für Schreibzugriff merken detected_encoding = "utf-8" original_bytes = pathlib.Path(_last_analysis_path).read_bytes() + if original_bytes.startswith(codecs.BOM_UTF8): + detected_encoding = "utf-8-sig" try: - original_text = original_bytes.decode("utf-8") + original_text = original_bytes.decode(detected_encoding) except UnicodeDecodeError: detected_encoding = "latin-1" original_text = original_bytes.decode("latin-1") diff --git a/README.md b/README.md index 96db3f3..728cb6b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
-
+
@@ -173,7 +173,7 @@ MethodenAnalyser enforces 10 strict architectural, runtime, and security invaria
| **INV-ENCODING-04** | Lossless Multi-Encoding Resilience | Default UTF-8 reading with automatic Latin-1 fallback. Never corrupts original character encodings, UTF-8 BOMs, or German umlauts. |
| **INV-ISOLATION-05** | Loopback Web Companion Isolation | Local helper companion binds strictly to `127.0.0.1` by default. Deliberate `--host 0.0.0.0` LAN test mode requires explicit user configuration. |
| **INV-TRAVERSAL-06** | Anti-Path-Traversal & ZIP Safety | Web companion rejects path traversal (`..`), bounds member counts, and enforces maximum archive byte limits to prevent ZIP bombs. |
-| **INV-AUTOFIX-07** | Reversible Auto-Fix with Backup | Auto-Fix requires explicit confirmation, rechecks current source usage and publishes a byte-exact backup without replacing existing copies (`.bak`, `.bak.1`, …), then atomically replaces the source from a flushed temporary file. Detected source changes abort the write; encoding, newline style and file mode are retained. |
+| **INV-AUTOFIX-07** | Reversible Auto-Fix with Backup | Auto-Fix requires explicit confirmation, rechecks current source usage and publishes a byte-exact backup without replacing existing copies (`.bak`, `.bak.1`, …), then atomically replaces the source from a flushed temporary file. Detected source changes abort the write; encoding (including a UTF-8 BOM), newline style and file mode are retained. |
| **INV-PLATFORM-08** | Multi-Platform Parity | 100% standard library core verified across Windows Server 2025, Ubuntu Linux, and macOS 26. |
| **INV-SYNC-09** | Cloud-Sync & Multi-Agent Lock Safety | Fails closed on lock detection (`LOCK.*`), ignores cloud sync conflict files (`*-conflict-*`), preventing concurrency races. |
| **INV-SLA-10** | 48h Security SLA & 5-Day Triage | Strict vulnerability commitment: 48-hour initial response SLA and 5-business-day triage commitment with coordinated disclosure. |
diff --git a/README_de.md b/README_de.md
index c2b96c0..a8e85b1 100644
--- a/README_de.md
+++ b/README_de.md
@@ -5,7 +5,7 @@
-
+
@@ -173,7 +173,7 @@ MethodenAnalyser garantiert 10 verbindliche Architektur-, Laufzeit- und Sicherhe
| **INV-ENCODING-04** | Verlustfreie Multi-Encoding-Resilienz | Liest Dateien standardmäßig als UTF-8 mit automatischem Latin-1 Fallback. Verändert niemals Original-Encodings oder deutsche Umlaute. |
| **INV-ISOLATION-05** | Loopback Web-Begleiter-Isolation | Der lokale Hilfsserver bindet sich standardmäßig ausschließlich an `127.0.0.1`. Der LAN-Modus `--host 0.0.0.0` erfordert explizite Aktivierung. |
| **INV-TRAVERSAL-06** | Pfad-Traversal- & ZIP-Sicherheit | Der Web-Begleiter blockiert Directory-Traversal (`..`), begrenzt Dateizahlen und unkomprimierte Byte-Mengen zur Abwehr von ZIP-Bomben. |
-| **INV-AUTOFIX-07** | Reversibler Auto-Fix mit Backup | Auto-Fix erfordert eine explizite Bestätigung, prüft die aktuelle Verwendung erneut und veröffentlicht eine bytegenaue Sicherung ohne bestehende Backups zu überschreiben (`.bak`, `.bak.1`, …) und ersetzt die Quelle anschließend atomar aus einer vollständig geschriebenen temporären Datei. Erkannte Quelländerungen brechen den Schreibvorgang ab; Kodierung, Zeilenumbruchstil und Dateimodus bleiben erhalten. |
+| **INV-AUTOFIX-07** | Reversibler Auto-Fix mit Backup | Auto-Fix erfordert eine explizite Bestätigung, prüft die aktuelle Verwendung erneut und veröffentlicht eine bytegenaue Sicherung ohne bestehende Backups zu überschreiben (`.bak`, `.bak.1`, …) und ersetzt die Quelle anschließend atomar aus einer vollständig geschriebenen temporären Datei. Erkannte Quelländerungen brechen den Schreibvorgang ab; Kodierung einschließlich UTF-8-BOM, Zeilenumbruchstil und Dateimodus bleiben erhalten. |
| **INV-PLATFORM-08** | Plattformübergreifende Parität | Vollständige Standardbibliothek-Kompatibilität, verifiziert auf Windows Server 2025, Ubuntu Linux und macOS 26. |
| **INV-SYNC-09** | Cloud-Sync- & Lock-Resilienz | Beachtet Dateisperren (`LOCK.*`) im Fail-Closed-Verfahren und ignoriert Cloud-Sync-Konfliktdateien (`*-conflict-*`). |
| **INV-SLA-10** | 48h Sicherheits-SLA & 5-Tage-Triage | Verbindliches Sicherheitsversprechen: Eingangsbestätigung binnen 48 Stunden, Triage und Behebungsplan binnen 5 Werktagen. |
diff --git a/llms.txt b/llms.txt
index ea5b237..8098dbc 100644
--- a/llms.txt
+++ b/llms.txt
@@ -35,7 +35,7 @@ The tool strictly adheres to 10 architectural and security invariants:
- INV-ENCODING-04: Lossless Multi-Encoding Resilience (UTF-8 default with Latin-1 fallback)
- INV-ISOLATION-05: Loopback Web Companion Isolation (binds to 127.0.0.1 by default)
- INV-TRAVERSAL-06: Path Traversal & ZIP Safety Bounds (blocks `..` and bounds member sizes)
-- INV-AUTOFIX-07: Reversible Auto-Fix with `.bak` Backup; current source is rechecked after confirmation, and only approved imports that remain unused are removed. Import spans preserve neighboring statements; empty suites retain pass, and the result is syntax-checked before writing. Auto-Fix stages and flushes complete byte payloads, exclusively publishes numbered backups via same-directory hard links and atomically replaces the source. Detected source changes abort; original bytes remain in the backup. Unsupported backup publication fails before source replacement.
+- INV-AUTOFIX-07: Reversible Auto-Fix with `.bak` Backup; current source is rechecked after confirmation, and only approved imports that remain unused are removed. Import spans preserve neighboring statements; empty suites retain pass, and the result is syntax-checked before writing. Auto-Fix stages and flushes complete byte payloads, exclusively publishes numbered backups via same-directory hard links and atomically replaces the source. Detected source changes abort; original bytes remain in the backup. Unsupported backup publication fails before source replacement. File analysis accepts a leading UTF-8 BOM; Auto-Fix preserves the signature and byte-exact recovery copy.
- INV-PLATFORM-08: Cross-Platform Parity across Windows, Linux, and macOS
- INV-SYNC-09: Cloud-Sync & Multi-Agent Lock Resilience (fail-closed against `LOCK.*`)
- INV-SLA-10: 48h Initial Response & 5-Business-Day Triage SLA
diff --git a/tests/test_utf8_bom.py b/tests/test_utf8_bom.py
new file mode 100644
index 0000000..32cd6ea
--- /dev/null
+++ b/tests/test_utf8_bom.py
@@ -0,0 +1,57 @@
+"""UTF-8 BOM is a valid Python signature and must survive import cleanup."""
+import codecs
+import json
+import subprocess
+import sys
+import warnings
+from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
+
+import MethodenAnalyser3 as m
+
+
+@pytest.mark.parametrize("newline", ["\n", "\r\n", "\r"])
+def test_analysis_accepts_utf8_bom_without_fallback(tmp_path, newline):
+ path = tmp_path / "signed.py"
+ source = newline.join(["import os", "label = 'Grüße'", "print(label)", ""])
+ path.write_bytes(source.encode("utf-8-sig"))
+ with warnings.catch_warnings(record=True) as caught:
+ result = m.analyze_file(str(path))
+ assert not caught
+ assert result.unused_imports == ["os"]
+
+
+@pytest.mark.parametrize("source,expected", [
+ ("import os\r\nanswer = 42\r\n", "answer = 42\r\n"),
+ ("label = 'Grüße'; import os; answer = 42\n", "label = 'Grüße'; pass; answer = 42\n"),
+ ("import os\n", ""),
+])
+def test_auto_fix_preserves_bom_and_exact_backup(monkeypatch, tmp_path, source, expected):
+ path = tmp_path / "signed.py"
+ raw = source.encode("utf-8-sig")
+ path.write_bytes(raw)
+ monkeypatch.setattr(m, "_last_analysis_path", str(path))
+ monkeypatch.setattr(m, "_last_analysis_result", m.analyze_source(source))
+ dialogs = Mock()
+ dialogs.askyesno.return_value = True
+ monkeypatch.setattr(m, "messagebox", dialogs)
+ m.auto_fix_unused_imports(Mock())
+ dialogs.showerror.assert_not_called()
+ assert path.read_bytes() == expected.encode("utf-8-sig")
+ assert path.read_bytes().startswith(codecs.BOM_UTF8)
+ assert path.with_suffix(".py.bak").read_bytes() == raw
+ assert not list(tmp_path.glob(".*.autofix-*"))
+
+
+def test_cli_reports_bom_file_findings(tmp_path):
+ path = tmp_path / "signed.py"
+ path.write_bytes("import os\nprint('Grüße')\n".encode("utf-8-sig"))
+ report = tmp_path / "report.json"
+ script = Path(m.__file__).resolve()
+ result = subprocess.run([sys.executable, str(script), "--lang", "de", "--file", str(path),
+ "--json-output", str(report)], capture_output=True, check=False)
+ assert result.returncode == m.EXIT_FINDINGS, result.stderr
+ payload = json.loads(report.read_text(encoding="utf-8"))
+ assert payload["unused_imports"] == {"signed.py": ["os"]}