Skip to content

Commit

Permalink
Merge pull request #55 from thombashi/fix_validate_filepath
Browse files Browse the repository at this point in the history
Fix validation functions of filepaths
  • Loading branch information
thombashi authored Aug 22, 2024
2 parents 1f0eb57 + 9c0b77e commit 26e6d67
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 62 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions examples/pathvalidate_examples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@
"from pathvalidate import ValidationError, validate_filename\n",
"\n",
"try:\n",
" validate_filename(\"fi:l*e/p\\\"a?t>h|.t<xt\")\n",
" validate_filename('fi:l*e/p\"a?t>h|.t<xt')\n",
"except ValidationError as e:\n",
" print(f\"{e}\\n\", file=sys.stderr)\n",
"\n",
"try:\n",
" validate_filename(\"COM1\")\n",
"except ValidationError as e:\n",
" print(f\"{e}\\n\", file=sys.stderr)\n"
" print(f\"{e}\\n\", file=sys.stderr)"
]
},
{
Expand Down Expand Up @@ -81,7 +81,7 @@
"from pathvalidate import ValidationError, validate_filepath\n",
"\n",
"try:\n",
" validate_filepath(\"fi:l*e/p\\\"a?t>h|.t<xt\")\n",
" validate_filepath('fi:l*e/p\"a?t>h|.t<xt')\n",
"except ValidationError as e:\n",
" print(e, file=sys.stderr)"
]
Expand All @@ -105,7 +105,7 @@
"source": [
"from pathvalidate import sanitize_filename\n",
"\n",
"fname = \"fi:l*e/p\\\"a?t>h|.t<xt\"\n",
"fname = 'fi:l*e/p\"a?t>h|.t<xt'\n",
"print(f\"{fname} -> {sanitize_filename(fname)}\\n\")\n",
"\n",
"fname = \"\\0_a*b:c<d>e%f/(g)h+i_0.txt\"\n",
Expand All @@ -131,7 +131,7 @@
"source": [
"from pathvalidate import sanitize_filepath\n",
"\n",
"fpath = \"fi:l*e/p\\\"a?t>h|.t<xt\"\n",
"fpath = 'fi:l*e/p\"a?t>h|.t<xt'\n",
"print(f\"{fpath} -> {sanitize_filepath(fpath)}\\n\")\n",
"\n",
"fpath = \"\\0_a*b:c<d>e%f/(g)h+i_0.txt\"\n",
Expand Down Expand Up @@ -177,7 +177,7 @@
"source": [
"from pathvalidate import is_valid_filename, sanitize_filename\n",
"\n",
"fname = \"fi:l*e/p\\\"a?t>h|.t<xt\"\n",
"fname = 'fi:l*e/p\"a?t>h|.t<xt'\n",
"print(f\"is_valid_filename('{fname}') return {is_valid_filename(fname)}\\n\")\n",
"\n",
"sanitized_fname = sanitize_filename(fname)\n",
Expand All @@ -203,7 +203,7 @@
"source": [
"from pathvalidate import is_valid_filepath, sanitize_filepath\n",
"\n",
"fpath = \"fi:l*e/p\\\"a?t>h|.t<xt\"\n",
"fpath = 'fi:l*e/p\"a?t>h|.t<xt'\n",
"print(f\"is_valid_filepath('{fpath}') return {is_valid_filepath(fpath)}\\n\")\n",
"\n",
"sanitized_fpath = sanitize_filepath(fpath)\n",
Expand All @@ -229,13 +229,17 @@
"source": [
"from pathvalidate import sanitize_filename, ValidationError\n",
"\n",
"\n",
"def add_trailing_underscore(e: ValidationError) -> 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",
")"
]
}
],
Expand Down
15 changes: 15 additions & 0 deletions pathvalidate/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
.. codeauthor:: Tsuyoshi Hombashi <[email protected]>
"""

import ntpath
import platform
import re
import string
import sys
from pathlib import PurePath
from typing import Any, List, Optional

Expand Down Expand Up @@ -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
Expand Down
7 changes: 2 additions & 5 deletions pathvalidate/_filename.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
"""

import itertools
import ntpath
import posixpath
import re
import warnings
from pathlib import Path, PurePath
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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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):
Expand Down
40 changes: 20 additions & 20 deletions pathvalidate/_filepath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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)
Expand Down
31 changes: 10 additions & 21 deletions test/test_filename.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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
Expand Down
24 changes: 17 additions & 7 deletions test/test_filepath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -360,24 +360,34 @@ 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"],
[
["windows", "period."],
["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(
Expand Down

0 comments on commit 26e6d67

Please sign in to comment.