tl; dr
Appelez la is_path_exists_or_creatable()
fonction définie ci-dessous.
Strictement Python 3. C'est comme ça que nous roulons.
Un conte de deux questions
La question "Comment tester la validité des chemins et, pour les chemins valides, l'existence ou l'écriture de ces chemins?" est clairement deux questions distinctes. Les deux sont intéressants, et ni l'un ni l'autre n'ont reçu de réponse vraiment satisfaisante ici ... ou, bien, partout où je pourrais grep.
Vikki de réponse HEWS probablement le plus proche, mais présente les inconvénients remarquables:
- Ouverture inutile ( ... et échec de la fermeture fiable ) des descripteurs de fichiers.
- Écriture inutile ( ... et échec de la fermeture ou de la suppression fiable ) de fichiers de 0 octet.
- Ignorer les erreurs spécifiques au système d'exploitation faisant la distinction entre les chemins d'accès non valides non ignorables et les problèmes de système de fichiers ignorables Sans surprise, c'est essentiel sous Windows. ( Voir ci-dessous. )
- Ignorer les conditions de concurrence résultant de processus externes (re) déplacer simultanément les répertoires parents du chemin à tester. ( Voir ci-dessous. )
- Ignorer les délais de connexion résultant de ce chemin résidant sur des systèmes de fichiers obsolètes, lents ou temporairement inaccessibles. Cela pourrait exposer les services publics à des attaques potentielles liées au DoS . ( Voir ci-dessous. )
On va réparer tout ça.
Question n ° 0: Quelle est encore la validité du chemin d'accès?
Avant de lancer nos fragiles combinaisons de viande dans les moshpits de douleur criblés de python, nous devrions probablement définir ce que nous entendons par «validité de chemin». Qu'est-ce qui définit la validité, exactement?
Par «validité de chemin», nous entendons l' exactitude syntaxique d'un chemin par rapport au système de fichiers racine du système actuel - indépendamment du fait que ce chemin ou ses répertoires parents existent physiquement. Un chemin d'accès est syntaxiquement correct selon cette définition s'il est conforme à toutes les exigences syntaxiques du système de fichiers racine.
Par «système de fichiers racine», nous entendons:
- Sur les systèmes compatibles POSIX, le système de fichiers est monté dans le répertoire racine (
/
).
- Sous Windows, le système de fichiers monté sur
%HOMEDRIVE%
, la lettre de lecteur avec le suffixe deux-points contenant l'installation actuelle de Windows (généralement mais pas nécessairement C:
).
La signification de «correction syntaxique» dépend à son tour du type de système de fichiers racine. Pour ext4
(et la plupart des systèmes de fichiers compatibles POSIX mais pas tous), un chemin d'accès est syntaxiquement correct si et seulement si ce chemin:
- Ne contient aucun octet nul (c'est-
\x00
à- dire en Python). C'est une exigence absolue pour tous les systèmes de fichiers compatibles POSIX.
- Ne contient aucun composant de chemin de plus de 255 octets (par exemple,
'a'*256
en Python). Un composant de chemin est une plus longue sous - chaîne d'un chemin ne contenant pas de /
caractère (par exemple bergtatt
, ind
, i
, et fjeldkamrene
dans le chemin d' accès /bergtatt/ind/i/fjeldkamrene
).
Exactitude syntaxique. Système de fichiers racine. C'est ça.
Question n ° 1: Comment allons-nous maintenant faire la validité des chemins d'accès?
La validation des chemins d'accès en Python n'est étonnamment pas intuitive. Je suis totalement d'accord avec Fake Name ici: le os.path
package officiel devrait fournir une solution prête à l'emploi pour cela. Pour des raisons inconnues (et probablement non convaincantes), ce n'est pas le cas. Heureusement, le déroulement de votre propre solution ad hoc n'est pas si déchirant ...
OK, c'est en fait. C'est velu; c'est dégueulasse; il glousse probablement en ronflant et glousse quand il brille. Mais qu'est-ce que tu vas faire? Nuthin '.
Nous allons bientôt descendre dans l'abîme radioactif du code de bas niveau. Mais d'abord, parlons de boutique de haut niveau. La norme os.stat()
et les os.lstat()
fonctions lèvent les exceptions suivantes lors de la transmission de chemins d'accès non valides:
- Pour les noms de chemin résidant dans des répertoires non existants, des instances de
FileNotFoundError
.
- Pour les chemins résidant dans des répertoires existants:
- Sous Windows, instances
WindowsError
dont l' winerror
attribut est 123
(c'est-à-dire ERROR_INVALID_NAME
).
- Sous tous les autres systèmes d'exploitation:
- Pour les chemins contenant des octets nuls (c'est-à-dire
'\x00'
), des instances de TypeError
.
- Pour les chemins contenant des composants de chemin plus longs que 255 octets, les instances
OSError
dont l' errcode
attribut est:
- SunOS et la famille * BSD de systèmes d' exploitation,
errno.ERANGE
. (Cela semble être un bogue au niveau du système d'exploitation, autrement appelé "interprétation sélective" du standard POSIX.)
- Dans tous les autres systèmes d' exploitation,
errno.ENAMETOOLONG
.
Fondamentalement, cela implique que seuls les chemins résidant dans les répertoires existants sont validables. Les fonctions os.stat()
et os.lstat()
lèvent des FileNotFoundError
exceptions génériques lors de la transmission de noms de chemins résidant dans des répertoires non existants, que ces chemins soient invalides ou non. L'existence de l'annuaire a priorité sur l'invalidité du chemin.
Cela signifie-t-il que les noms de chemins résidant dans des répertoires non existants ne sont pas validables? Oui - sauf si nous modifions ces chemins pour qu'ils résident dans des répertoires existants. Mais est-ce même faisable en toute sécurité? La modification d'un chemin ne devrait-il pas nous empêcher de valider le chemin d'origine?
Pour répondre à cette question, rappelez ci-dessus que les noms de chemin syntaxiquement corrects sur le ext4
système de fichiers ne contiennent aucun composant de chemin (A) contenant des octets nuls ou (B) de plus de 255 octets de longueur. Par conséquent, un ext4
chemin est valide si et seulement si tous les composants de chemin dans ce chemin sont valides. Ceci est vrai pour la plupart des systèmes de fichiers d'intérêt réels .
Cette vision pédante nous aide-t-elle réellement? Oui. Cela réduit le plus gros problème de validation du chemin complet d'un seul coup au problème plus petit de valider uniquement tous les composants du chemin dans ce chemin. Tout chemin arbitraire peut être validé (indépendamment du fait que ce chemin réside ou non dans un répertoire existant) de manière multiplateforme en suivant l'algorithme suivant:
- Divisez ce chemin en composants de chemin (par exemple, le chemin
/troldskog/faren/vild
dans la liste ['', 'troldskog', 'faren', 'vild']
).
- Pour chacun de ces composants:
- Joignez le chemin d'un répertoire garanti d'exister avec ce composant dans un nouveau chemin temporaire (par exemple,
/troldskog
).
- Transmettez ce chemin à
os.stat()
ou os.lstat()
. Si ce chemin d'accès et donc ce composant n'est pas valide, cet appel est garanti pour déclencher une exception exposant le type d'invalidité plutôt qu'une FileNotFoundError
exception générique . Pourquoi? Parce que ce chemin réside dans un répertoire existant. (La logique circulaire est circulaire.)
Existe-t-il un annuaire garanti? Oui, mais typiquement un seul: le répertoire le plus haut du système de fichiers racine (tel que défini ci-dessus).
Passer des noms de chemins résidant dans n'importe quel autre répertoire (et donc pas garanti d'exister) à os.stat()
ou os.lstat()
invite des conditions de concurrence, même si ce répertoire a été précédemment testé pour exister. Pourquoi? Parce que les processus externes ne peuvent pas être empêchés de supprimer simultanément ce répertoire après que ce test a été effectué mais avant que ce chemin ne soit passé à os.stat()
ou os.lstat()
. Libérez les chiens de la folie époustouflante!
L'approche ci-dessus présente également un avantage secondaire substantiel: la sécurité. (Est -ce pas ce bien?) Plus précisément:
Les applications frontales validant les noms de chemins arbitraires provenant de sources non fiables en transmettant simplement ces noms de chemins vers os.stat()
ou os.lstat()
sont susceptibles de subir des attaques par déni de service (DoS) et d'autres manigances au chapeau noir. Des utilisateurs malveillants peuvent tenter de valider à plusieurs reprises les noms de chemins résidant sur des systèmes de fichiers connus pour être obsolètes ou lents (par exemple, les partages NFS Samba); dans ce cas, la statistique aveugle des chemins d'accès entrants est susceptible d'échouer avec les délais de connexion ou de consommer plus de temps et de ressources que votre faible capacité à résister au chômage.
L'approche ci-dessus évite cela en validant uniquement les composants de chemin d'un chemin par rapport au répertoire racine du système de fichiers racine. (Même si cela est obsolète, lent ou inaccessible, vous avez des problèmes plus importants que la validation des chemins.)
Perdu? Génial. Commençons. (Python 3 supposé. Voir "Qu'est-ce que Fragile Hope pour 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
Terminé. Ne plissez pas les yeux sur ce code. ( Ça mord. )
Question n ° 2: Peut-être une existence ou une créabilité de chemin d'accès invalide, hein?
Tester l'existence ou la créabilité de chemins d'accès éventuellement invalides est, étant donné la solution ci-dessus, la plupart du temps trivial. La petite clé ici est d'appeler la fonction précédemment définie avant de tester le chemin passé:
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
Fait et fait. Sauf pas tout à fait.
Question n ° 3: Peut-être une existence de nom de chemin ou une écriture non valide sous Windows
Il existe une mise en garde. Bien sûr que oui.
Comme l' admet la os.access()
documentation officielle :
Remarque: les opérations d'E / S peuvent échouer même si cela os.access()
indique qu'elles réussiraient, en particulier pour les opérations sur les systèmes de fichiers réseau qui peuvent avoir une sémantique d'autorisations au-delà du modèle de bits d'autorisation POSIX habituel.
Sans surprise, Windows est le suspect habituel ici. Grâce à l'utilisation extensive des listes de contrôle d'accès (ACL) sur les systèmes de fichiers NTFS, le modèle simpliste de bits d'autorisation POSIX correspond mal à la réalité Windows sous-jacente. Bien que ce ne soit (sans doute) pas la faute de Python, cela pourrait néanmoins être préoccupant pour les applications compatibles Windows.
Si c'est vous, une alternative plus robuste est souhaitée. Si le chemin passé n'existe pas , nous essayons à la place de créer un fichier temporaire garanti d'être immédiatement supprimé dans le répertoire parent de ce chemin - un test de créabilité plus portable (bien que coûteux):
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
Notez, cependant, que même cela peut ne pas suffire.
Grâce au contrôle d'accès utilisateur (UAC), le Windows Vista toujours inimitable et toutes les itérations ultérieures de celui-ci mentent de manière flagrante sur les autorisations relatives aux répertoires système. Lorsque des utilisateurs non-administrateurs tentent de créer des fichiers dans le répertoire canonique C:\Windows
ou dans les C:\Windows\system32
répertoires, l'UAC autorise superficiellement l'utilisateur à le faire tout en isolant en fait tous les fichiers créés dans un "magasin virtuel" dans le profil de cet utilisateur. (Qui aurait pu imaginer que tromper les utilisateurs aurait des conséquences néfastes à long terme?)
C'est fou. C'est Windows.
Prouve le
Osons-nous? Il est temps de tester les tests ci-dessus.
Étant donné que NULL est le seul caractère interdit dans les chemins d'accès sur les systèmes de fichiers orientés UNIX, exploitons-le pour démontrer la froide et dure vérité - en ignorant les manigances Windows non ignorables, qui m'ennuient et me mettent en colère dans une égale mesure:
>>> 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
Au-delà de la raison. Au-delà de la douleur. Vous trouverez des problèmes de portabilité Python.