From 7e04a2e100593b9a90cb4e052d60a38168d97a1a Mon Sep 17 00:00:00 2001 From: Erfouni Date: Fri, 16 Oct 2026 14:36:06 +0330 Subject: [PATCH] Stop dotted sheet names from saving over each other _safe_output_path treated everything after the last dot as a file extension and replaced it. So "Sheet A.1" and "Sheet A.2" were both saved as "Sheet A.dwg", and the second save overwrote the first. Now a dot only counts as an extension when two to four ASCII letters follow it, such as .dwg, .pdf, .exe or .jpeg. Any other tail is kept and the target extension is added after it, so "Sheet A.1" is saved as "Sheet A.1.dwg". A real extension is still swapped, so "plan.exe" requested as a PDF is still "plan.pdf". Adds regression tests in tests/test_safe_output_path.py. --- tests/test_safe_output_path.py | 21 +++++++++++++++++++++ tools.py | 11 ++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/test_safe_output_path.py b/tests/test_safe_output_path.py index 1c559b0..8f49a32 100644 --- a/tests/test_safe_output_path.py +++ b/tests/test_safe_output_path.py @@ -54,6 +54,27 @@ def test_wrong_extension_is_replaced(self): tools._safe_output_path("plan.exe", ".pdf"), self.save_dir / "plan.pdf" ) + def test_longer_real_extension_is_replaced(self): + self.assertEqual( + tools._safe_output_path("plan.jpeg", ".pdf"), self.save_dir / "plan.pdf" + ) + + def test_dotted_sheet_name_keeps_its_tail(self): + # The tail is not an extension, so it stays in the name. + self.assertEqual( + tools._safe_output_path("Sheet A.1", ".dwg"), + self.save_dir / "Sheet A.1.dwg", + ) + self.assertEqual( + tools._safe_output_path("Sheet 1.A", ".dwg"), + self.save_dir / "Sheet 1.A.dwg", + ) + + def test_dotted_sheet_names_do_not_share_a_file(self): + first = tools._safe_output_path("Sheet A.1", ".dwg") + second = tools._safe_output_path("Sheet A.2", ".dwg") + self.assertNotEqual(first, second) + def test_subfolder_is_allowed_and_created(self): target = tools._safe_output_path("floor-1/plan.dwg", ".dwg") self.assertEqual(target, self.save_dir / "floor-1" / "plan.dwg") diff --git a/tools.py b/tools.py index 037d572..409b90f 100644 --- a/tools.py +++ b/tools.py @@ -59,6 +59,12 @@ def _color_index(color: str | int | None) -> int | None: ) +# A dot is read as a file extension only when two to four ASCII letters follow +# it (.dwg, .pdf, .exe, .jpeg). Any other tail, such as "Sheet A.1" or +# "Sheet 1.A", is part of the name and is kept. +_EXTENSION_SHAPE = re.compile(r"\.[A-Za-z]{2,4}") + + def _safe_output_path(filename: str, extension: str) -> Path: """Resolve filename inside the one folder the server may write to.""" candidate = Path(str(filename).strip().strip('"').strip("'")) @@ -69,7 +75,10 @@ def _safe_output_path(filename: str, extension: str) -> Path: if not candidate.name: raise AcadError("A file name is required.") if candidate.suffix.lower() != extension: - candidate = candidate.with_suffix(extension) + if _EXTENSION_SHAPE.fullmatch(candidate.suffix): + candidate = candidate.with_suffix(extension) + else: + candidate = candidate.with_name(candidate.name + extension) resolved = (config.SAVE_DIR / candidate).resolve() # A path comparison, not a string prefix: "...\Out-other" starts with