From 709e5eaa527a9da932483380c9c9e3e9fe58818f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 22:38:45 +0000 Subject: [PATCH 1/2] fix(library): validate snippet lock flag before writing on import import_payload saved each snippet first and checked its locked value afterwards. An invalid value made it delete the snippet again, which also removed a snippet of the same name that existed before the import. The value is now checked first and such entries are skipped untouched. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01THB2F2DbFt7kDuUx3ZdWAr --- CHANGELOG.md | 1 + PythonBox_v8.py | 14 ++++++++------ tests/test_pythonbox_regressions.py | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f50e8..3780f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/). ### Behoben / Fixed - Sichere Literalersetzung in `SearchReplaceBar.replace_all`: Case-insensitive Textersetzung verwendet jetzt einen Callable-Ersatz (`lambda _m: replace_text`) in `re.sub()`. Dies verhindert Abstürze mit `re.error: bad escape \u` bei Pfadangaben mit Backslashes (z. B. Windows-Pfade wie `C:\Users\...`) sowie Fehlinterpretationen von Regex-Gruppenreferenzen und stellt identisches Literal-Verhalten wie bei case-sensitiver Ersetzung sicher. +- Snippet-Import prüft `locked` vor dem Schreiben: `LibraryManager.import_payload` hat bei einem ungültigen `locked`-Wert das Snippet zuerst gespeichert und danach wieder gelöscht. Ein bereits vorhandenes Snippet gleichen Namens ging dabei verloren. Solche Einträge werden jetzt übersprungen, ohne die Bibliothek anzufassen. ## [1.0.1] - 2026-09-18 diff --git a/PythonBox_v8.py b/PythonBox_v8.py index 7095b52..f646bb8 100644 --- a/PythonBox_v8.py +++ b/PythonBox_v8.py @@ -3029,18 +3029,20 @@ def import_payload(self, payload: dict) -> dict: continue snippet_name = snippet_name.strip() + # Erst prüfen, dann schreiben: ein ungültiger "locked"-Wert darf + # kein bereits vorhandenes Snippet überschreiben oder löschen. + try: + is_locked = _coerce_portable_bool(locked) + except ValueError: + skipped += 1 + continue + success, _message = self.save_snippet(topic_name, snippet_name, content) if not success: skipped += 1 continue key = f"{topic_name}/{snippet_name}" - try: - is_locked = _coerce_portable_bool(locked) - except ValueError: - skipped += 1 - self.delete_item(topic_name, snippet_name) - continue if is_locked: self.locked_items.add(key) diff --git a/tests/test_pythonbox_regressions.py b/tests/test_pythonbox_regressions.py index 1b00cfb..38e9d6b 100644 --- a/tests/test_pythonbox_regressions.py +++ b/tests/test_pythonbox_regressions.py @@ -458,6 +458,29 @@ def test_snippet_library_json_roundtrip_preserves_content_and_locks(self): self.assertEqual("print('eins')\n", imported_manager.get_content("Tests", "Alpha")) self.assertTrue(imported_manager.is_locked("Tests", "Alpha")) + def test_import_with_invalid_locked_value_keeps_existing_snippet(self): + module = load_pythonbox_module() + + with tempfile.TemporaryDirectory() as temp_dir: + manager = module.LibraryManager(root=Path(temp_dir) / "library") + manager.save_snippet("Tests", "Alpha", "print('original')\n") + + result = manager.import_payload({ + "schema": module.SNIPPET_EXPORT_SCHEMA, + "topics": [{ + "name": "Tests", + "snippets": [ + {"name": "Alpha", "content": "print('neu')\n", "locked": "vielleicht"}, + {"name": "Beta", "content": "print('beta')\n", "locked": "vielleicht"}, + ], + }], + }) + + self.assertEqual(0, result["imported"]) + self.assertEqual(2, result["skipped"]) + self.assertEqual("print('original')\n", manager.get_content("Tests", "Alpha")) + self.assertEqual("", manager.get_content("Tests", "Beta")) + class MinimapToggleRegressionTests(unittest.TestCase): """Regression tests for toggle_minimap cleanup.""" From 95475e3c50a61e6e9960e85af6c898be4d805e66 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 23:38:56 +0000 Subject: [PATCH 2/2] test: move import validation test into its own class Keeps this branch mergeable alongside the open path-traversal PR, which appends tests at the same place in LibraryExportRegressionTests. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01THB2F2DbFt7kDuUx3ZdWAr --- tests/test_pythonbox_regressions.py | 48 +++++++++++++++-------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/test_pythonbox_regressions.py b/tests/test_pythonbox_regressions.py index 38e9d6b..48ba599 100644 --- a/tests/test_pythonbox_regressions.py +++ b/tests/test_pythonbox_regressions.py @@ -430,6 +430,31 @@ def test_save_file_as_cancel_keeps_original_path(self): app.processEvents() +class LibraryImportValidationRegressionTests(unittest.TestCase): + def test_import_with_invalid_locked_value_keeps_existing_snippet(self): + module = load_pythonbox_module() + + with tempfile.TemporaryDirectory() as temp_dir: + manager = module.LibraryManager(root=Path(temp_dir) / "library") + manager.save_snippet("Tests", "Alpha", "print('original')\n") + + result = manager.import_payload({ + "schema": module.SNIPPET_EXPORT_SCHEMA, + "topics": [{ + "name": "Tests", + "snippets": [ + {"name": "Alpha", "content": "print('neu')\n", "locked": "vielleicht"}, + {"name": "Beta", "content": "print('beta')\n", "locked": "vielleicht"}, + ], + }], + }) + + self.assertEqual(0, result["imported"]) + self.assertEqual(2, result["skipped"]) + self.assertEqual("print('original')\n", manager.get_content("Tests", "Alpha")) + self.assertEqual("", manager.get_content("Tests", "Beta")) + + class LibraryExportRegressionTests(unittest.TestCase): def test_snippet_library_json_roundtrip_preserves_content_and_locks(self): module = load_pythonbox_module() @@ -458,29 +483,6 @@ def test_snippet_library_json_roundtrip_preserves_content_and_locks(self): self.assertEqual("print('eins')\n", imported_manager.get_content("Tests", "Alpha")) self.assertTrue(imported_manager.is_locked("Tests", "Alpha")) - def test_import_with_invalid_locked_value_keeps_existing_snippet(self): - module = load_pythonbox_module() - - with tempfile.TemporaryDirectory() as temp_dir: - manager = module.LibraryManager(root=Path(temp_dir) / "library") - manager.save_snippet("Tests", "Alpha", "print('original')\n") - - result = manager.import_payload({ - "schema": module.SNIPPET_EXPORT_SCHEMA, - "topics": [{ - "name": "Tests", - "snippets": [ - {"name": "Alpha", "content": "print('neu')\n", "locked": "vielleicht"}, - {"name": "Beta", "content": "print('beta')\n", "locked": "vielleicht"}, - ], - }], - }) - - self.assertEqual(0, result["imported"]) - self.assertEqual(2, result["skipped"]) - self.assertEqual("print('original')\n", manager.get_content("Tests", "Alpha")) - self.assertEqual("", manager.get_content("Tests", "Beta")) - class MinimapToggleRegressionTests(unittest.TestCase): """Regression tests for toggle_minimap cleanup."""