tl; dr
Panggil is_path_exists_or_creatable()
fungsi yang ditentukan di bawah ini.
Ketat Python 3. Begitulah cara kami menggulung.
Kisah Dua Pertanyaan
Pertanyaan tentang "Bagaimana cara menguji validitas nama jalur dan, untuk nama jalur yang valid, keberadaan atau kemampuan penulisan jalur tersebut?" jelas merupakan dua pertanyaan terpisah. Keduanya menarik, dan tidak ada yang menerima jawaban yang benar-benar memuaskan di sini ... atau, di mana pun yang bisa saya grep.
vikki 's jawabannya mungkin hews yang paling dekat, namun memiliki kelemahan yang luar biasa:
- Tidak perlu membuka ( ... dan kemudian gagal menutup dengan andal ) pegangan file.
- Tidak perlu menulis ( ... dan kemudian gagal menutup atau menghapus secara andal ) file 0-byte.
- Mengabaikan kesalahan khusus OS yang membedakan antara nama jalur tidak valid yang tidak dapat diabaikan dan masalah sistem file yang dapat diabaikan. Tidak mengherankan, ini sangat penting di bawah Windows. ( Lihat di bawah. )
- Mengabaikan kondisi balapan yang dihasilkan dari proses eksternal secara bersamaan memindahkan (kembali) direktori induk dari nama jalur yang akan diuji. ( Lihat di bawah. )
- Mengabaikan waktu tunggu koneksi yang diakibatkan dari nama jalur ini yang berada di sistem file usang, lambat, atau sementara tidak dapat diakses. Ini dapat mengekspos layanan yang dihadapi publik terhadap potensi serangan yang didorong oleh DoS . ( Lihat di bawah. )
Kami akan memperbaiki semua itu.
Pertanyaan # 0: Apa itu Pathname Validity Lagi?
Sebelum melemparkan pakaian daging kita yang rapuh ke dalam moshpits yang penuh dengan ular piton, kita mungkin harus mendefinisikan apa yang kita maksud dengan "validitas nama jalur". Apa sebenarnya yang mendefinisikan validitas?
Yang kami maksud dengan "validitas nama jalur" adalah kebenaran sintaksis dari nama jalur yang terkait dengan sistem file root dari sistem saat ini - terlepas dari apakah jalur tersebut atau direktori induknya secara fisik ada. Sebuah nama jalur secara sintaksis benar di bawah definisi ini jika itu memenuhi semua persyaratan sintaksis dari sistem berkas root.
Yang kami maksud dengan "sistem file root" adalah:
- Pada sistem yang kompatibel dengan POSIX, sistem file dipasang ke direktori root (
/
).
- Pada Windows, sistem berkas dipasang ke
%HOMEDRIVE%
, huruf kandar dengan akhiran titik dua yang berisi penginstalan Windows saat ini (biasanya tetapi tidak harus C:
).
Arti "kebenaran sintaksis", pada gilirannya, bergantung pada jenis sistem file root. Untuk ext4
(dan sebagian besar tetapi tidak semua sistem file yang kompatibel dengan POSIX), nama jalur secara sintaksis benar jika dan hanya jika nama jalur itu:
- Tidak mengandung byte nol (yaitu,
\x00
dengan Python). Ini adalah persyaratan yang sulit untuk semua sistem file yang kompatibel dengan POSIX.
- Tidak berisi komponen jalur yang lebih panjang dari 255 byte (misalnya,
'a'*256
dengan Python). Sebuah komponen path adalah substring terpanjang dari pathname yang tidak mengandung /
karakter (misalnya, bergtatt
, ind
, i
, dan fjeldkamrene
di pathname /bergtatt/ind/i/fjeldkamrene
).
Ketepatan sintaksis. Sistem file root. Itu dia.
Pertanyaan # 1: Bagaimana Sekarang Kita Melakukan Validitas Pathname?
Memvalidasi nama jalur dengan Python ternyata tidak intuitif. Saya sangat setuju dengan Nama Palsu di sini: os.path
paket resmi harus memberikan solusi out-of-the-box untuk ini. Untuk alasan yang tidak diketahui (dan mungkin tidak menarik), ternyata tidak. Untungnya, membuka gulungan solusi ad-hoc Anda sendiri tidak terlalu memilukan ...
Oke, sebenarnya begitu. Itu berbulu; itu jorok; itu mungkin terkekeh saat menggelegar dan cekikikan saat bersinar. Tapi apa yang akan kamu lakukan? Nuthin '.
Kami akan segera turun ke jurang radioaktif kode tingkat rendah. Tapi pertama-tama, mari kita bicara tentang toko tingkat tinggi. Standar os.stat()
dan os.lstat()
fungsi memunculkan pengecualian berikut saat meneruskan nama jalur yang tidak valid:
- Untuk nama jalur yang berada di direktori yang tidak ada, contoh
FileNotFoundError
.
- Untuk nama jalur yang berada di direktori yang ada:
- Di bawah Windows, contoh
WindowsError
yang winerror
atributnya adalah 123
(yaitu, ERROR_INVALID_NAME
).
- Di bawah semua OS lainnya:
- Untuk nama jalur yang berisi byte nol (yaitu,
'\x00'
), instance dari TypeError
.
- Untuk nama jalur yang berisi komponen jalur lebih panjang dari 255 byte, contoh
OSError
yang errcode
atributnya adalah:
- Di bawah SunOS dan keluarga OS * BSD
errno.ERANGE
,. (Ini tampaknya merupakan bug tingkat OS, atau disebut sebagai "interpretasi selektif" dari standar POSIX.)
- Di bawah semua OS lainnya
errno.ENAMETOOLONG
,.
Yang terpenting, ini menyiratkan bahwa hanya nama jalur yang berada di direktori yang ada yang dapat divalidasi. Fungsi os.stat()
dan os.lstat()
memunculkan FileNotFoundError
pengecualian umum ketika nama jalur yang diteruskan yang berada di direktori yang tidak ada, terlepas dari apakah nama jalur tersebut tidak valid atau tidak. Keberadaan direktori lebih diutamakan daripada ketidakabsahan nama jalur.
Apakah ini berarti bahwa nama path yang berada di direktori yang tidak ada tidak dapat divalidasi? Ya - kecuali kita memodifikasi nama path tersebut agar berada di direktori yang ada. Namun, apakah itu mungkin secara aman? Bukankah memodifikasi nama jalur mencegah kami memvalidasi nama jalur asli?
Untuk menjawab pertanyaan ini, ingat dari atas bahwa nama jalur yang benar secara sintaksis pada sistem ext4
file tidak berisi komponen jalur (A) yang berisi byte nol atau (B) dengan panjang lebih dari 255 byte. Karenanya, ext4
nama jalur valid jika dan hanya jika semua komponen jalur dalam nama jalur itu valid. Hal ini berlaku untuk sebagian besar sistem file dunia nyata yang menarik.
Apakah wawasan bertele-tele itu benar-benar membantu kita? Iya. Ini mengurangi masalah yang lebih besar dalam memvalidasi nama jalur lengkap dalam satu gerakan ke masalah yang lebih kecil hanya memvalidasi semua komponen jalur dalam nama jalur itu. Nama jalur arbitrer apa pun dapat divalidasi (terlepas dari apakah nama jalur tersebut berada di direktori yang ada atau tidak) secara lintas platform dengan mengikuti algoritme berikut:
- Pisahkan nama jalur tersebut menjadi beberapa komponen jalur (misalnya, nama
/troldskog/faren/vild
jalur ke dalam daftar ['', 'troldskog', 'faren', 'vild']
).
- Untuk setiap komponen tersebut:
- Gabung dengan nama jalur direktori yang dijamin ada dengan komponen itu menjadi nama jalur sementara baru (mis.,
/troldskog
).
- Berikan nama jalur itu ke
os.stat()
atau os.lstat()
. Jika nama jalur itu dan karenanya komponen itu tidak valid, panggilan ini dijamin akan memunculkan pengecualian yang mengekspos jenis ketidakabsahan daripada FileNotFoundError
pengecualian umum . Mengapa? Karena nama jalur itu berada di direktori yang sudah ada. (Logika melingkar adalah melingkar.)
Apakah ada direktori yang dijamin ada? Ya, tetapi biasanya hanya satu: direktori paling atas dari sistem berkas root (seperti yang didefinisikan di atas).
Meneruskan nama jalur yang berada di direktori lain (dan karenanya tidak dijamin ada) ke os.stat()
atau os.lstat()
mengundang kondisi balapan, meskipun direktori tersebut sebelumnya telah diuji keberadaannya. Mengapa? Karena proses eksternal tidak dapat dicegah untuk secara bersamaan menghapus direktori tersebut setelah pengujian dilakukan, tetapi sebelum nama jalur tersebut diteruskan ke os.stat()
atau os.lstat()
. Bebaskan anjing kegilaan yang menghancurkan pikiran!
Ada juga manfaat sampingan yang substansial dari pendekatan di atas: keamanan. (Bukankah itu bagus?) Secara khusus:
Aplikasi hadap depan yang memvalidasi nama jalur sewenang-wenang dari sumber yang tidak tepercaya hanya dengan meneruskan nama jalur tersebut ke os.stat()
atau os.lstat()
rentan terhadap serangan Denial of Service (DoS) dan kejahatan black-hat lainnya. Pengguna jahat mungkin mencoba untuk berulang kali memvalidasi nama path yang berada pada filesystem yang dikenal basi atau lambat (misalnya, NFS Samba share); dalam hal ini, nama jalur masuk yang secara membabi buta dapat menyebabkan kegagalan pada akhirnya dengan batas waktu koneksi atau menghabiskan lebih banyak waktu dan sumber daya daripada kapasitas Anda yang lemah untuk menahan pengangguran.
Pendekatan di atas menghilangkan ini dengan hanya memvalidasi komponen path dari nama path terhadap direktori root dari filesystem root. (Bahkan jika itu basi, lambat, atau tidak dapat diakses, Anda memiliki masalah yang lebih besar daripada validasi nama jalur.)
Kalah? Bagus. Mari kita mulai. (Python 3 diasumsikan. Lihat "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
Selesai. Jangan menyipitkan mata pada kode itu. ( Itu menggigit. )
Pertanyaan # 2: Keberadaan atau Keberadaan Nama Jalur yang Mungkin Tidak Valid, Eh?
Menguji keberadaan atau pembuatan nama jalur yang mungkin tidak valid, mengingat solusi di atas, kebanyakan sepele. Kunci kecil di sini adalah memanggil fungsi yang ditentukan sebelumnya sebelum menguji jalur yang dilewati:
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
Selesai dan selesai. Kecuali tidak sepenuhnya.
Pertanyaan # 3: Keberadaan Pathname atau Penulisan yang Mungkin Tidak Valid di Windows
Ada peringatan. Tentu saja ada.
Seperti yang diakui oleh os.access()
dokumentasi resmi :
Catatan: Operasi I / O mungkin gagal bahkan ketika os.access()
menunjukkan bahwa mereka akan berhasil, terutama untuk operasi pada sistem file jaringan yang mungkin memiliki semantik izin di luar model bit izin POSIX biasa.
Tidak mengherankan, Windows adalah tersangka biasa di sini. Berkat penggunaan Access Control List (ACL) yang ekstensif pada sistem file NTFS, model izin-bit POSIX yang sederhana memetakan dengan buruk ke realitas Windows yang mendasarinya. Meskipun ini (bisa dibilang) bukan kesalahan Python, itu mungkin menjadi perhatian untuk aplikasi yang kompatibel dengan Windows.
Jika ini Anda, alternatif yang lebih kuat dibutuhkan. Jika jalur yang diteruskan tidak ada, kami malah mencoba membuat file sementara yang dijamin akan segera dihapus di direktori induk dari jalur tersebut - tes kreasi yang lebih portabel (jika mahal):
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
Perhatikan, bagaimanapun, bahwa ini pun mungkin tidak cukup.
Berkat User Access Control (UAC), Windows Vista yang selalu tidak dapat ditiru dan semua iterasi berikutnya secara terang - terangan berbohong tentang perizinan yang berkaitan dengan direktori sistem. Ketika pengguna non-Administrator mencoba membuat file baik di kanonis C:\Windows
atau C:\Windows\system32
direktori, UAC secara dangkal mengizinkan pengguna untuk melakukannya sambil benar-benar mengisolasi semua file yang dibuat ke dalam "Toko Virtual" di profil pengguna tersebut. (Siapa yang menyangka bahwa menipu pengguna akan memiliki konsekuensi jangka panjang yang berbahaya?)
Ini gila. Ini adalah Windows.
Buktikan itu
Berani kita? Saatnya untuk menguji coba tes di atas.
Karena NULL adalah satu-satunya karakter yang dilarang dalam nama path pada sistem file berorientasi UNIX, mari kita manfaatkan itu untuk menunjukkan kebenaran yang dingin dan sulit - mengabaikan kejahatan Windows yang tidak dapat diabaikan, yang terus terang membuat saya bosan dan marah dalam ukuran yang sama:
>>> 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
Di luar kewarasan. Melampaui rasa sakit. Anda akan menemukan masalah portabilitas Python.