tl; dr
Zadzwoń do is_path_exists_or_creatable()
funkcję zdefiniowaną poniżej.
Ściśle Python 3. Tak właśnie działamy.
Opowieść o dwóch pytaniach
Pytanie „Jak sprawdzić poprawność nazw ścieżek i, w przypadku prawidłowych ścieżek, istnienie lub możliwość zapisu tych ścieżek?” to wyraźnie dwa oddzielne pytania. Obie są interesujące i żadna z nich nie otrzymała tutaj naprawdę satysfakcjonującej odpowiedzi ... lub, cóż, gdziekolwiek , gdzie mogłem grep.
Vikki jest odpowiedź prawdopodobnie hews najbliżej, ale ma niezwykłe wady:
- Niepotrzebnie się otwierają ( ... a potem nie zamykają się niezawodnie ) uchwytów pilników.
- Niepotrzebne zapisywanie ( ... a następnie niepowodzenie w niezawodnym zamykaniu lub usuwaniu ) plików 0-bajtowych.
- Ignorowanie błędów specyficznych dla systemu operacyjnego, rozróżniających między niepoprawnymi nazwami ścieżek, których nie można ignorować, a problemami z systemem plików, które można zignorować. Nic dziwnego, że jest to krytyczne w systemie Windows. ( Zobacz poniżej. )
- Ignorowanie warunków wyścigu wynikających z zewnętrznych procesów jednocześnie (ponownego) przenoszenia katalogów nadrzędnych testowanej nazwy ścieżki. ( Zobacz poniżej. )
- Ignorowanie limitów czasu połączenia wynikających z tej nazwy ścieżki rezydującej na przestarzałych, powolnych lub w inny sposób tymczasowo niedostępnych systemach plików. Może to narazić publiczne usługi na potencjalne ataki spowodowane atakiem DoS . ( Zobacz poniżej. )
Naprawimy to wszystko.
Pytanie # 0: Jaka jest ponownie ważność ścieżki?
Zanim wrzucimy nasze kruche kombinezony mięsne w podziurawione pytonami moshpity bólu, powinniśmy prawdopodobnie zdefiniować, co rozumiemy przez „ważność ścieżki”. Co dokładnie definiuje ważność?
Przez „ważność ścieżki” rozumiemy poprawność składniową ścieżki w odniesieniu do głównego systemu plików bieżącego systemu - niezależnie od tego, czy ta ścieżka lub jej katalogi nadrzędne istnieją fizycznie. W ramach tej definicji nazwa ścieżki jest poprawna składniowo, jeśli spełnia wszystkie wymagania składniowe głównego systemu plików.
Przez „główny system plików” rozumiemy:
- W systemach zgodnych z POSIX system plików podłączony do katalogu głównego (
/
).
- W systemie Windows, system plików zamontowany
%HOMEDRIVE%
, litera dysku okrężnicy sufiksem zawierającym bieżącej instalacji systemu Windows (zazwyczaj, ale nie koniecznie C:
).
Z kolei znaczenie „poprawności składniowej” zależy od typu głównego systemu plików. Dla ext4
(i większości, ale nie wszystkich zgodnych z POSIX) systemów plików, nazwa ścieżki jest poprawna składniowo wtedy i tylko wtedy, gdy ta nazwa ścieżki:
- Nie zawiera bajtów zerowych (tj.
\x00
W Pythonie). Jest to trudne wymaganie dla wszystkich systemów plików zgodnych z POSIX.
- Nie zawiera składników ścieżki dłuższych niż 255 bajtów (np.
'a'*256
W Pythonie). Składnik ścieżka jest najdłuższy podciąg z ścieżki nie zawierającej /
znak (na przykład bergtatt
, ind
, i
oraz fjeldkamrene
w ścieżkę /bergtatt/ind/i/fjeldkamrene
).
Poprawność składniowa. Główny system plików. Otóż to.
Pytanie 1: Jak teraz powinniśmy sprawdzić ważność nazwy ścieżki?
Weryfikacja ścieżek w Pythonie jest zaskakująco nieintuicyjna. W tym przypadku zgadzam się z Fake Name : oficjalny os.path
pakiet powinien zapewniać gotowe rozwiązanie tego problemu. Z nieznanych (i prawdopodobnie niekwestionowanych) powodów tak nie jest. Na szczęście rozwijanie własnego rozwiązania ad hoc nie jest aż tak bolesne ...
OK, faktycznie jest. Jest włochaty; to jest paskudne; prawdopodobnie chichocze, burczy i chichocze, gdy się świeci. Ale co zrobisz? Nic.
Wkrótce zejdziemy w radioaktywną otchłań niskopoziomowego kodu. Ale najpierw porozmawiajmy o sklepie na wysokim poziomie. Standard os.stat()
i os.lstat()
funkcje zgłaszają następujące wyjątki po przekazaniu nieprawidłowych nazw ścieżek:
- W przypadku nazw ścieżek znajdujących się w nieistniejących katalogach wystąpienia
FileNotFoundError
.
- W przypadku ścieżek znajdujących się w istniejących katalogach:
- W systemie Windows instancje,
WindowsError
których winerror
atrybut to 123
(tj ERROR_INVALID_NAME
.).
- We wszystkich innych systemach operacyjnych:
- W przypadku ścieżek zawierających bajty o wartości null (tj.
'\x00'
), Wystąpienia TypeError
.
- W przypadku ścieżek zawierających składniki ścieżek dłuższe niż 255 bajtów, wystąpienia
OSError
których errcode
atrybutu to:
- Pod SunOS i * BSD, rodzina systemów operacyjnych
errno.ERANGE
. (Wydaje się, że jest to błąd na poziomie systemu operacyjnego, inaczej określany jako „selektywna interpretacja” standardu POSIX).
- We wszystkich innych systemach operacyjnych
errno.ENAMETOOLONG
.
Co najważniejsze, oznacza to, że tylko ścieżki znajdujące się w istniejących katalogach są weryfikowalne. Funkcje os.stat()
i os.lstat()
zgłaszają ogólne FileNotFoundError
wyjątki, gdy przekazywane są nazwy ścieżek rezydujące w nieistniejących katalogach, niezależnie od tego, czy te nazwy ścieżek są nieprawidłowe, czy nie. Istnienie katalogu ma pierwszeństwo przed nieważnością nazwy ścieżki.
Czy to oznacza, że nazwy ścieżek znajdujące się w nieistniejących katalogach nie podlegają walidacji? Tak - chyba że zmodyfikujemy te ścieżki, aby znajdowały się w istniejących katalogach. Czy jest to jednak w ogóle możliwe do wykonania? Czy modyfikacja nazwy ścieżki nie powinna uniemożliwić nam sprawdzenia oryginalnej ścieżki?
Aby odpowiedzieć na to pytanie, przypomnij sobie z góry, że poprawne składniowo nazwy ścieżek w ext4
systemie plików nie zawierają składników ścieżki (A) zawierających bajty zerowe lub (B) o długości przekraczającej 255 bajtów. Dlatego ext4
nazwa ścieżki jest poprawna wtedy i tylko wtedy, gdy wszystkie składniki ścieżki w tej nazwie ścieżki są prawidłowe. Dotyczy to większości interesujących nas systemów plików w świecie rzeczywistym .
Czy ten pedantyczny wgląd rzeczywiście nam pomaga? Tak. Zmniejsza to większy problem związany z walidacją pełnej nazwy ścieżki za jednym zamachem do mniejszego problemu sprawdzania poprawności tylko wszystkich składników ścieżki w tej nazwie ścieżki. Dowolną dowolną nazwę ścieżki można zweryfikować (niezależnie od tego, czy ta ścieżka znajduje się w istniejącym katalogu, czy nie) w sposób wieloplatformowy, stosując następujący algorytm:
- Podziel tę nazwę ścieżki na składniki ścieżki (np. Ścieżkę
/troldskog/faren/vild
do listy ['', 'troldskog', 'faren', 'vild']
).
- Dla każdego takiego składnika:
- Połącz ścieżkę katalogu, który może istnieć z tym komponentem, do nowej tymczasowej nazwy ścieżki (np
/troldskog
.).
- Przekaż tę ścieżkę do
os.stat()
lub os.lstat()
. Jeśli ta nazwa ścieżki, a tym samym ten składnik, jest nieprawidłowa, to wywołanie gwarantuje zgłoszenie wyjątku ujawniającego typ nieważności, a nie FileNotFoundError
wyjątek ogólny . Czemu? Ponieważ ta nazwa ścieżki znajduje się w istniejącym katalogu. (Logika kołowa jest kołowa.)
Czy istnieje katalog na pewno istnieje? Tak, ale zazwyczaj tylko jeden: najwyższy katalog głównego systemu plików (zgodnie z powyższą definicją).
Przekazywanie nazw ścieżek znajdujących się w jakimkolwiek innym katalogu (a zatem nie gwarantuje się, że istnieją) do warunków wyścigu os.stat()
lub powoduje os.lstat()
zaproszenie do warunków wyścigu, nawet jeśli wcześniej sprawdzono, czy katalog ten istnieje. Czemu? Ponieważ nie można zapobiec równoczesnemu usuwaniu tego katalogu przez procesy zewnętrzne po wykonaniu tego testu, ale przed przekazaniem tej os.stat()
nazwy ścieżki do lub os.lstat()
. Uwolnij psy oszałamiającego szaleństwa!
Powyższe podejście ma również istotną zaletę: bezpieczeństwo. (Nie jest to miłe?) W szczególności:
Aplikacje frontowe weryfikujące dowolne ścieżki z niezaufanych źródeł, po prostu przekazując takie ścieżki do ataków typu Denial of Service (DoS) i innych oszustw typu „black hat” os.stat()
lub os.lstat()
są na nie podatne. Złośliwi użytkownicy mogą próbować wielokrotnie sprawdzać poprawność nazw ścieżek znajdujących się na systemach plików, o których wiadomo, że są przestarzałe lub w inny sposób powolne (np. Udziały NFS Samby); w takim przypadku ślepe rejestrowanie przychodzących nazw ścieżek może ostatecznie zakończyć się niepowodzeniem z powodu przekroczenia limitów czasu połączenia lub pochłonąć więcej czasu i zasobów niż Twoja słaba zdolność wytrzymania bezrobocia.
Powyższe podejście zapobiega temu, sprawdzając tylko składniki ścieżki ścieżki względem katalogu głównego głównego systemu plików. (Jeśli nawet to jest nieaktualne, powolne lub niedostępne, masz większe problemy niż weryfikacja ścieżki).
Stracony? Świetny. Zaczynajmy. (Założono Python 3. Zobacz „What Is Fragile Hope for 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
Gotowe. Nie mruż oczy na ten kod. ( Gryzie. )
Pytanie 2: Możliwe, że istnieje nieprawidłowa nazwa ścieżki lub możliwość jej wykonania, co?
Testowanie istnienia lub możliwości tworzenia potencjalnie nieprawidłowych nazw ścieżek jest, biorąc pod uwagę powyższe rozwiązanie, w większości trywialne. Mały klucz polega na wywołaniu wcześniej zdefiniowanej funkcji przed przetestowaniem przekazanej ścieżki:
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
Gotowe i gotowe. Z wyjątkiem niezupełnie.
Pytanie # 3: Prawdopodobnie nieprawidłowe istnienie ścieżki lub możliwość zapisu w systemie Windows
Istnieje zastrzeżenie. Oczywiście, że tak.
Jak przyznaje oficjalna os.access()
dokumentacja :
Uwaga: Operacje we / wy mogą się nie powieść nawet wtedy os.access()
, gdy wskazują, że zakończyłyby się powodzeniem, szczególnie w przypadku operacji na sieciowych systemach plików, które mogą mieć semantykę uprawnień wykraczającą poza typowy model bitowy uprawnień POSIX.
Nic dziwnego, że podejrzanym jest tutaj zwykle Windows. Dzięki szerokiemu wykorzystaniu list kontroli dostępu (ACL) w systemach plików NTFS, uproszczony model bitowy uprawnień POSIX słabo odwzorowuje rzeczywistość systemu Windows. Chociaż nie jest to (prawdopodobnie) wina Pythona, może to jednak dotyczyć aplikacji zgodnych z systemem Windows.
Jeśli to ty, potrzebna jest solidniejsza alternatywa. Jeśli przekazana ścieżka nie istnieje, zamiast tego spróbujemy utworzyć plik tymczasowy z gwarancją natychmiastowego usunięcia w katalogu nadrzędnym tej ścieżki - bardziej przenośny (jeśli kosztowny) test kreatywności:
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
Należy jednak pamiętać, że nawet to może nie wystarczyć.
Dzięki kontroli dostępu użytkownika (UAC), zawsze niemożliwy do wyobrażenia system Windows Vista i wszystkie jego kolejne iteracje rażąco kłamie na temat uprawnień odnoszących się do katalogów systemowych. Kiedy użytkownicy niebędący administratorami próbują tworzyć pliki w katalogu kanonicznym C:\Windows
lub C:\Windows\system32
katalogach, UAC powierzchownie zezwala użytkownikowi na to, podczas gdy w rzeczywistości izoluje wszystkie utworzone pliki do „magazynu wirtualnego” w profilu tego użytkownika. (Kto mógł sobie wyobrazić, że oszukiwanie użytkowników będzie miało szkodliwe długoterminowe konsekwencje?)
To jest szalone. To jest Windows.
Udowodnij to
Czy mamy odwagę? Czas przetestować powyższe testy.
Ponieważ NULL jest jedyną postacią zabronioną w nazwach ścieżek w systemach plików zorientowanych na UNIX, wykorzystajmy to, aby zademonstrować zimną, twardą prawdę - ignorując nie dające się zignorować shenanigans Windows, które szczerze mnie nudzą i złości w równym stopniu:
>>> 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
Poza zdrowiem psychicznym. Poza bólem. Znajdziesz obawy dotyczące przenośności języka Python.