Skip to content

Commit d687427

Browse files
committed
Handle empty Bambu Studio URIs
Ignore empty bambustudioopen URI triggers, log problematic input URIs during extraction failures, and bump the package version to 0.1.2.
1 parent 6f78a2d commit d687427

5 files changed

Lines changed: 91 additions & 6 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "slicer-uri-bridge"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
description = "Register slicer URI handlers and bridge slicer links to Bambu Studio."
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/slicer_uri_bridge/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
"""Slicer URI Bridge package."""
22

33
from __future__ import annotations
4-
5-
__version__ = "0.1.1"

src/slicer_uri_bridge/cli.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import subprocess
77
import sys
88
import tomllib
9+
from importlib.metadata import version as package_version
910
from pathlib import Path
1011

11-
from . import __version__
1212
from .config import config_matches_default, init_user_config, user_config_path
1313
from .manager import main as manager_main
1414

@@ -32,7 +32,11 @@ def build_parser() -> argparse.ArgumentParser:
3232
prog="slicer-uri-bridge",
3333
description="Register slicer URI handlers and bridge slicer links to Bambu Studio.",
3434
)
35-
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
35+
parser.add_argument(
36+
"--version",
37+
action="version",
38+
version=f"%(prog)s {package_version('slicer-uri-bridge')}",
39+
)
3640

3741
subparsers = parser.add_subparsers(dest="command", required=True)
3842

src/slicer_uri_bridge/handler.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,15 @@ def extract_download(protocol_uri: str, allowed_extensions: set[str]) -> tuple[s
224224
return strip_trailing_model_slash(download_url, allowed_extensions), suggested_name
225225

226226

227+
def is_empty_bambustudioopen_uri(protocol_uri: str) -> bool:
228+
parsed = urllib.parse.urlsplit(protocol_uri)
229+
if parsed.scheme.lower() != "bambustudioopen":
230+
return False
231+
232+
payload = protocol_uri.split(":", 1)[1].lstrip("/")
233+
return not urllib.parse.unquote(payload).strip()
234+
235+
227236
def filename_from_url(url: str) -> str | None:
228237
parsed = urllib.parse.urlsplit(url)
229238
query_name = urllib.parse.parse_qs(parsed.query).get("name", [""])[0].strip()
@@ -609,6 +618,7 @@ def show_error(message: str) -> None:
609618
def main(argv: list[str] | None = None) -> int:
610619
setup_logging()
611620
args = parse_args(sys.argv[1:] if argv is None else argv)
621+
uri: str | None = None
612622
local_path: Path | None = None
613623
download_folder: Path | None = None
614624

@@ -620,7 +630,15 @@ def main(argv: list[str] | None = None) -> int:
620630
download_folder = download_folder_from_config(config)
621631

622632
uri = resolve_protocol_uri(args)
623-
download_url, suggested_name = extract_download(uri, allowed_extensions)
633+
if is_empty_bambustudioopen_uri(uri):
634+
logger.info("Ignoring empty bambustudioopen URI: %r", uri)
635+
return 0
636+
637+
try:
638+
download_url, suggested_name = extract_download(uri, allowed_extensions)
639+
except BridgeError:
640+
logger.error("Input URI: %r", uri)
641+
raise
624642
logger.info(f"Resolved input URI with download URL: {download_url}")
625643
allowed_hosts, allow_any_original_host = load_allowed_hosts(config)
626644
validate_remote_url(

tests/test_handler.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
choose_filename,
1616
extract_download,
1717
filename_from_url,
18+
is_empty_bambustudioopen_uri,
1819
normalize_host,
20+
main,
1921
read_protocol_uri,
2022
has_executable_bits,
2123
launch_bambu,
@@ -69,6 +71,69 @@ def test_download_url_rejects_control_characters(self) -> None:
6971
)
7072

7173

74+
class BambuEmptyUriTests(unittest.TestCase):
75+
def test_detects_empty_bambustudioopen_uri(self) -> None:
76+
self.assertTrue(is_empty_bambustudioopen_uri("bambustudioopen:///"))
77+
self.assertTrue(is_empty_bambustudioopen_uri("bambustudioopen://%20%20"))
78+
self.assertFalse(
79+
is_empty_bambustudioopen_uri(
80+
"bambustudioopen://https%3A%2F%2Ffiles.example%2Fmodels%2Fbenchy.3mf"
81+
)
82+
)
83+
self.assertFalse(is_empty_bambustudioopen_uri("prusaslicer://open?file="))
84+
85+
def test_main_ignores_empty_bambustudioopen_uri(self) -> None:
86+
config = {
87+
"security": {
88+
"allowed_extensions": {".3mf"},
89+
"allow_plain_http": False,
90+
"allowed_hosts": [],
91+
"allow_any_original_host": True,
92+
},
93+
"bambu_studio": {},
94+
}
95+
96+
with (
97+
patch("slicer_uri_bridge.handler.load_config", return_value=config),
98+
patch("slicer_uri_bridge.handler.resolve_bambu_command") as resolve_command,
99+
patch("slicer_uri_bridge.handler.extract_download") as extract,
100+
patch("slicer_uri_bridge.handler.launch_bambu") as launch,
101+
):
102+
exit_code = main(["bambustudioopen:///"])
103+
104+
self.assertEqual(exit_code, 0)
105+
resolve_command.assert_not_called()
106+
extract.assert_not_called()
107+
launch.assert_not_called()
108+
109+
110+
class MainLoggingTests(unittest.TestCase):
111+
def test_logs_input_uri_when_download_extraction_fails(self) -> None:
112+
config = {
113+
"security": {
114+
"allowed_extensions": {".3mf"},
115+
"allow_plain_http": False,
116+
"allowed_hosts": [],
117+
"allow_any_original_host": True,
118+
}
119+
}
120+
121+
with (
122+
patch("slicer_uri_bridge.handler.load_config", return_value=config),
123+
patch("slicer_uri_bridge.handler.show_error"),
124+
self.assertLogs("slicer_uri_bridge", level="ERROR") as captured,
125+
):
126+
exit_code = main(["prusaslicer://open?file=%20%20"])
127+
128+
self.assertEqual(exit_code, 1)
129+
self.assertTrue(
130+
any("Input URI: 'prusaslicer://open?file=%20%20'" in line for line in captured.output)
131+
)
132+
self.assertTrue(
133+
any("Failed: Invalid prusaslicer URI." in line for line in captured.output)
134+
)
135+
136+
72137
class FilenameTests(unittest.TestCase):
73138
def test_filename_from_url_prefers_query_name_basename(self) -> None:
74139
self.assertEqual(

0 commit comments

Comments
 (0)