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