tl; dr
Вызовите is_path_exists_or_creatable()
функцию, определенную ниже.
Строго Python 3. Вот так мы и катимся.
Повесть о двух вопросах
Вопрос: «Как мне проверить правильность имени пути и, для действительных имен, наличие или возможность записи этих путей?» это явно два отдельных вопроса. Оба они интересны, и ни один из них не получил действительно удовлетворительного ответа здесь ... или, ну, в любом месте , где я мог бы grep.
Викки «s ответ , вероятно , рубит ближе, но имеет замечательные недостатки:
- Излишне открывать ( ... а затем не удается надежно закрыть ) дескрипторы файлов.
- Излишняя запись ( ... а затем невозможность надежного закрытия или удаления ) 0-байтовых файлов.
- Игнорирование ошибок, связанных с ОС, различие между неотвратимыми недопустимыми именами пути и игнорируемыми проблемами файловой системы. Неудивительно, что под Windows это критично. ( См. Ниже. )
- Игнорирование условий гонки, возникающих из-за того, что внешние процессы одновременно (повторно) перемещают родительские каталоги проверяемого пути. ( См. Ниже. )
- Игнорирование тайм-аутов соединения, возникающих из-за того, что этот путь находится в устаревших, медленных или иным образом временно недоступных файловых системах. Это может подвергнуть общедоступные сервисы потенциальным DoS- атакам. ( См. Ниже. )
Мы все это исправим.
Вопрос № 0: что еще раз озвучивает действительность имени пути?
Прежде чем швырять наши хрупкие мясные скафандры в кишащие питонами мошпиты боли, нам, вероятно, следует определить, что мы подразумеваем под «достоверностью имени пути». Что конкретно определяет действительность?
Под «достоверностью имени пути» мы подразумеваем синтаксическую правильность имени пути по отношению к корневой файловой системе текущей системы - независимо от того, существует ли физически этот путь или его родительские каталоги. Имя пути является синтаксически правильным в соответствии с этим определением, если оно соответствует всем синтаксическим требованиям корневой файловой системы.
Под "корневой файловой системой" мы подразумеваем:
- В POSIX-совместимых системах файловая система смонтирована в корневой каталог (
/
).
- В Windows файловая система подключена к
%HOMEDRIVE%
букве диска с суффиксом двоеточия, содержащей текущую установку Windows (обычно, но не обязательно 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()
. Развяжите псов головокружительного безумия!
У вышеупомянутого подхода также есть существенное побочное преимущество: безопасность. (Не что приятно?) В частности:
Внешние приложения, проверяющие произвольные имена путей из ненадежных источников, просто передавая такие имена пути к атакам типа «отказ в обслуживании» (DoS) и другим махинациям «черной шляпы» os.stat()
или os.lstat()
подвержены им. Злоумышленники могут неоднократно пытаться проверять имена путей в файловых системах, которые заведомо устарели или работают медленно (например, общие ресурсы NFS Samba); в этом случае слепая статистика входящих путей может либо в конечном итоге выйти из строя с тайм-аутом соединения, либо потребовать больше времени и ресурсов, чем ваша слабая способность противостоять безработице.
Вышеупомянутый подход устраняет это, только проверяя компоненты пути в имени пути относительно корневого каталога корневой файловой системы. (Если даже это устаревшее, медленное или недоступное, у вас есть более серьезные проблемы, чем проверка имени пути.)
Потерянный? Отлично. Давайте начнем. (Предполагается Python 3. См. «Что такое хрупкая надежда для 300, leycec ?»)
import errno, os
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.
See Also
----------
https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
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.
'''
try:
if not isinstance(pathname, str) or not pathname:
return False
_, pathname = os.path.splitdrive(pathname)
root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
if sys.platform == 'win32' else os.path.sep
assert os.path.isdir(root_dirname)
root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep
for pathname_part in pathname.split(os.path.sep):
try:
os.lstat(root_dirname + pathname_part)
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
except TypeError as exc:
return False
else:
return True
Выполнено. Не прищуривайся на этот код. ( Кусает. )
Вопрос № 2: Возможно, неверное существование или возможность существования пути, а?
Проверка существования или возможности создания возможно недопустимых имен путей, учитывая вышеупомянутое решение, в основном тривиально. Маленький ключ здесь - вызвать ранее определенную функцию перед проверкой пройденного пути:
def is_path_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create the passed
pathname; `False` otherwise.
'''
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:
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_creatable(pathname))
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.
'''
dirname = os.path.dirname(pathname) or os.getcwd()
try:
with tempfile.TemporaryFile(dir=dirname): pass
return True
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:
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_sibling_creatable(pathname))
except OSError:
return False
Учтите, однако, что даже этого может быть недостаточно.
Благодаря контролю доступа пользователей (UAC) неподражаемая Windows Vista и все ее последующие версии откровенно лгут о разрешениях, относящихся к системным каталогам. Когда пользователи не администраторы пытаются создать файлы в любом каноническом C:\Windows
или C:\Windows\system32
каталогах, UAC внешне позволяет пользователю сделать это в то время как на самом деле изолировать все созданные файлы в «виртуальный магазин» в профиле этого пользователя. (Кто бы мог подумать, что обман пользователей приведет к пагубным долгосрочным последствиям?)
Это безумие. Это винда.
Докажите это
Смеем ли мы? Пришло время протестировать вышеперечисленные тесты.
Поскольку 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.