tl; dr
Вызовите функцию is_path_exists_or_creatable()
, определенную ниже.
Строго Python 3. Вот как мы катимся.
Повесть о двух вопросах
Вопрос «Как проверить правильность имени пути и, для допустимых имен пути, существование или возможность записи этих путей?»это явно два отдельных вопроса.Оба интересны, и ни один из них не получил действительно удовлетворительного ответа здесь ... или, ну, где-нибудь , что я мог бы grep.
vikki '* ответ , вероятно, имеет наиболее близкие значения, но имеет замечательные недостатки:
- Необязательно открывать ( ... и затем не удается надежно закрыть ) файловые дескрипторы.
- Бесполезная запись ( ... и невозможность надежного закрытия или удаления ) 0-байтовых файлов.
- Игнорирование специфических для ОС ошибок, различающих неустранимые неверные пути и игнорируемую файловую системупроблемы.Неудивительно, что это важно для Windows.( См. Ниже. )
- Игнорирование условий гонки, возникающих из-за внешних процессов, одновременно (пере) перемещающих родительские каталоги проверяемого пути.( См. Ниже. )
- Игнорирование тайм-аутов соединения, возникающих из-за того, что это имя пути находится в устаревших, медленных или временно недоступных по другим причинам файловых системах.Это может подвергать общедоступные службы потенциальным DoS атакам.( См. Ниже. )
Мы исправим все это.
Вопрос № 0: Что такое валидность пути снова?
Дошвыряя наши хрупкие мясные костюмы в пронизанные питоном мошпиты боли, мы, вероятно, должны определить, что мы подразумеваем под «валидностью пути».Что именно определяет валидность?
Под «валидностью пути» мы понимаем синтаксическую корректность пути по отношению к корневой файловой системе текущей системы - независимо оттого, существует ли этот путь или его родительские каталоги физически.В соответствии с этим определением путь синтаксически корректен, если он соответствует всем синтаксическим требованиям корневой файловой системы.
Под «корневой файловой системой» мы подразумеваем:
- В POSIX-совместимых системах,файловая система, смонтированная в корневой каталог (
/
). - В Windows файловая система смонтирована на
%HOMEDRIVE%
, буква диска с суффиксом двоеточия содержит текущую установку Windows (обычно, но не * 1066)* обязательно C:
).
Значение «синтаксической правильности», в свою очередь, зависит от типа корневой файловой системы.Для ext4
(и большинства, но не всех POSIX-совместимых) файловых систем синтаксически правильное имя пути, если и только если этот путь:
- Не содержит нулевых байтов (т. Е.
\x00
в Python). Это жесткое требование для всех POSIX-совместимых файловых систем. - Не содержит компонентов пути длиннее 255 байтов (например,
'a'*256
в Python).Компонент пути - это самая длинная подстрока пути, не содержащая символов /
(например, bergtatt
, ind
, i
и fjeldkamrene
в имени пути /bergtatt/ind/i/fjeldkamrene
).
Синтаксическая правильность.Корневая файловая система.Вот и все.
Вопрос № 1: Как теперь мы будем делать валидность пути?
Проверка имен пути в Python на удивление не интуитивна.Я полностью согласен с Fake Name здесь: официальный пакет os.path
должен обеспечить готовое решение для этого.По неизвестным (и, вероятно, неубедительным) причинам, это не так.К счастью, развертывание вашего собственного специального решения не , что мучительно ...
ОК, на самом деле это так. Это волосатое тело;это противно;это, вероятно, хихикает, поскольку это гремит и хихикает, поскольку это пылает.Но что ты собираешься делать? Nuthin '.
Мы скоро спустимся в радиоактивную пропасть низкоуровневого кода.Но сначала поговорим о магазине высокого уровня.Стандартные функции os.stat()
и os.lstat()
вызывают следующие исключения, когда передаются неверные имена путей:
- Для имен путей, находящихся в несуществующих каталогах, экземпляры
FileNotFoundError
. - Для имен путей, находящихся в существующих каталогах:
- Под Windows, экземпляры
WindowsError
, чей атрибут winerror
равен123
(т. Е. ERROR_INVALID_NAME
). - Под всеми другими ОС:
- Для путей, содержащих нулевые байты (т. Е.
'\x00'
), экземпляры TypeError
. - Для имен путей, содержащих компоненты пути длиннее 255 байт, экземпляры
OSError
, атрибут errcode
которых: - Под SunOS и ОС семейства * BSD,
errno.ERANGE
.(Похоже, это ошибка на уровне ОС, иначе называемая «выборочной интерпретацией» стандарта POSIX.) - Во всех других ОС
errno.ENAMETOOLONG
.
Важно, что это означает, что только пути, находящиеся в существующих каталогах, могут быть проверены. Функции os.stat()
и os.lstat()
генерируют общие исключения FileNotFoundError
, когда переданные пути находятсяв несуществующих каталогах, независимо от того, являются ли эти пути недействительными или нет.Существование каталога имеет приоритет над недействительностью пути.
Означает ли это, что пути, находящиеся в несуществующих каталогах, не проверяемы?Да, если мы не изменим эти пути, чтобы они находились в существующих каталогах.Однако возможно ли это безопасно?Разве изменение пути не должно помешать нам проверить исходное имя пути?
Чтобы ответить на этот вопрос, вспомните сверху, что синтаксически правильные имена путей в файловой системе ext4
не содержат компонентов пути (A) содержит нулевые байты или (B) длиной более 255 байтов.Следовательно, путь ext4
действителен тогда и только тогда, когда все компоненты пути в этом пути допустимы.Это верно для большинства реальных файловых систем , представляющих интерес.
Помогает ли нам эта педантичность?Да.Это уменьшает большую проблему проверки полного имени пути одним махом к меньшей проблеме проверки только компонентов пути в этом имени пути.Любой произвольный путь может быть проверен (независимо от того, находится ли этот путь в существующем каталоге или нет) кросс-платформенным способом, следуя следующему алгоритму:
- Разделить это имя на компоненты пути (например,путь
/troldskog/faren/vild
в список ['', 'troldskog', 'faren', 'vild']
). - Для каждого такого компонента:
- Объединить путь каталога, который гарантированно существует с этим компонентом, в новый временный путь (например,
/troldskog
). - Передать этот путь к
os.stat()
или os.lstat()
.Если это имя пути и, следовательно, этот компонент недопустимы, этот вызов гарантированно вызовет исключение, раскрывающее тип недействительности, а не общее исключение FileNotFoundError
.Зачем? Поскольку этот путь находится в существующем каталоге. (Круговая логика круговая.)
Существует ли гарантированный каталог?Да, но обычно только один: самый верхний каталог корневой файловой системы (как определено выше).
Передача имен путей, находящихся в любом другом каталоге (и, следовательно, не гарантированно существовать), в os.stat()
или os.lstat()
приглашаетусловия гонки, даже если этот каталог был ранее проверен на существование.Зачем?Поскольку внешние процессы не могут быть защищены от одновременного удаления этого каталога после , этот тест был выполнен, но до , что путь передается в os.stat()
или os.lstat()
.Дайте волю собакам безумного безумия!
Существует также существенная побочная выгода от вышеупомянутого подхода: безопасность. (Разве это приятно?)В частности:
Фронтальные приложения, проверяющие произвольные имена путей из ненадежных источников путем простой передачи таких имен в os.stat()
или os.lstat()
, подвержены атакам типа «отказ в обслуживании» (DoS) и другим махинациям с черной шляпой.Вредоносные пользователи могут пытаться повторно проверять имена путей, которые находятся в файловых системах, о которых известно, что они устарели или иным образом медленные (например, общие ресурсы NFS Samba);в этом случае слепая проверка входящих имен путей может либо в конечном итоге привести к сбою при превышении времени ожидания соединения, либо потреблять больше времени и ресурсов, чем ваша слабая способность противостоять безработице.
Приведенный выше подход устраняет это только путем проверки путикомпоненты пути к корневому каталогу корневой файловой системы.(Если даже это устаревшее, медленное или недоступное, у вас проблемы больше, чем проверка пути.)
Потерян? Отлично. Давайте начнем.(Предполагается, что Python 3. См. «Что такое хрупкая надежда на 300, leycec ?»)
import errno, os
# Sadly, Python fails to provide the following magic number for us.
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.
See Also
----------
https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx
Official listing of all such codes.
'''
def is_pathname_valid(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS;
`False` otherwise.
'''
# If this pathname is either not a string or is but is empty, this pathname
# is invalid.
try:
if not isinstance(pathname, str) or not pathname:
return False
# Strip this pathname's Windows-specific drive specifier (e.g., `C:\`)
# if any. Since Windows prohibits path components from containing `:`
# characters, failing to strip this `:`-suffixed prefix would
# erroneously invalidate all valid absolute Windows pathnames.
_, pathname = os.path.splitdrive(pathname)
# Directory guaranteed to exist. If the current OS is Windows, this is
# the drive to which Windows was installed (e.g., the "%HOMEDRIVE%"
# environment variable); else, the typical root directory.
root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
if sys.platform == 'win32' else os.path.sep
assert os.path.isdir(root_dirname) # ...Murphy and her ironclad Law
# Append a path separator to this directory if needed.
root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep
# Test whether each path component split from this pathname is valid or
# not, ignoring non-existent and non-readable path components.
for pathname_part in pathname.split(os.path.sep):
try:
os.lstat(root_dirname + pathname_part)
# If an OS-specific exception is raised, its error code
# indicates whether this pathname is valid or not. Unless this
# is the case, this exception implies an ignorable kernel or
# filesystem complaint (e.g., path not found or inaccessible).
#
# Only the following exceptions indicate invalid pathnames:
#
# * Instances of the Windows-specific "WindowsError" class
# defining the "winerror" attribute whose value is
# "ERROR_INVALID_NAME". Under Windows, "winerror" is more
# fine-grained and hence useful than the generic "errno"
# attribute. When a too-long pathname is passed, for example,
# "errno" is "ENOENT" (i.e., no such file or directory) rather
# than "ENAMETOOLONG" (i.e., file name too long).
# * Instances of the cross-platform "OSError" class defining the
# generic "errno" attribute whose value is either:
# * Under most POSIX-compatible OSes, "ENAMETOOLONG".
# * Under some edge-case OSes (e.g., SunOS, *BSD), "ERANGE".
except OSError as exc:
if hasattr(exc, 'winerror'):
if exc.winerror == ERROR_INVALID_NAME:
return False
elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
return False
# If a "TypeError" exception was raised, it almost certainly has the
# error message "embedded NUL character" indicating an invalid pathname.
except TypeError as exc:
return False
# If no exception was raised, all path components and hence this
# pathname itself are valid. (Praise be to the curmudgeonly python.)
else:
return True
# If any other exception was raised, this is an unrelated fatal issue
# (e.g., a bug). Permit this exception to unwind the call stack.
#
# Did we mention this should be shipped with Python already?
Готово. Не щурите этот код.( Он кусается. )
Вопрос № 2: Возможно, существует недопустимое имя пути или Creatability, а?
Проверка наличия или возможности создания возможно недопустимых имен путей, учитывая вышеРешение, в основном, тривиальное.Маленький ключ здесь - вызвать ранее определенную функцию перед проверкой пройденного пути:
def is_path_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create the passed
pathname; `False` otherwise.
'''
# Parent directory of the passed path. If empty, we substitute the current
# working directory (CWD) instead.
dirname = os.path.dirname(pathname) or os.getcwd()
return os.access(dirname, os.W_OK)
def is_path_exists_or_creatable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS _and_
either currently exists or is hypothetically creatable; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
# To prevent "os" module calls from raising undesirable exceptions on
# invalid pathnames, is_pathname_valid() is explicitly called first.
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_creatable(pathname))
# Report failure on non-fatal filesystem complaints (e.g., connection
# timeouts, permissions issues) implying this path to be inaccessible. All
# other exceptions are unrelated fatal issues and should not be caught here.
except OSError:
return False
Выполнено и Выполнено. За исключениемвесьма.
Вопрос № 3: Возможно, существует неверный путь или возможность записи в Windows
Существует предупреждение.Конечно, есть.
Поскольку официальная документация os.access()
допускает:
Примечание: Операции ввода-вывода могут завершаться неудачно дажекогда os.access()
указывает, что они будут успешными, особенно для операций с сетевыми файловыми системами, которые могут иметь семантику разрешений, выходящую за рамки обычной модели битов разрешений POSIX.
Ни для кого не вызывает удивления, что здесь Windows обычно подозревает,Благодаря широкому использованию списков контроля доступа (ACL) в файловых системах NTFS упрощенная модель битов разрешений POSIX плохо соответствует реальности Windows.Хотя это (возможно) не является ошибкой Python, тем не менее, это может быть проблемой для Windows-совместимых приложений.
Если это вы, то нужна более надежная альтернатива.Если переданный путь существует , а не , мы вместо этого пытаемся создать временный файл, который гарантированно будет немедленно удален в родительском каталоге этого пути - более переносимый (если дорогой) тест на создаваемость:
import os, tempfile
def is_path_sibling_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create **siblings**
(i.e., arbitrary files in the parent directory) of the passed pathname;
`False` otherwise.
'''
# Parent directory of the passed path. If empty, we substitute the current
# working directory (CWD) instead.
dirname = os.path.dirname(pathname) or os.getcwd()
try:
# For safety, explicitly close and hence delete this temporary file
# immediately after creating it in the passed path's parent directory.
with tempfile.TemporaryFile(dir=dirname): pass
return True
# While the exact type of exception raised by the above function depends on
# the current version of the Python interpreter, all such types subclass the
# following exception superclass.
except EnvironmentError:
return False
def is_path_exists_or_creatable_portable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname on the current OS _and_
either currently exists or is hypothetically creatable in a cross-platform
manner optimized for POSIX-unfriendly filesystems; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
# To prevent "os" module calls from raising undesirable exceptions on
# invalid pathnames, is_pathname_valid() is explicitly called first.
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_sibling_creatable(pathname))
# Report failure on non-fatal filesystem complaints (e.g., connection
# timeouts, permissions issues) implying this path to be inaccessible. All
# other exceptions are unrelated fatal issues and should not be caught here.
except OSError:
return False
Обратите внимание, однако, что даже этого может быть недостаточно.
Благодаря пользовательскому контролю доступа (UAC), неизменно непревзойденной Windows Vista и всем последующим ее итерациям нагло лгут о разрешениях, относящихся к системным каталогам.Когда пользователи без прав администратора пытаются создать файлы в канонических каталогах C:\Windows
или C:\Windows\system32
, UAC поверхностно разрешает пользователю делать это, в то время как фактически изолирует все созданные файлы в «виртуальном хранилище» в этомпрофиль пользователя.(Кто мог предположить, что обман пользователей будет иметь вредные долгосрочные последствия?)
Это безумие.Это Windows.
Докажите это
Смеем ли мы?Пришло время протестировать приведенные выше тесты.
Поскольку NULL - единственный символ, запрещенный в именах путей в файловых системах, ориентированных на UNIX, давайте воспользуемся этим, чтобы продемонстрировать холодную, жесткую правду - игнорирование невежественных махинаций Windows,откровенно утомлять и злить меня в равной мере:
>>> print('"foo.bar" valid? ' + str(is_pathname_valid('foo.bar')))
"foo.bar" valid? True
>>> print('Null byte valid? ' + str(is_pathname_valid('\x00')))
Null byte valid? False
>>> print('Long path valid? ' + str(is_pathname_valid('a' * 256)))
Long path valid? False
>>> print('"/dev" exists or creatable? ' + str(is_path_exists_or_creatable('/dev')))
"/dev" exists or creatable? True
>>> print('"/dev/foo.bar" exists or creatable? ' + str(is_path_exists_or_creatable('/dev/foo.bar')))
"/dev/foo.bar" exists or creatable? False
>>> print('Null byte exists or creatable? ' + str(is_path_exists_or_creatable('\x00')))
Null byte exists or creatable? False
Вне здравомыслия.Помимо боли.Вы найдете проблемы переносимости Python.