From 3d299b0b90ed06ba9941ed8b978ba67c2f04f82c Mon Sep 17 00:00:00 2001 From: Tsuyoshi Hombashi Date: Thu, 22 Aug 2024 22:53:15 +0900 Subject: [PATCH 1/3] Apply formatter Signed-off-by: Tsuyoshi Hombashi --- examples/pathvalidate_examples.ipynb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/pathvalidate_examples.ipynb b/examples/pathvalidate_examples.ipynb index 5d03abf..f9ab943 100644 --- a/examples/pathvalidate_examples.ipynb +++ b/examples/pathvalidate_examples.ipynb @@ -30,14 +30,14 @@ "from pathvalidate import ValidationError, validate_filename\n", "\n", "try:\n", - " validate_filename(\"fi:l*e/p\\\"a?t>h|.th|.th|.th|.th|.t {sanitize_filename(fname)}\\n\")\n", "\n", "fname = \"\\0_a*b:ce%f/(g)h+i_0.txt\"\n", @@ -131,7 +131,7 @@ "source": [ "from pathvalidate import sanitize_filepath\n", "\n", - "fpath = \"fi:l*e/p\\\"a?t>h|.t {sanitize_filepath(fpath)}\\n\")\n", "\n", "fpath = \"\\0_a*b:ce%f/(g)h+i_0.txt\"\n", @@ -177,7 +177,7 @@ "source": [ "from pathvalidate import is_valid_filename, sanitize_filename\n", "\n", - "fname = \"fi:l*e/p\\\"a?t>h|.th|.t str:\n", " if e.reusable_name:\n", " return e.reserved_name\n", "\n", " return f\"{e.reserved_name}_\"\n", "\n", - "sanitize_filename(\".\", reserved_name_handler=add_trailing_underscore, additional_reserved_names=[\".\"])\n" + "\n", + "sanitize_filename(\n", + " \".\", reserved_name_handler=add_trailing_underscore, additional_reserved_names=[\".\"]\n", + ")" ] } ], From d1c58d2c6f2a6b8d9e316ff21c5fd572766833a7 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Hombashi Date: Thu, 22 Aug 2024 22:53:50 +0900 Subject: [PATCH 2/3] chore: update sigstore/gh-action-sigstore-python to v3.0.0 Signed-off-by: Tsuyoshi Hombashi --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce2e30d..052d731 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: path: ./dist - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v2.1.1 + uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: >- ./dist/*.tar.gz From 9c0b77ef1f75b2eeca31a23c71153a2901b47b65 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Hombashi Date: Fri, 23 Aug 2024 02:09:54 +0900 Subject: [PATCH 3/3] Fix validation functions of filepaths: #39 - If `platform` argument is `windows` or `universal`, filepaths ending with a space or a period are should be detected as an error - Fix POSIX-style absolute paths were not detected as errors with `platform="windows"` on Python 3.12 and below Signed-off-by: Tsuyoshi Hombashi --- pathvalidate/_common.py | 15 +++++++++++++++ pathvalidate/_filename.py | 7 ++----- pathvalidate/_filepath.py | 40 +++++++++++++++++++-------------------- test/test_filename.py | 31 ++++++++++-------------------- test/test_filepath.py | 24 ++++++++++++++++------- 5 files changed, 64 insertions(+), 53 deletions(-) diff --git a/pathvalidate/_common.py b/pathvalidate/_common.py index 066e5b1..47620a3 100644 --- a/pathvalidate/_common.py +++ b/pathvalidate/_common.py @@ -2,9 +2,11 @@ .. codeauthor:: Tsuyoshi Hombashi """ +import ntpath import platform import re import string +import sys from pathlib import PurePath from typing import Any, List, Optional @@ -39,6 +41,19 @@ def to_str(name: PathType) -> str: return name +def is_nt_abspath(value: str) -> bool: + ver_info = sys.version_info[:2] + if ver_info <= (3, 10): + if value.startswith("\\\\"): + return True + elif ver_info >= (3, 13): + return ntpath.isabs(value) + + drive, _tail = ntpath.splitdrive(value) + + return ntpath.isabs(value) and len(drive) > 0 + + def is_null_string(value: Any) -> bool: if value is None: return True diff --git a/pathvalidate/_filename.py b/pathvalidate/_filename.py index 1b2168f..d9a1453 100644 --- a/pathvalidate/_filename.py +++ b/pathvalidate/_filename.py @@ -3,7 +3,6 @@ """ import itertools -import ntpath import posixpath import re import warnings @@ -11,7 +10,7 @@ from typing import Optional, Pattern, Sequence, Tuple from ._base import AbstractSanitizer, AbstractValidator, BaseFile, BaseValidator -from ._common import findall_to_str, to_str, truncate_str, validate_pathtype +from ._common import findall_to_str, is_nt_abspath, to_str, truncate_str, validate_pathtype from ._const import DEFAULT_MIN_LEN, INVALID_CHAR_ERR_MSG_TMPL, Platform from ._types import PathType, PlatformType from .error import ErrorAttrKey, ErrorReason, InvalidCharError, ValidationError @@ -55,7 +54,6 @@ def __init__( null_value_handler=null_value_handler, reserved_name_handler=reserved_name_handler, additional_reserved_names=additional_reserved_names, - platform_max_len=_DEFAULT_MAX_FILENAME_LEN, platform=platform, validate_after_sanitize=validate_after_sanitize, validator=fname_validator, @@ -161,7 +159,6 @@ def __init__( fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, - platform_max_len=_DEFAULT_MAX_FILENAME_LEN, platform=platform, ) @@ -208,7 +205,7 @@ def validate_abspath(self, value: str) -> None: ) if self._is_windows(include_universal=True): - if ntpath.isabs(value): + if is_nt_abspath(value): raise err if posixpath.isabs(value): diff --git a/pathvalidate/_filepath.py b/pathvalidate/_filepath.py index 38192a8..808d589 100644 --- a/pathvalidate/_filepath.py +++ b/pathvalidate/_filepath.py @@ -11,7 +11,7 @@ from typing import List, Optional, Pattern, Sequence, Tuple from ._base import AbstractSanitizer, AbstractValidator, BaseFile, BaseValidator -from ._common import findall_to_str, to_str, validate_pathtype +from ._common import findall_to_str, is_nt_abspath, to_str, validate_pathtype from ._const import _NTFS_RESERVED_FILE_NAMES, DEFAULT_MIN_LEN, INVALID_CHAR_ERR_MSG_TMPL, Platform from ._filename import FileNameSanitizer, FileNameValidator from ._types import PathType, PlatformType @@ -178,7 +178,8 @@ def __init__( self.__fname_validator = FileNameValidator( min_len=min_len, - max_len=max_len, + max_len=self.max_len, + fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, platform=platform, @@ -229,7 +230,7 @@ def validate(self, value: PathType) -> None: if not entry or entry in (".", ".."): continue - self.__fname_validator._validate_reserved_keywords(entry) + self.__fname_validator.validate(entry) if self._is_windows(include_universal=True): self.__validate_win_filepath(unicode_filepath) @@ -238,7 +239,18 @@ def validate(self, value: PathType) -> None: def validate_abspath(self, value: PathType) -> None: is_posix_abs = posixpath.isabs(value) - is_nt_abs = ntpath.isabs(value) + is_nt_abs = is_nt_abspath(to_str(value)) + + if any([self._is_windows() and is_nt_abs, self._is_posix() and is_posix_abs]): + return + + if self._is_universal() and any([is_nt_abs, is_posix_abs]): + ValidationError( + "platform-independent absolute file path is not supported", + platform=self.platform, + reason=ErrorReason.MALFORMED_ABS_PATH, + ) + err_object = ValidationError( description=( "an invalid absolute file path ({}) for the platform ({}).".format( @@ -251,25 +263,13 @@ def validate_abspath(self, value: PathType) -> None: reason=ErrorReason.MALFORMED_ABS_PATH, ) - if any([self._is_windows() and is_nt_abs, self._is_linux() and is_posix_abs]): - return - - if self._is_universal() and any([is_posix_abs, is_nt_abs]): - ValidationError( - description=( - ("POSIX style" if is_posix_abs else "NT style") - + " absolute file path found. expected a platform-independent file path." - ), - platform=self.platform, - reason=ErrorReason.MALFORMED_ABS_PATH, - ) - if self._is_windows(include_universal=True) and is_posix_abs: raise err_object - drive, _tail = ntpath.splitdrive(value) - if not self._is_windows() and drive and is_nt_abs: - raise err_object + if not self._is_windows(): + drive, _tail = ntpath.splitdrive(value) + if drive and is_nt_abs: + raise err_object def __validate_unix_filepath(self, unicode_filepath: str) -> None: match = _RE_INVALID_PATH.findall(unicode_filepath) diff --git a/test/test_filename.py b/test/test_filename.py index 176b647..0aab9cc 100644 --- a/test/test_filename.py +++ b/test/test_filename.py @@ -397,28 +397,32 @@ def test_normal_additional_reserved_names(self, value, arn, expected): assert is_valid_filename(value, additional_reserved_names=arn) == expected @pytest.mark.parametrize( - ["platform", "value", "expected"], + ["platform", "value", "expected", "reason"], [ - [win_abspath, platform, None] + [win_abspath, platform, None, None] for win_abspath, platform in product( ["linux", "macos", "posix"], ["\\", "\\\\", "\\ ", "C:\\", "c:\\", "\\xyz", "\\xyz "], ) ] + [ - [win_abspath, platform, ValidationError] + [win_abspath, platform, ValidationError, ErrorReason.FOUND_ABS_PATH] + for win_abspath, platform in product(["windows", "universal"], ["\\\\", "C:\\", "c:\\"]) + ] + + [ + [win_abspath, platform, ValidationError, ErrorReason.INVALID_CHARACTER] for win_abspath, platform in product( - ["windows", "universal"], ["\\", "\\\\", "\\ ", "C:\\", "c:\\", "\\xyz", "\\xyz "] + ["windows", "universal"], ["\\", "\\ ", "\\xyz", "\\xyz "] ) ], ) - def test_win_abs_path(self, platform, value, expected): + def test_win_abs_path(self, platform, value, expected, reason): if expected is None: validate_filename(value, platform=platform) else: with pytest.raises(expected) as e: validate_filename(value, platform=platform) - assert e.value.reason == ErrorReason.FOUND_ABS_PATH + assert e.value.reason == reason @pytest.mark.parametrize( ["value", "platform"], @@ -465,21 +469,6 @@ def test_exception_null_value(self, value, expected): validate_filename(value) assert not is_valid_filename(value) - @pytest.mark.parametrize( - ["value", "expected"], - [ - ["a" * 256, ValidationError], - [1, TypeError], - [True, TypeError], - [nan, TypeError], - [inf, TypeError], - ], - ) - def test_exception(self, value, expected): - with pytest.raises(expected): - validate_filename(value) - assert not is_valid_filename(value) - class Test_sanitize_filename: SANITIZE_CHARS = INVALID_WIN_FILENAME_CHARS + unprintable_ascii_chars diff --git a/test/test_filepath.py b/test/test_filepath.py index ece45cc..b71af25 100644 --- a/test/test_filepath.py +++ b/test/test_filepath.py @@ -289,7 +289,7 @@ def test_minmax_len(self, value, min_len, max_len, expected): [ ["linux", "/a/b/c.txt", None], ["linux", "C:\\a\\b\\c.txt", ValidationError], - ["windows", "/a/b/c.txt", None], + ["windows", "/a/b/c.txt", ValidationError], ["windows", "C:\\a\\b\\c.txt", None], ["universal", "/a/b/c.txt", ValidationError], ["universal", "C:\\a\\b\\c.txt", ValidationError], @@ -360,6 +360,18 @@ def test_relative_path(self, test_platform, value, expected): with pytest.raises(expected): validate_filepath(value, platform=test_platform) + @pytest.mark.parametrize( + ["platform", "value"], + [ + ["linux", "period."], + ["linux", "space "], + ["linux", "space_and_period. "], + ], + ) + def test_normal_space_or_period_at_tail(self, platform, value): + validate_filepath(value, platform=platform) + assert is_valid_filepath(value, platform=platform) + @pytest.mark.parametrize( ["platform", "value"], [ @@ -367,17 +379,15 @@ def test_relative_path(self, test_platform, value, expected): ["windows", "space "], ["windows", "space_and_period ."], ["windows", "space_and_period. "], - ["linux", "period."], - ["linux", "space "], - ["linux", "space_and_period. "], ["universal", "period."], ["universal", "space "], ["universal", "space_and_period ."], ], ) - def test_normal_space_or_period_at_tail(self, platform, value): - validate_filepath(value, platform=platform) - assert is_valid_filepath(value, platform=platform) + def test_exception_space_or_period_at_tail(self, platform, value): + with pytest.raises(ValidationError): + validate_filepath(value, platform=platform) + assert not is_valid_filepath(value, platform=platform) @pytest.mark.skipif(not is_faker_installed(), reason="requires faker") @pytest.mark.parametrize(