tl; д-р
Викличте is_path_exists_or_creatable()
функцію, визначену нижче.
Строго Python 3. Ось так ми і рухаємося.
Казка про два запитання
Питання "Як перевірити правильність імені шляху та, для дійсних імен шляхів, існування чи запис до цих шляхів?" це явно два окремі питання. Обидва вони цікаві, і жоден з них не отримав справді задовільної відповіді тут ... або, ну, де-небудь, що я міг зрозуміти.
Віккі «s відповідь , ймовірно , рубає ближче, але має чудові недоліки:
- Без потреби відкривати ( ... а потім не вдається надійно закрити ) дескриптори файлів.
- Зайве написання ( ... а потім неможливість надійного закриття чи видалення ) 0-байтових файлів.
- Ігнорування помилок, специфічних для ОС, розмежування неігноруваних невірних імен шляхів та проблем файлової системи. Не дивно, що це критично важливо для Windows. ( Див. Нижче. )
- Ігнорування умов перегонів, що виникають внаслідок зовнішніх процесів одночасно (пере) переміщення батьківських каталогів імені шляху, що перевіряється. ( Див. Нижче. )
- Ігноруючи час очікування підключення, що виникає внаслідок цього імені шляху, яке знаходиться у застарілих, повільних або тимчасово недоступних файлових системах. Це може піддати публічні служби потенційним атакам DoS- Driven. ( Див. Нижче. )
Ми все це виправимо.
Запитання № 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
пакет повинен надати нестандартне рішення для цього. З невідомих (і, мабуть, незрозумілих) причин, це не так. На щастя, розгортання власного спеціального рішення - це не те, що викручувати кишечник ...
Добре, насправді так. Волохата; це неприємно; ймовірно, він бурчить, коли бурчить і хихикає, коли світиться. Але що ти будеш робити? Нутін.
Незабаром ми спустимося в радіоактивну прірву низькорівневого коду. Але спочатку поговоримо про магазин високого рівня. Стандарт 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()
and 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
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 поверхнево дозволяє користувачеві робити це, фактично ізолюючи всі створені файли у "Віртуальному магазині" у профілі цього користувача. (Хто міг уявити, що обман користувачів матиме шкідливі довгострокові наслідки?)
Це божевілля. Це 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.