Skip to content

Commit 58fca7c

Browse files
authored
Merge pull request #534 from MerginMaps/refuse_trailing_whitespace_in_path
Refuse trailing whitespace in path
2 parents f8f718c + 6dc9bb7 commit 58fca7c

File tree

5 files changed

+41
-22
lines changed

5 files changed

+41
-22
lines changed

server/mergin/sync/files.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
from .utils import (
1515
is_file_name_blacklisted,
16-
is_qgis,
1716
is_supported_extension,
1817
is_valid_path,
1918
is_versioned_file,
19+
has_trailing_space,
2020
)
2121
from ..app import DateTimeWithZ, ma
2222

@@ -212,14 +212,21 @@ def validate(self, data, **kwargs):
212212

213213
if not is_valid_path(file_path):
214214
raise ValidationError(
215-
f"Unsupported file name detected: {file_path}. Please remove the invalid characters."
215+
f"Unsupported file name detected: '{file_path}'. Please remove the invalid characters."
216216
)
217217

218218
if not is_supported_extension(file_path):
219219
raise ValidationError(
220-
f"Unsupported file type detected: {file_path}. "
220+
f"Unsupported file type detected: '{file_path}'. "
221221
f"Please remove the file or try compressing it into a ZIP file before uploading.",
222222
)
223+
# new checks must restrict only new files not to block existing projects
224+
for file in data["added"]:
225+
file_path = file["path"]
226+
if has_trailing_space(file_path):
227+
raise ValidationError(
228+
f"Folder name contains a trailing space. Please remove the space in: '{file_path}'."
229+
)
223230

224231

225232
class ProjectFileSchema(FileSchema):

server/mergin/sync/public_api_controller.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,7 @@
5555
from .files import (
5656
ProjectFileChange,
5757
ChangesSchema,
58-
UploadFileSchema,
5958
ProjectFileSchema,
60-
FileSchema,
6159
files_changes_from_upload,
6260
mergin_secure_filename,
6361
)
@@ -83,17 +81,11 @@
8381
generate_checksum,
8482
Toucher,
8583
get_x_accel_uri,
86-
is_file_name_blacklisted,
8784
get_ip,
8885
get_user_agent,
8986
generate_location,
9087
is_valid_uuid,
91-
is_versioned_file,
92-
get_project_path,
9388
get_device_id,
94-
is_valid_path,
95-
is_supported_type,
96-
is_supported_extension,
9789
get_mimetype,
9890
wkb2wkt,
9991
)
@@ -980,7 +972,7 @@ def push_finish(transaction_id):
980972
if len(unsupported_files):
981973
abort(
982974
400,
983-
f"Unsupported file type detected: {unsupported_files[0]}. "
975+
f"Unsupported file type detected: '{unsupported_files[0]}'. "
984976
f"Please remove the file or try compressing it into a ZIP file before uploading.",
985977
)
986978

server/mergin/sync/utils.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
import magic
2727
from flask import current_app
28+
from pathlib import Path
2829

2930

3031
def generate_checksum(file, chunk_size=4096):
@@ -100,11 +101,6 @@ def is_qgis(path: str) -> bool:
100101
return ext.lower() in [".qgs", ".qgz"]
101102

102103

103-
def int_version(version):
104-
"""Convert v<n> format of version to integer representation."""
105-
return int(version.lstrip("v")) if re.match(r"v\d", version) else None
106-
107-
108104
def is_versioned_file(file):
109105
"""Check if file is compatible with geodiff lib and hence suitable for versioning."""
110106
diff_extensions = [".gpkg", ".sqlite"]
@@ -338,14 +334,19 @@ def files_size():
338334
def is_valid_path(filepath: str) -> bool:
339335
"""Check filepath and filename for invalid characters, absolute path or path traversal"""
340336
return (
341-
not len(re.split(r"\.[/\\]", filepath)) > 1 # ./ or .\
337+
not re.search(r"\.[/\\]", filepath) # ./ or .\
342338
and is_valid_filepath(filepath) # invalid characters in filepath, absolute path
343339
and is_valid_filename(
344340
os.path.basename(filepath)
345341
) # invalid characters in filename, reserved filenames
346342
)
347343

348344

345+
def has_trailing_space(filepath: str) -> bool:
346+
"""Check filepath for trailing spaces that makes the project impossible to download on Windows"""
347+
return any(part != part.rstrip() for part in Path(filepath).parts)
348+
349+
349350
def is_supported_extension(filepath) -> bool:
350351
"""Check whether file's extension is supported."""
351352
ext = os.path.splitext(filepath)[1].lower()

server/mergin/tests/test_project_controller.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,7 +2495,7 @@ def test_filepath_manipulation(client):
24952495
assert resp.status_code == 400
24962496
assert (
24972497
resp.json["detail"]
2498-
== f"Unsupported file name detected: {manipulated_path}. Please remove the invalid characters."
2498+
== f"Unsupported file name detected: '{manipulated_path}'. Please remove the invalid characters."
24992499
)
25002500

25012501

@@ -2528,7 +2528,7 @@ def test_supported_file_upload(client):
25282528
assert resp.status_code == 400
25292529
assert (
25302530
resp.json["detail"]
2531-
== f"Unsupported file type detected: {script_filename}. Please remove the file or try compressing it into a ZIP file before uploading."
2531+
== f"Unsupported file type detected: '{script_filename}'. Please remove the file or try compressing it into a ZIP file before uploading."
25322532
)
25332533
# Extension spoofing to trick the validator
25342534
spoof_name = "script.gpkg"
@@ -2567,7 +2567,7 @@ def test_supported_file_upload(client):
25672567
assert resp.status_code == 400
25682568
assert (
25692569
resp.json["detail"]
2570-
== f"Unsupported file type detected: {spoof_name}. Please remove the file or try compressing it into a ZIP file before uploading."
2570+
== f"Unsupported file type detected: '{spoof_name}'. Please remove the file or try compressing it into a ZIP file before uploading."
25712571
)
25722572

25732573

server/mergin/tests/test_utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import base64
66
from datetime import datetime
77
import json
8-
import os
98
import pytest
109
from flask import url_for, current_app
1110
from sqlalchemy import desc
1211
import os
1312
from unittest.mock import patch
1413
from pathvalidate import sanitize_filename
1514
from pygeodiff import GeoDiff
15+
from pathlib import PureWindowsPath
1616

1717
from ..utils import save_diagnostic_log_file
1818

@@ -24,6 +24,7 @@
2424
is_valid_path,
2525
get_x_accel_uri,
2626
wkb2wkt,
27+
has_trailing_space,
2728
)
2829
from ..auth.models import LoginHistory, User
2930
from . import json_headers
@@ -228,6 +229,24 @@ def test_is_valid_path(client, filepath, allow):
228229
assert is_valid_path(filepath) == allow
229230

230231

232+
trailing_spaces_paths = [
233+
("photos /lutraHQ.jpg", "posix", True),
234+
("photo s/ lutraHQ.jpg", "posix", False),
235+
("assets\photos \lutraHQ.jpg", "windows", True),
236+
("assets\ photos\lutraHQ.jpg", "windows", False),
237+
]
238+
239+
240+
@pytest.mark.parametrize("path,path_platform,result", trailing_spaces_paths)
241+
def test_has_trailing_space(path, path_platform, result):
242+
if path_platform == "windows":
243+
# we must mock Path to instantiate as Windows path
244+
with patch("mergin.sync.utils.Path", PureWindowsPath):
245+
assert has_trailing_space(path) is result
246+
else:
247+
assert has_trailing_space(path) is result
248+
249+
231250
def test_get_x_accell_uri(client):
232251
"""Test get_x_accell_uri"""
233252
client.application.config["LOCAL_PROJECTS"] = "/data/"

0 commit comments

Comments
 (0)