From 2be334922abd6635b8c8e50ee6ed0a217b0ecc4d Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Wed, 22 Apr 2026 20:09:12 +0200 Subject: [PATCH] user/path value check: add support for no-unicode --- radicale/app/__init__.py | 17 +++++++++++++++-- radicale/app/base.py | 11 ++++++++--- radicale/storage/multifilesystem/__init__.py | 6 ++++-- radicale/storage/multifilesystem/base.py | 4 ++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index 26f27e46..2b2f06c2 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -163,8 +163,21 @@ class Application(ApplicationPartDelete, ApplicationPartHead, # Format checks self._validate_user_value = configuration.get("server", "validate_user_value") self._validate_path_value = configuration.get("server", "validate_path_value") - logger.info("validate user value: %r", self._validate_user_value) - logger.info("validate path value: %r", self._validate_path_value) + if not self._storage._filesystem_root_folder_supports_unicode: + if self._validate_user_value not in ["strict", "no-unicode"]: + self._validate_user_value = "no-unicode" + if self._validate_path_value not in ["strict", "no-unicode"]: + self._validate_path_value = "no-unicode" + if not self._storage._filesystem_root_folder_supports_problematic_chars or not self._storage._filesystem_root_folder_supports_trailing_whitespace: + if self._validate_user_value not in ["strict"]: + self._validate_user_value = "strict" + if self._validate_path_value not in ["strict"]: + self._validate_path_value = "strict" + logger.notice("validate user value: %r (enforced by missing support of collection storage)", self._validate_user_value) + logger.notice("validate path value: %r (enforced by missing support of collection storage)", self._validate_path_value) + else: + logger.info("validate user value: %r", self._validate_user_value) + logger.info("validate path value: %r", self._validate_path_value) # Profiling options self._profiling = configuration.get("logging", "profiling") self._profiling_per_request_min_duration = configuration.get("logging", "profiling_per_request_min_duration") diff --git a/radicale/app/base.py b/radicale/app/base.py index dd6a68bd..453515cf 100644 --- a/radicale/app/base.py +++ b/radicale/app/base.py @@ -124,23 +124,28 @@ class ApplicationBase: validation_type: str, ) -> bool: check_minimal = (validation_type == "minimal") - check_unicode = (validation_type == "unicodeletter") - logger.trace("_check_format investigate %r (validation_type=%r check_minimal=%s check_unicode=%s)", string, validation_type, check_minimal, check_unicode) + check_unicode_letter = (validation_type == "unicode-letter") + check_no_unicode = (validation_type == "no-unicode") + logger.trace("_check_format investigate %r (validation_type=%r check_minimal=%s check_unicode_letter=%s check_no_unicode=%s)", string, validation_type, check_minimal, check_unicode_letter, check_no_unicode) for c in string: if c <= chr(31) or (c >= chr(127) and c <= chr(159)): # ASCII: control char return False if unicodedata.category(c)[0] == "C": + # https://unicodeplus.com/category # Unicode: control return False if check_minimal: if c in blacklist_minimal: logger.trace("_check_format found %r", c) return False - elif check_unicode: + elif check_unicode_letter: if c not in whitelist_unicode: if unicodedata.category(c)[0] != "L": return False + elif check_no_unicode: + if ord(c) > 255: + return False return True def _check_user_format(self, user: str) -> bool: diff --git a/radicale/storage/multifilesystem/__init__.py b/radicale/storage/multifilesystem/__init__.py index 82bb6cc4..b695b09a 100644 --- a/radicale/storage/multifilesystem/__init__.py +++ b/radicale/storage/multifilesystem/__init__.py @@ -177,13 +177,15 @@ class Storage( filesystem_root_folder_is_collision_free_no_short_filename = pathutils.path_is_collision_free_no_short_filename(self._get_collection_root_folder()) self._filesystem_root_folder_is_collision_free = filesystem_root_folder_is_collision_free_case_sensitive and filesystem_root_folder_is_collision_free_no_short_filename self._filesystem_root_folder_supports_unicode = pathutils.path_supports_unicode(self._get_collection_root_folder()) + self._filesystem_root_folder_supports_trailing_whitespace = pathutils.path_supports_trailing_whitespace(self._get_collection_root_folder()) + self._filesystem_root_folder_supports_problematic_chars = pathutils.path_supports_problematic_chars(self._get_collection_root_folder()) logger.info("Storage location subfolder is collision free: %s (case-sensitive=%s no-short-filename=%s)", self._filesystem_root_folder_is_collision_free, filesystem_root_folder_is_collision_free_case_sensitive, filesystem_root_folder_is_collision_free_no_short_filename) logger.info("Storage location subfolder supports unicode: %s", self._filesystem_root_folder_supports_unicode) - logger.info("Storage location subfolder supports trailing whitespace: %s", pathutils.path_supports_trailing_whitespace(self._get_collection_root_folder())) - logger.info("Storage location subfolder supports problematic chars: %s", pathutils.path_supports_problematic_chars(self._get_collection_root_folder())) + logger.info("Storage location subfolder supports trailing whitespace: %s", self._filesystem_root_folder_supports_trailing_whitespace) + logger.info("Storage location subfolder supports problematic chars: %s", self._filesystem_root_folder_supports_problematic_chars) logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item) logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history) logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken) diff --git a/radicale/storage/multifilesystem/base.py b/radicale/storage/multifilesystem/base.py index 515f39d7..40533b89 100644 --- a/radicale/storage/multifilesystem/base.py +++ b/radicale/storage/multifilesystem/base.py @@ -34,6 +34,8 @@ class CollectionBase(storage.BaseCollection): _filesystem_path: str _filesystem_root_folder_is_collision_free: bool _filesystem_root_folder_supports_unicode: bool + _filesystem_root_folder_supports_trailing_whitespace: bool + _filesystem_root_folder_supports_problematic_chars: bool def __init__(self, storage_: "multifilesystem.Storage", path: str, filesystem_path: Optional[str] = None) -> None: @@ -46,6 +48,8 @@ class CollectionBase(storage.BaseCollection): self._skip_broken_item = storage_.configuration.get("storage", "skip_broken_item") self._filesystem_root_folder_is_collision_free = storage_._filesystem_root_folder_is_collision_free self._filesystem_root_folder_supports_unicode = storage_._filesystem_root_folder_supports_unicode + self._filesystem_root_folder_supports_trailing_whitespace = storage_._filesystem_root_folder_supports_trailing_whitespace + self._filesystem_root_folder_supports_problematic_chars = storage_._filesystem_root_folder_supports_problematic_chars if filesystem_path is None: filesystem_path = pathutils.path_to_filesystem(folder, self.path, self._filesystem_root_folder_is_collision_free) self._filesystem_path = filesystem_path