tl; dr
Rufen Sie die is_path_exists_or_creatable()
unten definierte Funktion auf.
Streng Python 3. Genau so rollen wir.
Eine Geschichte von zwei Fragen
Die Frage "Wie teste ich die Gültigkeit von Pfadnamen und bei gültigen Pfadnamen die Existenz oder Schreibbarkeit dieser Pfade?" ist eindeutig zwei getrennte Fragen. Beide sind interessant und haben hier keine wirklich zufriedenstellende Antwort erhalten ... oder irgendwo , wo ich sie finden könnte.
vikki ‚s Antwort haue wahrscheinlich in der Nähe, hat aber die bemerkenswerten Nachteile:
- Unnötiges Öffnen ( ... und nicht zuverlässiges Schließen ) von Dateihandles.
- Unnötiges Schreiben ( ... und dann nicht zuverlässiges Schließen oder Löschen ) von 0-Byte-Dateien.
- Ignorieren von betriebssystemspezifischen Fehlern, die zwischen nicht ignorierbaren ungültigen Pfadnamen und ignorierbaren Dateisystemproblemen unterscheiden. Es überrascht nicht, dass dies unter Windows von entscheidender Bedeutung ist. ( Siehe unten. )
- Ignorieren von Race-Bedingungen, die sich aus externen Prozessen ergeben, die gleichzeitig übergeordnete Verzeichnisse des zu testenden Pfadnamens (neu) verschieben. ( Siehe unten. )
- Ignorieren von Verbindungszeitlimits, die sich aus diesem Pfadnamen ergeben, der sich auf veralteten, langsamen oder anderweitig vorübergehend unzugänglichen Dateisystemen befindet. Dies könnte öffentlich zugängliche Dienste potenziellen DoS- gesteuerten Angriffen aussetzen . ( Siehe unten. )
Wir werden das alles reparieren.
Frage 0: Was ist wieder die Gültigkeit von Pfadnamen?
Bevor wir unsere zerbrechlichen Fleischanzüge in die von Python durchsetzten Moshpits des Schmerzes schleudern, sollten wir wahrscheinlich definieren, was wir unter "Gültigkeit von Pfadnamen" verstehen. Was genau definiert Gültigkeit?
Mit "Gültigkeit von Pfadnamen" ist die syntaktische Korrektheit eines Pfadnamens in Bezug auf das Root-Dateisystem des aktuellen Systems gemeint - unabhängig davon, ob dieser Pfad oder dessen übergeordnete Verzeichnisse physisch vorhanden sind. Ein Pfadname ist unter dieser Definition syntaktisch korrekt, wenn er alle syntaktischen Anforderungen des Root-Dateisystems erfüllt.
Mit "Root-Dateisystem" meinen wir:
- Auf POSIX-kompatiblen Systemen wird das Dateisystem im Stammverzeichnis (
/
) bereitgestellt .
- Unter Windows ist das Dateisystem mit
%HOMEDRIVE%
dem Laufwerksbuchstaben mit Doppelpunkt versehen, der die aktuelle Windows-Installation enthält (normalerweise, aber nicht unbedingt C:
).
Die Bedeutung von "syntaktischer Korrektheit" hängt wiederum von der Art des Root-Dateisystems ab. Für ext4
(und die meisten, aber nicht alle POSIX-kompatiblen) Dateisysteme ist ein Pfadname genau dann syntaktisch korrekt, wenn dieser Pfadname:
- Enthält keine Null-Bytes (dh
\x00
in Python). Dies ist eine wichtige Voraussetzung für alle POSIX-kompatiblen Dateisysteme.
- Enthält keine Pfadkomponenten, die länger als 255 Byte sind (z. B.
'a'*256
in Python). Eine Pfadkomponente ist eine längste Teilkette eines Pfadnamen NO /
Charakter (zB bergtatt
, ind
, i
, und fjeldkamrene
in dem Pfadnamen /bergtatt/ind/i/fjeldkamrene
).
Syntaktische Korrektheit. Root-Dateisystem. Das ist es.
Frage 1: Wie sollen wir jetzt die Gültigkeit von Pfadnamen tun?
Das Überprüfen von Pfadnamen in Python ist überraschenderweise nicht intuitiv. Ich bin hier fest mit Fake Name einverstanden : Das offizielle os.path
Paket sollte eine sofort einsatzbereite Lösung dafür bieten. Aus unbekannten (und wahrscheinlich nicht zwingenden) Gründen ist dies nicht der Fall. Glücklicherweise ist Ihre eigene Ad-hoc - Lösung Abrollen nicht , dass stechende ...
OK, das ist es tatsächlich. Es ist haarig; es ist fies; es gluckst wahrscheinlich, wenn es knurrt und kichert, wenn es glüht. Aber was wirst du tun? Nuthin '.
Wir werden bald in den radioaktiven Abgrund des Low-Level-Codes abtauchen. Aber zuerst sprechen wir über High-Level-Shop. Der Standard os.stat()
und die os.lstat()
Funktionen lösen die folgenden Ausnahmen aus, wenn ungültige Pfadnamen übergeben werden:
- Für Pfadnamen, die sich in nicht vorhandenen Verzeichnissen befinden, Instanzen von
FileNotFoundError
.
- Für Pfadnamen, die sich in vorhandenen Verzeichnissen befinden:
- Unter Windows Instanzen,
WindowsError
deren winerror
Attribut 123
(dh ERROR_INVALID_NAME
) ist.
- Unter allen anderen Betriebssystemen:
- Für Pfadnamen, die Null-Bytes (dh
'\x00'
) enthalten, Instanzen von TypeError
.
- Für Pfadnamen, die Pfadkomponenten enthalten, die länger als 255 Byte sind,
OSError
deren Instanzen errcode
Folgendes sind:
- Unter SunOS und der * BSD-Betriebssystemfamilie
errno.ERANGE
. (Dies scheint ein Fehler auf Betriebssystemebene zu sein, der auch als "selektive Interpretation" des POSIX-Standards bezeichnet wird.)
- Unter allen anderen Betriebssystemen
errno.ENAMETOOLONG
.
Entscheidend ist, dass nur Pfadnamen in vorhandenen Verzeichnissen validiert werden können. Die Funktionen os.stat()
und os.lstat()
lösen generische FileNotFoundError
Ausnahmen aus, wenn übergebene Pfadnamen in nicht vorhandenen Verzeichnissen übergeben werden, unabhängig davon, ob diese Pfadnamen ungültig sind oder nicht. Die Existenz eines Verzeichnisses hat Vorrang vor der Ungültigkeit des Pfadnamens.
Bedeutet dies, dass Pfadnamen, die sich in nicht vorhandenen Verzeichnissen befinden, nicht validierbar sind? Ja - es sei denn, wir ändern diese Pfadnamen so, dass sie sich in vorhandenen Verzeichnissen befinden. Ist das aber überhaupt sicher machbar? Sollte das Ändern eines Pfadnamens nicht verhindern, dass wir den ursprünglichen Pfadnamen überprüfen?
Um diese Frage zu beantworten, erinnern Sie sich von oben daran, dass syntaktisch korrekte Pfadnamen im ext4
Dateisystem keine Pfadkomponenten (A) enthalten, die Null-Bytes oder (B) mehr als 255 Bytes enthalten. Daher ist ein ext4
Pfadname nur dann gültig, wenn alle Pfadkomponenten in diesem Pfadnamen gültig sind. Dies gilt für die meisten interessierenden realen Dateisysteme .
Hilft uns diese pedantische Einsicht tatsächlich? Ja. Es reduziert das größere Problem der Validierung des vollständigen Pfadnamens auf einen Schlag auf das kleinere Problem der Validierung aller Pfadkomponenten in diesem Pfadnamen. Jeder beliebige Pfadname kann plattformübergreifend überprüft werden (unabhängig davon, ob sich dieser Pfadname in einem vorhandenen Verzeichnis befindet oder nicht), indem der folgende Algorithmus befolgt wird:
- Teilen Sie diesen Pfadnamen in Pfadkomponenten auf (z. B. den Pfadnamen
/troldskog/faren/vild
in die Liste ['', 'troldskog', 'faren', 'vild']
).
- Für jede solche Komponente:
- Verbinden Sie den Pfadnamen eines Verzeichnisses, das garantiert mit dieser Komponente vorhanden ist, zu einem neuen temporären Pfadnamen (z
/troldskog
. B. ).
- Geben Sie diesen Pfadnamen an
os.stat()
oder weiter os.lstat()
. Wenn dieser Pfadname und damit diese Komponente ungültig ist, löst dieser Aufruf garantiert eine Ausnahme aus, die den Typ der Ungültigkeit und nicht eine generische FileNotFoundError
Ausnahme aufdeckt . Warum? Weil sich dieser Pfadname in einem vorhandenen Verzeichnis befindet. (Zirkuläre Logik ist zirkulär.)
Gibt es ein garantiertes Verzeichnis? Ja, aber normalerweise nur eines: das oberste Verzeichnis des Root-Dateisystems (wie oben definiert).
Das Übergeben von Pfadnamen, die sich in einem anderen Verzeichnis befinden (und daher nicht garantiert existieren), an die Rennbedingungen os.stat()
oder os.lstat()
lädt zu Rennbedingungen ein, selbst wenn dieses Verzeichnis zuvor auf Existenz getestet wurde. Warum? Weil externe Prozesse nicht daran gehindert werden können, dieses Verzeichnis gleichzeitig zu entfernen, nachdem dieser Test durchgeführt wurde, aber bevor dieser Pfadname an os.stat()
oder übergeben wird os.lstat()
. Entfessle die Hunde des wahnsinnigen Wahnsinns!
Der obige Ansatz hat auch einen erheblichen Nebeneffekt: Sicherheit. (Ist das nicht das schön?) Im Einzelnen:
Front-Faced-Anwendungen, die beliebige Pfadnamen aus nicht vertrauenswürdigen Quellen validieren, indem sie diese Pfadnamen einfach an DoS-Angriffe (Denial of Service) und andere Black-Hat-Spielereien weitergeben os.stat()
oder os.lstat()
für diese anfällig sind. Böswillige Benutzer versuchen möglicherweise wiederholt, Pfadnamen zu überprüfen, die sich auf Dateisystemen befinden, von denen bekannt ist, dass sie veraltet oder auf andere Weise langsam sind (z. B. NFS-Samba-Freigaben). In diesem Fall kann es vorkommen, dass blind eingehende Pfadnamen entweder mit Verbindungszeitüberschreitungen fehlschlagen oder mehr Zeit und Ressourcen verbrauchen als Ihre schwache Fähigkeit, der Arbeitslosigkeit standzuhalten.
Der obige Ansatz vermeidet dies, indem nur die Pfadkomponenten eines Pfadnamens anhand des Stammverzeichnisses des Stammdateisystems überprüft werden. (Wenn selbst das veraltet, langsam oder unzugänglich ist, haben Sie größere Probleme als die Überprüfung des Pfadnamens.)
Hat verloren? Toll. Lass uns anfangen. (Python 3 angenommen. Siehe "Was ist fragile Hoffnung für 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://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.
'''
# 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?
Getan. Schielen Sie nicht auf diesen Code. ( Es beißt. )
Frage 2: Möglicherweise ungültige Existenz oder Erstellbarkeit von Pfadnamen, was?
Das Testen der Existenz oder Erstellbarkeit möglicherweise ungültiger Pfadnamen ist angesichts der obigen Lösung meist trivial. Der kleine Schlüssel hier ist, die zuvor definierte Funktion aufzurufen, bevor der übergebene Pfad getestet wird:
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
Fertig und fertig. Außer nicht ganz.
Frage 3: Möglicherweise ungültige Pfadnamen oder Beschreibbarkeit unter Windows
Es gibt eine Einschränkung. Natürlich gibt es das.
Wie die offizielle os.access()
Dokumentation zulässt:
Hinweis: E / A-Vorgänge können fehlschlagen, auch wenn dies os.access()
darauf hinweist, dass sie erfolgreich sind, insbesondere bei Vorgängen auf Netzwerkdateisystemen, deren Berechtigungssemantik möglicherweise über das übliche POSIX-Berechtigungsbitmodell hinausgeht.
Zu keiner Überraschung ist Windows hier der übliche Verdächtige. Dank der umfassenden Verwendung von Zugriffssteuerungslisten (Access Control Lists, ACL) in NTFS-Dateisystemen lässt sich das vereinfachte POSIX-Berechtigungsbitmodell nur schlecht auf die zugrunde liegende Windows-Realität abbilden. Obwohl dies (wohl) nicht Pythons Schuld ist, könnte es dennoch für Windows-kompatible Anwendungen von Belang sein.
Wenn Sie es sind, wird eine robustere Alternative gesucht. Wenn der übergebene Pfad nicht vorhanden ist, versuchen wir stattdessen, eine temporäre Datei zu erstellen, die garantiert sofort im übergeordneten Verzeichnis dieses Pfads gelöscht wird - ein portablerer (wenn auch teurer) Test der Erstellbarkeit:
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
Beachten Sie jedoch, dass auch dies möglicherweise nicht ausreicht.
Dank User Access Control (UAC), die ständig inimicable Windows Vista und alle nachfolgenden Iterationen davon liegt offensichtlich über Berechtigungen für Systemverzeichnisse betreffen. Wenn Nicht-Administrator - Benutzer in Dateien entweder den kanonischen zu erstellen versuchen C:\Windows
oder C:\Windows\system32
Verzeichnissen, erlaubt UAC oberflächlich den Benutzer so zu tun , während tatsächlich alle erstellten Dateien in ein „Virtual Store“ Isolierung in dem Profil des Benutzers. (Wer hätte sich vorstellen können, dass die Täuschung von Benutzern langfristig schädliche Folgen haben würde?)
Das ist verrückt. Das ist Windows.
Beweise es
Wagen wir es? Es ist Zeit, die oben genannten Tests zu testen.
Da NULL das einzige Zeichen ist, das in Pfadnamen auf UNIX-orientierten Dateisystemen verboten ist, nutzen wir dies, um die kalte, harte Wahrheit zu demonstrieren - und ignorieren Sie nicht ignorierbare Windows-Spielereien, die mich offen gesagt gleichermaßen langweilen und verärgern:
>>> 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
Jenseits der Vernunft. Jenseits des Schmerzes. Sie werden Bedenken hinsichtlich der Python-Portabilität finden.