Skip to content

Commit 2656ae4

Browse files
yashhzdamilcarlucas
authored andcommitted
fix(project): keep new vehicle names within the selected base directory
1 parent 22bebda commit 2656ae4

3 files changed

Lines changed: 84 additions & 30 deletions

File tree

ardupilot_methodic_configurator/backend_filesystem_program_settings.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@
2424
from ntpath import normpath as ntpath_normpath
2525
from os import makedirs as os_makedirs
2626
from os import path as os_path
27-
from os import sep as os_sep
2827
from pathlib import Path
2928
from platform import system as platform_system
3029
from posixpath import normpath as posixpath_normpath
31-
from re import escape as re_escape
3230
from re import match as re_match
3331
from typing import Any, Optional, Union
3432

@@ -40,6 +38,16 @@
4038

4139
# Platform detection constant to avoid repeated system calls
4240
IS_WINDOWS = platform_system() == "Windows"
41+
WINDOWS_RESERVED_FILENAMES = frozenset(
42+
{
43+
"CON",
44+
"PRN",
45+
"AUX",
46+
"NUL",
47+
*(f"COM{i}" for i in range(1, 10)),
48+
*(f"LPT{i}" for i in range(1, 10)),
49+
}
50+
)
4351

4452

4553
@dataclass(frozen=True)
@@ -248,11 +256,13 @@ def create_new_vehicle_dir(new_vehicle_dir: str) -> str:
248256
@staticmethod
249257
def valid_directory_name(dir_name: str) -> bool:
250258
"""
251-
Check if a name contains only alphanumeric characters, underscores, hyphens, dots and the OS directory separator.
259+
Check if a vehicle directory name is a single safe path segment.
252260
253-
This function is designed to ensure that the directory name does not contain characters that are
254-
invalid for directory names in many operating systems. It does not guarantee that the name
255-
is valid in all contexts or operating systems, as directory name validity can vary.
261+
This function is designed to validate a user-provided vehicle name, not an
262+
arbitrary path. Path separators and traversal segments are rejected so project
263+
creation always remains under the selected base directory. Dots within a name
264+
are allowed, but `.` and `..` are rejected as standalone traversal segments.
265+
On Windows, trailing dots and reserved device names are also rejected.
256266
257267
Args:
258268
dir_name (str): The directory name to check.
@@ -261,8 +271,16 @@ def valid_directory_name(dir_name: str) -> bool:
261271
bool: True if the directory name matches the allowed pattern, False otherwise.
262272
263273
"""
264-
# Include os.sep and dot in the pattern
265-
pattern = r"^[\w" + re_escape(os_sep) + ".-]+$"
274+
if not dir_name or dir_name in {".", ".."}:
275+
return False
276+
if "/" in dir_name or "\\" in dir_name:
277+
return False
278+
if IS_WINDOWS:
279+
if dir_name.endswith("."):
280+
return False
281+
if dir_name.split(".", maxsplit=1)[0].upper() in WINDOWS_RESERVED_FILENAMES:
282+
return False
283+
pattern = r"^[\w.-]+$"
266284
return re_match(pattern, dir_name) is not None
267285

268286
@staticmethod

tests/test_backend_filesystem_program_settings.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -239,25 +239,39 @@ def test_user_can_validate_directory_names_correctly(self) -> None:
239239
GIVEN: A user needs to validate potential directory names
240240
WHEN: User checks various directory name formats
241241
THEN: Validation should correctly identify valid and invalid names
242-
AND: Platform-specific path separators should be handled appropriately
242+
AND: Path separators and traversal segments should be rejected
243243
"""
244244
# Arrange & Act & Assert: Test various directory name patterns
245245

246246
# Valid names should pass validation
247247
assert ProgramSettings.valid_directory_name("valid_dir_name-123") is True
248248
assert ProgramSettings.valid_directory_name("valid_dir_name") is True
249249

250-
# Platform-specific path separators
251-
forward_slash_valid = ProgramSettings.valid_directory_name("valid_dir_name/")
252-
assert forward_slash_valid is (platform.system() != "Windows")
253-
254-
backward_slash_valid = ProgramSettings.valid_directory_name("valid_dir_name\\")
255-
assert backward_slash_valid is (platform.system() == "Windows")
250+
# Path separators and traversal are not valid in a vehicle name
251+
assert ProgramSettings.valid_directory_name("valid_dir_name/child") is False
252+
assert ProgramSettings.valid_directory_name("valid_dir_name\\child") is False
253+
assert ProgramSettings.valid_directory_name("../escape") is False
254+
assert ProgramSettings.valid_directory_name("..") is False
255+
assert ProgramSettings.valid_directory_name(".") is False
256256

257257
# Invalid characters should fail validation
258258
assert ProgramSettings.valid_directory_name("invalid<dir>name") is False
259259
assert ProgramSettings.valid_directory_name("invalid*dir?name") is False
260260

261+
def test_windows_specific_vehicle_names_are_rejected(self) -> None:
262+
"""
263+
User receives consistent validation for Windows-incompatible vehicle names.
264+
265+
GIVEN: AMC is validating a new vehicle name for a Windows environment
266+
WHEN: The candidate name maps to a reserved device file or ends with a trailing dot
267+
THEN: Validation should fail before AMC attempts to create the directory
268+
"""
269+
with patch("ardupilot_methodic_configurator.backend_filesystem_program_settings.IS_WINDOWS", new=True):
270+
assert ProgramSettings.valid_directory_name("CON") is False
271+
assert ProgramSettings.valid_directory_name("Lpt1.param") is False
272+
assert ProgramSettings.valid_directory_name("vehicle.") is False
273+
assert ProgramSettings.valid_directory_name("vehicle.v1") is True
274+
261275

262276
class TestUserConfigurationAccess:
263277
"""Test user configuration directory access and settings management."""

tests/test_data_model_vehicle_project_creator.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -202,16 +202,16 @@ def test_user_cannot_create_project_with_nonexistent_template_directory(self, pr
202202
new_base_dir = "/valid/base/dir"
203203
new_vehicle_name = "valid_name"
204204

205-
with patch.object(LocalFilesystem, "directory_exists", return_value=False):
205+
with (
206+
patch.object(LocalFilesystem, "directory_exists", return_value=False),
207+
pytest.raises(VehicleProjectCreationError) as exc_info,
208+
):
206209
# Act & Assert: Should raise validation error
207-
with pytest.raises(VehicleProjectCreationError) as exc_info:
208-
project_creator.create_new_vehicle_from_template(
209-
template_dir, new_base_dir, new_vehicle_name, default_settings
210-
)
210+
project_creator.create_new_vehicle_from_template(template_dir, new_base_dir, new_vehicle_name, default_settings)
211211

212-
assert "Vehicle template directory" in exc_info.value.title
213-
assert "does not exist" in exc_info.value.message
214-
assert template_dir in exc_info.value.message
212+
assert "Vehicle template directory" in exc_info.value.title
213+
assert "does not exist" in exc_info.value.message
214+
assert template_dir in exc_info.value.message
215215

216216
def test_user_cannot_create_project_with_empty_vehicle_name(self, project_creator, default_settings) -> None:
217217
"""
@@ -226,15 +226,15 @@ def test_user_cannot_create_project_with_empty_vehicle_name(self, project_creato
226226
new_base_dir = "/valid/base/dir"
227227
new_vehicle_name = ""
228228

229-
with patch.object(LocalFilesystem, "directory_exists", return_value=True):
229+
with (
230+
patch.object(LocalFilesystem, "directory_exists", return_value=True),
231+
pytest.raises(VehicleProjectCreationError) as exc_info,
232+
):
230233
# Act & Assert: Should raise validation error
231-
with pytest.raises(VehicleProjectCreationError) as exc_info:
232-
project_creator.create_new_vehicle_from_template(
233-
template_dir, new_base_dir, new_vehicle_name, default_settings
234-
)
234+
project_creator.create_new_vehicle_from_template(template_dir, new_base_dir, new_vehicle_name, default_settings)
235235

236-
assert "New vehicle directory" in exc_info.value.title
237-
assert "must not be empty" in exc_info.value.message
236+
assert "New vehicle directory" in exc_info.value.title
237+
assert "must not be empty" in exc_info.value.message
238238

239239
def test_user_cannot_create_project_with_invalid_vehicle_name(self, project_creator, default_settings) -> None:
240240
"""
@@ -263,6 +263,28 @@ def test_user_cannot_create_project_with_invalid_vehicle_name(self, project_crea
263263
assert "invalid characters" in exc_info.value.message
264264
assert new_vehicle_name in exc_info.value.message
265265

266+
def test_user_cannot_create_project_with_traversal_style_vehicle_name(self, project_creator, default_settings) -> None:
267+
"""
268+
User receives validation error when vehicle name contains path traversal syntax.
269+
270+
GIVEN: A user attempts to create a vehicle project
271+
WHEN: They provide a vehicle name that looks like a relative path
272+
THEN: AMC rejects it before any filesystem changes are attempted
273+
"""
274+
template_dir = "/valid/template/dir"
275+
new_base_dir = "/valid/base/dir"
276+
new_vehicle_name = "../escape"
277+
278+
with (
279+
patch.object(LocalFilesystem, "directory_exists", return_value=True),
280+
pytest.raises(VehicleProjectCreationError) as exc_info,
281+
):
282+
project_creator.create_new_vehicle_from_template(template_dir, new_base_dir, new_vehicle_name, default_settings)
283+
284+
assert "New vehicle directory" in exc_info.value.title
285+
assert "invalid characters" in exc_info.value.message
286+
project_creator.local_filesystem.create_new_vehicle_dir.assert_not_called()
287+
266288

267289
class TestVehicleProjectCreationWorkflow:
268290
"""Test complete vehicle project creation workflows."""

0 commit comments

Comments
 (0)