tl; dr
Chiama la is_path_exists_or_creatable()
funzione definita di seguito.
Rigorosamente Python 3. È così che rotoliamo.
Un racconto di due domande
La domanda: "Come si verifica la validità del percorso e, per i nomi di percorso validi, l'esistenza o la scrivibilità di quei percorsi?" sono chiaramente due domande separate. Entrambi sono interessanti, e nessuno dei due ha ricevuto una risposta veramente soddisfacente qui ... o, beh, ovunque io possa grep.
La risposta di vikki probabilmente è la più vicina, ma ha i notevoli svantaggi di:
- Inutile aprire ( ... e quindi non chiudere in modo affidabile ) handle di file.
- Scrivere inutilmente ( ... e quindi non riuscire a chiudere o eliminare in modo affidabile ) file a 0 byte.
- Ignorando gli errori specifici del sistema operativo che distinguono tra nomi di percorso non ignorabili e problemi di file system ignorabili. Non sorprende che questo sia fondamentale in Windows. ( Vedi sotto. )
- Ignorando le race condition risultanti da processi esterni simultaneamente (ri) spostando le directory padre del percorso da testare. ( Vedi sotto. )
- Ignorando i timeout di connessione risultanti da questo percorso che risiede su filesystem obsoleti, lenti o altrimenti temporaneamente inaccessibili. Ciò potrebbe esporre i servizi rivolti al pubblico a potenziali attacchi guidati dal DoS . ( Vedi sotto. )
Sistemeremo tutto questo.
Domanda # 0: Qual è ancora la validità del nome del percorso?
Prima di lanciare le nostre fragili tute di carne nei moshpits del dolore crivellati di pitoni, dovremmo probabilmente definire cosa intendiamo per "validità del nome del percorso". Cosa definisce esattamente la validità?
Per "validità del percorso", intendiamo la correttezza sintattica di un percorso rispetto al filesystem radice del sistema corrente, indipendentemente dal fatto che quel percorso o le sue directory padre esistano fisicamente. Un nome di percorso è sintatticamente corretto in questa definizione se è conforme a tutti i requisiti sintattici del filesystem di root.
Con "root filesystem" intendiamo:
- Su sistemi compatibili con POSIX, il filesystem è stato montato nella directory root (
/
).
- Su Windows, il filesystem è montato su
%HOMEDRIVE%
, la lettera di unità con il suffisso dei due punti contenente l'attuale installazione di Windows (normalmente ma non necessariamente C:
).
Il significato di "correttezza sintattica", a sua volta, dipende dal tipo di filesystem di root. Per ext4
(e per la maggior parte ma non per tutti i file system compatibili con POSIX), un percorso è sintatticamente corretto se e solo se quel percorso:
- Non contiene byte nulli (cioè,
\x00
in Python). Questo è un requisito fondamentale per tutti i filesystem compatibili con POSIX.
- Non contiene componenti di percorso più lunghi di 255 byte (ad esempio,
'a'*256
in Python). Un componente percorso è una stringa più lunga di un percorso contenente alcun /
carattere (per esempio, bergtatt
, ind
, i
, e fjeldkamrene
nel percorso /bergtatt/ind/i/fjeldkamrene
).
Correttezza sintattica. File system di root. Questo è tutto.
Domanda n. 1: come faremo ora la validità del nome del percorso?
La convalida dei nomi di percorso in Python è sorprendentemente non intuitiva. Sono assolutamente d'accordo con Fake Name qui: il os.path
pacchetto ufficiale dovrebbe fornire una soluzione immediata per questo. Per ragioni sconosciute (e probabilmente poco convincenti), non è così. Fortunatamente, srotolando la propria soluzione ad-hoc non è che le budella ...
OK, in realtà lo è. È peloso; è brutto; probabilmente ridacchia mentre borbotta e ridacchia quando si illumina. Ma cosa farai? Nuthin '.
Presto scenderemo nell'abisso radioattivo del codice di basso livello. Ma prima parliamo di negozio di alto livello. Lo standard os.stat()
e le os.lstat()
funzioni sollevano le seguenti eccezioni quando vengono passati nomi di percorso non validi:
- Per i nomi di percorso che risiedono in directory non esistenti, istanze di
FileNotFoundError
.
- Per i nomi di percorso che risiedono nelle directory esistenti:
- In Windows, istanze del
WindowsError
cui winerror
attributo è 123
(ie, ERROR_INVALID_NAME
).
- In tutti gli altri sistemi operativi:
- Per i nomi di percorso contenenti byte nulli (cioè
'\x00'
), istanze di TypeError
.
- Per i nomi di percorso contenenti componenti di percorso più lunghi di 255 byte, istanze il
OSError
cui errcode
attributo è:
- Sotto SunOS e * BSD famiglia di sistemi operativi,
errno.ERANGE
. (Questo sembra essere un bug a livello di sistema operativo, altrimenti indicato come "interpretazione selettiva" dello standard POSIX.)
- Sotto tutti gli altri sistemi operativi,
errno.ENAMETOOLONG
.
Fondamentalmente, questo implica che solo i nomi di percorso che risiedono nelle directory esistenti sono validabili. Le funzioni os.stat()
e os.lstat()
sollevano FileNotFoundError
eccezioni generiche quando vengono passati nomi di percorso che risiedono in directory non esistenti, indipendentemente dal fatto che tali nomi di percorso non siano validi o meno. L'esistenza della directory ha la precedenza sull'invalidità del nome del percorso.
Ciò significa che i nomi di percorso che risiedono in directory non esistenti non sono convalidabili? Sì, a meno che non modifichiamo quei nomi di percorso in modo che risiedano nelle directory esistenti. Tuttavia, è anche possibile in modo sicuro? La modifica di un percorso non dovrebbe impedirci di convalidare il percorso originale?
Per rispondere a questa domanda, ricorda dall'alto che i nomi di percorso sintatticamente corretti sul ext4
filesystem non contengono componenti di percorso (A) contenenti byte nulli o (B) di lunghezza superiore a 255 byte. Quindi, un ext4
percorso è valido se e solo se tutti i componenti del percorso in quel percorso sono validi. Questo è vero per la maggior parte dei filesystem di interesse del mondo reale .
Questa intuizione pedante ci aiuta davvero? Sì. Riduce il problema più grande di convalidare il percorso completo in un colpo solo al problema più piccolo di convalidare solo tutti i componenti del percorso in quel percorso. Qualsiasi percorso arbitrario è convalidabile (indipendentemente dal fatto che quel percorso risieda in una directory esistente o meno) in modo multipiattaforma seguendo il seguente algoritmo:
- Dividi quel percorso in componenti del percorso (ad esempio, il percorso
/troldskog/faren/vild
nell'elenco ['', 'troldskog', 'faren', 'vild']
).
- Per ciascuno di questi componenti:
- Unisci il percorso di una directory garantita per esistere con quel componente in un nuovo percorso temporaneo (ad esempio,
/troldskog
).
- Passa quel percorso a
os.stat()
o os.lstat()
. Se quel percorso e quindi quel componente non sono validi, è garantito che questa chiamata sollevi un'eccezione che espone il tipo di invalidità piuttosto che FileNotFoundError
un'eccezione generica . Perché? Perché quel percorso risiede in una directory esistente. (La logica circolare è circolare.)
Esiste una directory garantita per esistere? Sì, ma in genere solo uno: la directory più in alto del filesystem root (come definito sopra).
Il passaggio di nomi di percorso che risiedono in qualsiasi altra directory (e quindi non è garantita l'esistenza) os.stat()
o os.lstat()
invita a condizioni di competizione, anche se quella directory è stata precedentemente testata per esistere. Perché? Perché non è possibile impedire ai processi esterni di rimuovere contemporaneamente quella directory dopo che il test è stato eseguito ma prima che quel percorso venga passato a os.stat()
o os.lstat()
. Scatena i cani della follia folle!
Esiste anche un sostanziale vantaggio collaterale nell'approccio di cui sopra: la sicurezza. (Non è che bello?) In particolare:
Applicazioni frontali che convalidano nomi di percorso arbitrari da fonti non attendibili semplicemente passando tali nomi di percorso a os.stat()
o os.lstat()
sono suscettibili di attacchi Denial of Service (DoS) e altri imbrogli. Gli utenti malintenzionati possono tentare di convalidare ripetutamente nomi di percorso che risiedono su file system noti per essere obsoleti o altrimenti lenti (ad esempio, condivisioni NFS Samba); in tal caso, pronunciare ciecamente i nomi di percorso in entrata rischia di fallire con timeout di connessione o consumare più tempo e risorse della tua debole capacità di resistere alla disoccupazione.
L'approccio precedente evita questo problema convalidando solo i componenti del percorso di un nome di percorso rispetto alla directory root del filesystem root. (Se anche questo è obsoleto, lento o inaccessibile, hai problemi maggiori rispetto alla convalida del nome del percorso.)
Perduto? Grande. Cominciamo. (Si presume Python 3. Vedi "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
Fatto. Non strizzare gli occhi davanti a quel codice. ( Morde. )
Domanda n. 2: Possibile esistenza o creabilità del percorso non valido, eh?
Testare l'esistenza o la creabilità di nomi di percorso potenzialmente non validi è, data la soluzione di cui sopra, per lo più banale. La piccola chiave qui è chiamare la funzione definita in precedenza prima di testare il percorso passato:
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
Fatto e fatto. Tranne non del tutto.
Domanda n. 3: Possibile esistenza o scrivibilità del percorso non valido su Windows
Esiste un avvertimento. Certo che sì.
Come ammette la os.access()
documentazione ufficiale :
Nota: le operazioni di I / O potrebbero non riuscire anche quando os.access()
indica che avrebbero avuto successo, in particolare per le operazioni su file system di rete che potrebbero avere semantiche di permessi oltre il consueto modello POSIX di bit di autorizzazione.
Con sorpresa di nessuno, Windows è il solito sospetto qui. Grazie all'ampio uso di elenchi di controllo di accesso (ACL) sui file system NTFS, il semplicistico modello POSIX a bit di autorizzazione si mappa male alla realtà Windows sottostante. Anche se questo (probabilmente) non è colpa di Python, potrebbe comunque essere motivo di preoccupazione per le applicazioni compatibili con Windows.
Se sei tu, è necessaria un'alternativa più robusta. Se il percorso passato non esiste, proviamo invece a creare un file temporaneo garantito per essere immediatamente cancellato nella directory padre di quel percorso - un test più portabile (se costoso) di creabilità:
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
Nota, tuttavia, che anche questo potrebbe non essere sufficiente.
Grazie a User Access Control (UAC), l'inimitabile Windows Vista e tutte le successive iterazioni mentono palesemente sulle autorizzazioni relative alle directory di sistema. Quando gli utenti non amministratori tentano di creare file nella directory canonica C:\Windows
o nelle C:\Windows\system32
directory, UAC consente superficialmente all'utente di farlo isolando effettivamente tutti i file creati in un "archivio virtuale" nel profilo di quell'utente. (Chi avrebbe potuto immaginare che ingannare gli utenti avrebbe conseguenze dannose a lungo termine?)
Questo è pazzesco. Questo è Windows.
Provalo
Osiamo? È ora di provare i test di cui sopra.
Poiché NULL è l'unico carattere proibito nei nomi di percorso sui filesystem orientati a UNIX, sfruttiamolo per dimostrare la fredda e dura verità, ignorando gli imbrogli non ignorabili di Windows, che francamente mi annoiano e mi fanno arrabbiare in egual misura:
>>> 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
Oltre la sanità mentale. Oltre il dolore. Troverai problemi di portabilità di Python.