Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix validation functions of filepaths #55

Merged
merged 3 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <tsuyoshi.hombashi@gmail.com>
"""

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
Loading