Skip to content

Commit c03459f

Browse files
Centralize safe path display formatting for file inputs
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 3ba18bc commit c03459f

File tree

5 files changed

+96
-25
lines changed

5 files changed

+96
-25
lines changed

hyperbrowser/client/file_utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,40 @@
66
from hyperbrowser.exceptions import HyperbrowserError
77
from hyperbrowser.type_utils import is_plain_string
88

9+
_MAX_FILE_PATH_DISPLAY_LENGTH = 200
10+
11+
12+
def format_file_path_for_error(
13+
file_path: object,
14+
*,
15+
max_length: int = _MAX_FILE_PATH_DISPLAY_LENGTH,
16+
) -> str:
17+
try:
18+
path_value = (
19+
os.fspath(file_path)
20+
if is_plain_string(file_path) or isinstance(file_path, PathLike)
21+
else file_path
22+
)
23+
except Exception:
24+
return "<provided path>"
25+
if not is_plain_string(path_value):
26+
return "<provided path>"
27+
try:
28+
sanitized_path = "".join(
29+
"?" if ord(character) < 32 or ord(character) == 127 else character
30+
for character in path_value
31+
)
32+
except Exception:
33+
return "<provided path>"
34+
if not is_plain_string(sanitized_path):
35+
return "<provided path>"
36+
if len(sanitized_path) <= max_length:
37+
return sanitized_path
38+
truncated_length = max_length - 3
39+
if truncated_length <= 0:
40+
return "..."
41+
return f"{sanitized_path[:truncated_length]}..."
42+
943

1044
def _normalize_file_path_text(file_path: Union[str, PathLike[str]]) -> str:
1145
try:

hyperbrowser/client/managers/extension_create_utils.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from hyperbrowser.exceptions import HyperbrowserError
44
from hyperbrowser.models.extension import CreateExtensionParams
55

6-
from ..file_utils import ensure_existing_file_path
6+
from ..file_utils import ensure_existing_file_path, format_file_path_for_error
77
from .serialization_utils import serialize_model_dump_to_dict
88

99

@@ -26,9 +26,10 @@ def normalize_extension_create_input(params: Any) -> Tuple[str, Dict[str, Any]]:
2626
)
2727
payload.pop("filePath", None)
2828

29+
file_path_display = format_file_path_for_error(raw_file_path)
2930
file_path = ensure_existing_file_path(
3031
raw_file_path,
31-
missing_file_message=f"Extension file not found at path: {raw_file_path}",
32-
not_file_message=f"Extension file path must point to a file: {raw_file_path}",
32+
missing_file_message=f"Extension file not found at path: {file_path_display}",
33+
not_file_message=f"Extension file path must point to a file: {file_path_display}",
3334
)
3435
return file_path, payload

hyperbrowser/client/managers/session_upload_utils.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,11 @@
66
from hyperbrowser.exceptions import HyperbrowserError
77
from hyperbrowser.type_utils import is_plain_string, is_string_subclass_instance
88

9-
from ..file_utils import ensure_existing_file_path, open_binary_file
10-
11-
_MAX_FILE_PATH_DISPLAY_LENGTH = 200
12-
13-
14-
def _format_upload_path_for_error(raw_file_path: object) -> str:
15-
if not is_plain_string(raw_file_path):
16-
return "<provided path>"
17-
try:
18-
normalized_path = "".join(
19-
"?" if ord(character) < 32 or ord(character) == 127 else character
20-
for character in raw_file_path
21-
)
22-
except Exception:
23-
return "<provided path>"
24-
if not is_plain_string(normalized_path):
25-
return "<provided path>"
26-
if len(normalized_path) <= _MAX_FILE_PATH_DISPLAY_LENGTH:
27-
return normalized_path
28-
return f"{normalized_path[:_MAX_FILE_PATH_DISPLAY_LENGTH]}..."
9+
from ..file_utils import (
10+
ensure_existing_file_path,
11+
format_file_path_for_error,
12+
open_binary_file,
13+
)
2914

3015

3116
def normalize_upload_file_input(
@@ -41,7 +26,7 @@ def normalize_upload_file_input(
4126
"file_input path is invalid",
4227
original_error=exc,
4328
) from exc
44-
file_path_display = _format_upload_path_for_error(raw_file_path)
29+
file_path_display = format_file_path_for_error(raw_file_path)
4530
file_path = ensure_existing_file_path(
4631
raw_file_path,
4732
missing_file_message=f"Upload file not found at path: {file_path_display}",

tests/test_extension_create_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,18 @@ def test_normalize_extension_create_input_rejects_missing_file(tmp_path):
115115

116116
with pytest.raises(HyperbrowserError, match="Extension file not found"):
117117
normalize_extension_create_input(params)
118+
119+
120+
def test_normalize_extension_create_input_rejects_control_character_path():
121+
params = CreateExtensionParams(
122+
name="bad-extension",
123+
file_path="bad\tpath.zip",
124+
)
125+
126+
with pytest.raises(
127+
HyperbrowserError,
128+
match="file_path must not contain control characters",
129+
) as exc_info:
130+
normalize_extension_create_input(params)
131+
132+
assert exc_info.value.original_error is None

tests/test_file_utils.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import pytest
44

55
import hyperbrowser.client.file_utils as file_utils
6-
from hyperbrowser.client.file_utils import ensure_existing_file_path, open_binary_file
6+
from hyperbrowser.client.file_utils import (
7+
ensure_existing_file_path,
8+
format_file_path_for_error,
9+
open_binary_file,
10+
)
711
from hyperbrowser.exceptions import HyperbrowserError
812

913

@@ -266,6 +270,38 @@ def test_ensure_existing_file_path_rejects_control_character_paths():
266270
)
267271

268272

273+
def test_format_file_path_for_error_sanitizes_control_characters():
274+
display_path = format_file_path_for_error("bad\tpath\nvalue")
275+
276+
assert display_path == "bad?path?value"
277+
278+
279+
def test_format_file_path_for_error_truncates_long_paths():
280+
display_path = format_file_path_for_error("abcdef", max_length=5)
281+
282+
assert display_path == "ab..."
283+
284+
285+
def test_format_file_path_for_error_falls_back_for_non_string_values():
286+
assert format_file_path_for_error(object()) == "<provided path>"
287+
288+
289+
def test_format_file_path_for_error_falls_back_for_fspath_failures():
290+
class _BrokenPathLike:
291+
def __fspath__(self) -> str:
292+
raise RuntimeError("bad fspath")
293+
294+
assert format_file_path_for_error(_BrokenPathLike()) == "<provided path>"
295+
296+
297+
def test_format_file_path_for_error_uses_pathlike_string_values():
298+
class _PathLike:
299+
def __fspath__(self) -> str:
300+
return "/tmp/path-value"
301+
302+
assert format_file_path_for_error(_PathLike()) == "/tmp/path-value"
303+
304+
269305
def test_ensure_existing_file_path_wraps_invalid_path_os_errors(monkeypatch):
270306
def raising_exists(path: str) -> bool:
271307
raise OSError("invalid path")

0 commit comments

Comments
 (0)