tl; dr
เรียกใช้is_path_exists_or_creatable()
ฟังก์ชันที่กำหนดไว้ด้านล่าง
Python 3 อย่างเคร่งครัดนั่นเป็นเพียงวิธีที่เราหมุน
เรื่องของสองคำถาม
คำถามที่ว่า "ฉันจะทดสอบความถูกต้องของชื่อพา ธ ได้อย่างไรและสำหรับชื่อพา ธ ที่ถูกต้องการมีอยู่หรือความสามารถในการเขียนของเส้นทางเหล่านั้น" เป็นคำถามสองข้อที่แยกจากกันอย่างชัดเจน ทั้งสองอย่างน่าสนใจและไม่ได้รับคำตอบที่น่าพอใจอย่างแท้จริงที่นี่ ... หรือทุกที่ที่ฉันสามารถ grep ได้
คำตอบของvikkiอาจจะใกล้เคียงที่สุด แต่มีข้อเสียที่น่าทึ่งคือ:
- โดยไม่จำเป็นต้องเปิด ( ... แล้วไม่สามารถปิดได้อย่างน่าเชื่อถือ ) ที่จับไฟล์
- ไม่จำเป็นต้องเขียน ( ... แล้วล้มเหลวในการปิดหรือลบที่เชื่อถือได้ ) ไฟล์ 0 ไบต์
- การละเว้นข้อผิดพลาดเฉพาะระบบปฏิบัติการที่แยกความแตกต่างระหว่างชื่อพา ธ ที่ไม่ถูกต้องที่ไม่สามารถละเว้นและปัญหาระบบไฟล์ที่ละเว้นได้ ไม่น่าแปลกใจที่นี่เป็นสิ่งสำคัญสำหรับ Windows ( ดูด้านล่าง )
- ละเว้นเงื่อนไขการแข่งขันที่เกิดจากกระบวนการภายนอกพร้อมกัน (อีกครั้ง) การย้ายไดเร็กทอรีหลักของชื่อพา ธ ที่จะทดสอบ ( ดูด้านล่าง )
- การละเว้นการหมดเวลาการเชื่อมต่อซึ่งเป็นผลมาจากชื่อพา ธ นี้ที่อยู่ในระบบไฟล์เก่าช้าหรือไม่สามารถเข้าถึงได้ชั่วคราว สิ่งนี้อาจเปิดเผยบริการที่เปิดเผยต่อสาธารณะถึงการโจมตี DoS -driven ที่อาจเกิดขึ้น ( ดูด้านล่าง )
เราจะแก้ไขทั้งหมดนั้น
คำถาม # 0: ความถูกต้องของชื่อพา ธ คืออะไรอีกครั้ง?
ก่อนที่จะเหวี่ยงชุดเนื้อบอบบางของเราเข้าไปในโพรงแห่งความเจ็บปวดที่เต็มไปด้วยงูหลามเราน่าจะนิยามความหมายของ "ความถูกต้องของชื่อพา ธ " สิ่งที่กำหนดความถูกต้องคืออะไร?
โดย "ความถูกต้องของชื่อพา ธ " เราหมายถึงความถูกต้องทางไวยากรณ์ของชื่อพา ธ ที่เกี่ยวข้องกับระบบไฟล์รูทของระบบปัจจุบัน - ไม่ว่าพา ธ นั้นหรือไดเร็กทอรีพาเรนต์นั้นจะมีอยู่จริง ชื่อพา ธ ถูกต้องตามหลักไวยากรณ์ภายใต้ข้อกำหนดนี้หากเป็นไปตามข้อกำหนดทางวากยสัมพันธ์ทั้งหมดของระบบไฟล์รูท
โดย "root filesystem" เราหมายถึง:
- บนระบบที่เข้ากันได้กับ POSIX ระบบไฟล์ที่ติดตั้งกับไดเร็กทอรีราก (
/
)
- บน Windows ระบบไฟล์ที่ติดตั้งกับ
%HOMEDRIVE%
อักษรชื่อไดรฟ์ที่ต่อท้ายด้วยโคลอนที่มีการติดตั้ง Windows ปัจจุบัน (โดยทั่วไป แต่ไม่จำเป็นC:
)
ในทางกลับกันความหมายของ "ความถูกต้องทางวากยสัมพันธ์" ขึ้นอยู่กับประเภทของระบบไฟล์รูท สำหรับระบบไฟล์ext4
(และส่วนใหญ่ แต่ไม่ใช่ทั้งหมดที่เข้ากันได้กับ POSIX) ชื่อพา ธ จะถูกต้องตามหลักไวยากรณ์ถ้าชื่อพา ธ นั้น
- ไม่มีไบต์ว่าง (เช่น
\x00
ใน Python) นี่เป็นข้อกำหนดที่ยากสำหรับระบบไฟล์ที่เข้ากันได้กับ POSIX ทั้งหมด
- ไม่มีส่วนประกอบพา ธ ที่ยาวเกิน 255 ไบต์ (เช่น
'a'*256
ใน Python) องค์ประกอบที่เป็นเส้นทางย่อยที่ยาวที่สุดของพา ธ ที่มีไม่มี/
ตัวอักษร (เช่นbergtatt
, ind
, i
และfjeldkamrene
ในพา ธ ที่/bergtatt/ind/i/fjeldkamrene
)
ความถูกต้องของไวยากรณ์ ระบบไฟล์รูท แค่นั้นแหละ.
คำถาม # 1: ตอนนี้เราจะทำให้ชื่อพา ธ ถูกต้องได้อย่างไร?
การตรวจสอบชื่อพา ธ ใน Python นั้นไม่ง่ายอย่างน่าประหลาดใจ ฉันเห็นด้วยกับFake Nameที่นี่: os.path
แพ็คเกจอย่างเป็นทางการควรมีโซลูชันที่พร้อมใช้งานสำหรับสิ่งนี้ ด้วยเหตุผลที่ไม่รู้จัก (และอาจไม่ได้อธิบาย) ก็ไม่ได้ โชคดีที่การคลายการแก้ปัญหาเฉพาะกิจของคุณเองไม่ใช่เรื่องที่น่าเบื่อ ...
ตกลงมันเป็นจริง มันมีขน; มันน่ารังเกียจ; มันอาจจะสั่นขณะที่มันระเบิดและหัวเราะคิกคักเมื่อมันเรืองแสง แต่คุณจะทำอย่างไร Nuthin '.
ในไม่ช้าเราจะลงสู่ก้นบึ้งของกัมมันตภาพรังสีของรหัสระดับต่ำ แต่ก่อนอื่นเรามาพูดถึงร้านค้าระดับสูง มาตรฐานos.stat()
และos.lstat()
ฟังก์ชันจะเพิ่มข้อยกเว้นต่อไปนี้เมื่อส่งชื่อพา ธ ที่ไม่ถูกต้อง:
- สำหรับชื่อพา ธ ที่อยู่ในไดเร็กทอรีที่ไม่มีอยู่อินสแตนซ์ของ
FileNotFoundError
.
- สำหรับชื่อพา ธ ที่อยู่ในไดเร็กทอรีที่มีอยู่:
- ภายใต้ Windows อินสแตนซ์
WindowsError
ที่มีwinerror
แอตทริบิวต์คือ123
(เช่นERROR_INVALID_NAME
)
- ภายใต้ระบบปฏิบัติการอื่น ๆ ทั้งหมด:
- สำหรับชื่อพา ธ ที่มี null ไบต์ (เช่น
'\x00'
) อินสแตนซ์ของTypeError
.
- สำหรับชื่อพา ธ ที่มีส่วนประกอบพา ธ ที่ยาวกว่า 255 ไบต์อินสแตนซ์
OSError
ที่มีerrcode
แอตทริบิวต์คือ:
- ภายใต้ SunOS และครอบครัว * BSD
errno.ERANGE
ระบบปฏิบัติการ, (สิ่งนี้ดูเหมือนจะเป็นข้อบกพร่องระดับ OS หรือเรียกอีกอย่างว่า "การตีความแบบเลือก" ของมาตรฐาน POSIX)
- ภายใต้ OS อื่น ๆ ทั้งหมด,
errno.ENAMETOOLONG
.
โดยนัยสำคัญนี้หมายความว่าเฉพาะชื่อพา ธ ที่อยู่ในไดเร็กทอรีที่มีอยู่เท่านั้นที่สามารถตรวจสอบได้ os.stat()
และos.lstat()
ฟังก์ชั่นเพิ่มทั่วไปFileNotFoundError
ยกเว้นเมื่อผ่าน pathnames พำนักอยู่ในไดเรกทอรีไม่ใช่ที่มีอยู่โดยไม่คำนึงว่า pathnames เหล่านี้จะไม่ถูกต้องหรือไม่ การมีอยู่ของไดเร็กทอรีมีความสำคัญเหนือความไม่ถูกต้องของชื่อพา ธ
นี่หมายความว่าชื่อพา ธ ที่อยู่ในไดเร็กทอรีที่ไม่มีอยู่นั้นไม่สามารถใช้ได้หรือไม่? ใช่ - เว้นแต่เราจะปรับเปลี่ยนชื่อพา ธ เหล่านั้นให้อยู่ในไดเร็กทอรีที่มีอยู่ เป็นไปได้อย่างปลอดภัยหรือไม่? การแก้ไขชื่อพา ธ ไม่ควรทำให้เราไม่สามารถตรวจสอบชื่อพา ธ เดิมได้ใช่หรือไม่
ในการตอบคำถามนี้โปรดจำจากด้านบนว่าชื่อพา ธ ที่ถูกต้องตามหลักไวยากรณ์บนext4
ระบบไฟล์ไม่มีส่วนประกอบพา ธ(A)ที่มี null ไบต์หรือ(B) ที่มีความยาวเกิน 255 ไบต์ ดังนั้นext4
ชื่อพา ธ จึงใช้ได้ก็ต่อเมื่อคอมโพเนนต์พา ธ ทั้งหมดในชื่อพา ธ นั้นถูกต้อง นี่เป็นเรื่องจริงสำหรับระบบไฟล์ที่น่าสนใจในโลกแห่งความเป็นจริงส่วนใหญ่
ความเข้าใจเชิงลึกนั้นช่วยเราได้จริงหรือ ใช่. ช่วยลดปัญหาใหญ่ในการตรวจสอบความถูกต้องของชื่อพา ธ แบบเต็มในคราวเดียวไปจนถึงปัญหาเล็ก ๆ ในการตรวจสอบความถูกต้องเฉพาะส่วนประกอบของเส้นทางทั้งหมดในชื่อพา ธ นั้น ชื่อพา ธ ที่กำหนดเองสามารถตรวจสอบได้ (ไม่ว่าชื่อพา ธ นั้นจะอยู่ในไดเร็กทอรีที่มีอยู่หรือไม่ก็ตาม) ในลักษณะข้ามแพลตฟอร์มโดยทำตามอัลกอริทึมต่อไปนี้:
- แยกชื่อพา ธ นั้นออกเป็นส่วนประกอบพา ธ (เช่นชื่อพา ธ
/troldskog/faren/vild
ในรายการ['', 'troldskog', 'faren', 'vild']
)
- สำหรับแต่ละองค์ประกอบดังกล่าว:
- รวมชื่อพา ธ ของไดเร็กทอรีที่รับประกันว่ามีอยู่กับคอมโพเนนต์นั้นในชื่อพา ธ ชั่วคราวใหม่ (เช่น
/troldskog
)
- ผ่านชื่อพา ธ ที่หรือ
os.stat()
os.lstat()
หากชื่อพา ธ นั้นและด้วยเหตุนี้ส่วนประกอบนั้นจึงไม่ถูกต้องการเรียกนี้จะรับประกันได้ว่าจะยกข้อยกเว้นที่เปิดเผยประเภทของความไม่ถูกต้องแทนที่จะเป็นFileNotFoundError
ข้อยกเว้นทั่วไป ทำไม? เนื่องจากชื่อพา ธ นั้นอยู่ในไดเร็กทอรีที่มีอยู่ (ลอจิกแบบวงกลมคือวงกลม)
มีไดเร็กทอรีที่รับประกันว่ามีอยู่จริงหรือไม่? ใช่ แต่โดยทั่วไปจะมีเพียงไดเร็กทอรีบนสุดของระบบไฟล์รูท (ตามที่กำหนดไว้ด้านบน)
การส่งชื่อพา ธ ที่อยู่ในไดเร็กทอรีอื่น ๆ (และไม่รับประกันว่าจะมีอยู่) ไปยังos.stat()
หรือos.lstat()
เชิญเงื่อนไขการแข่งขันแม้ว่าไดเร็กทอรีนั้นจะถูกทดสอบก่อนหน้านี้ ทำไม? เพราะกระบวนการภายนอกไม่สามารถป้องกันจากการควบลบไดเรกทอรีที่หลังจากการทดสอบที่ได้รับการดำเนินการ แต่ก่อนที่จะพา ธ ที่ส่งผ่านไปหรือos.stat()
os.lstat()
ปลดปล่อยสุนัขบ้าคลั่ง!
แนวทางข้างต้นมีประโยชน์มากมายเช่นกัน: ความปลอดภัย (ไม่ว่าดี?) โดยเฉพาะ:
แอปพลิเคชันแบบหันหน้าไปทางด้านหน้าตรวจสอบความถูกต้องของชื่อพา ธ โดยพลการจากแหล่งที่มาที่ไม่น่าเชื่อถือโดยเพียงแค่ส่งชื่อพา ธ ดังกล่าวไปยังos.stat()
หรือos.lstat()
มีความอ่อนไหวต่อการโจมตี Denial of Service (DoS) และการโจมตีด้วยหมวกดำอื่น ๆ ผู้ใช้ที่เป็นอันตรายอาจพยายามตรวจสอบชื่อพา ธ ซ้ำ ๆ ที่อยู่ในระบบไฟล์ที่ทราบว่าเก่าหรือช้า (เช่น NFS Samba share) ในกรณีนี้การระบุชื่อพา ธ ขาเข้าแบบสุ่มสี่สุ่มห้ามีแนวโน้มที่จะล้มเหลวในที่สุดด้วยการหมดเวลาการเชื่อมต่อหรือใช้เวลาและทรัพยากรมากกว่าความสามารถที่อ่อนแอของคุณในการต้านทานการว่างงาน
วิธีการข้างต้นขัดขวางสิ่งนี้โดยการตรวจสอบความถูกต้องของส่วนประกอบพา ธ ของชื่อพา ธ กับไดเร็กทอรีรากของระบบไฟล์รูทเท่านั้น (หากแม้ว่าสิ่งนั้นจะค้างช้าหรือไม่สามารถเข้าถึงได้แสดงว่าคุณมีปัญหาใหญ่กว่าการตรวจสอบชื่อพา ธ )
สูญหาย? เยี่ยมมาก เอาล่ะ. (สมมติว่า Python 3 ดู "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
เสร็จแล้ว อย่าเหล่รหัสนั้น ( มันกัด )
คำถาม # 2: ชื่อพา ธ ที่มีอยู่หรือความคิดสร้างสรรค์อาจไม่ถูกต้องเอ๊ะ?
การทดสอบการมีอยู่หรือความสามารถในการสร้างสรรค์ของชื่อพา ธ ที่อาจไม่ถูกต้องนั้นให้วิธีแก้ปัญหาข้างต้นซึ่งส่วนใหญ่เป็นเรื่องเล็กน้อย คีย์เล็ก ๆ ที่นี่คือการเรียกใช้ฟังก์ชันที่กำหนดไว้ก่อนหน้านี้ก่อนที่จะทดสอบเส้นทางที่ผ่าน:
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
เสร็จสิ้นและทำ ยกเว้นไม่มาก
คำถาม # 3: ชื่อพา ธ ที่มีอยู่หรือความสามารถในการเขียนบน Windows อาจไม่ถูกต้อง
มีข้อแม้ มีแน่นอน
ตามที่os.access()
เอกสารอย่างเป็นทางการยอมรับ:
หมายเหตุ:การดำเนินการ I / O อาจล้มเหลวแม้ว่าจะos.access()
ระบุว่าจะประสบความสำเร็จโดยเฉพาะอย่างยิ่งสำหรับการดำเนินการบนระบบไฟล์เครือข่ายซึ่งอาจมีความหมายของสิทธิ์นอกเหนือจากโมเดลบิตสิทธิ์ POSIX ตามปกติ
ไม่แปลกใจเลยที่ Windows เป็นผู้ต้องสงสัยตามปกติที่นี่ ด้วยการใช้ Access Control Lists (ACL) อย่างกว้างขวางบนระบบไฟล์ NTFS โมเดลบิตอนุญาต POSIX ที่เรียบง่ายจะจับคู่กับความเป็นจริงของ Windows ที่ไม่ดี แม้ว่าสิ่งนี้ (เนื้อหา) จะไม่ใช่ความผิดพลาดของ Python แต่ก็อาจเป็นเรื่องที่น่ากังวลสำหรับแอปพลิเคชันที่เข้ากันได้กับ Windows
หากนี่คือคุณต้องการทางเลือกอื่นที่มีประสิทธิภาพมากขึ้น หากเส้นทางผ่านไม่ได้อยู่เราแทนที่จะพยายามที่จะสร้างแฟ้มชั่วคราวรับประกันว่าจะถูกลบออกทันทีในไดเรกทอรีหลักของเส้นทางที่ - การทดสอบแบบพกพาอื่น ๆ (ถ้าแพง) ของ creatability:
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
อย่างไรก็ตามโปรดทราบว่าสิ่งนี้อาจไม่เพียงพอ
ขอบคุณ User Access Control (UAC), Windows Vista ที่ไม่เคยมีมาก่อนและการทำซ้ำที่ตามมาทั้งหมดนั้นโกหกอย่างโจ่งแจ้งเกี่ยวกับสิทธิ์ที่เกี่ยวข้องกับไดเรกทอรีระบบ เมื่อผู้ใช้ที่ไม่ใช่ผู้ดูแลพยายามที่จะสร้างไฟล์ในทั้งที่ยอมรับC:\Windows
หรือC:\Windows\system32
ไดเรกทอรี UAC เผินอนุญาตให้ผู้ที่จะทำในขณะที่จริงการแยกไฟล์ที่สร้างขึ้นทั้งหมดลงใน "เสมือนร้าน" ในโปรไฟล์ของผู้ใช้ว่า (ใครจะคิดได้บ้างว่าการหลอกลวงผู้ใช้จะส่งผลเสียในระยะยาว)
มันบ้าไปแล้ว. นี่คือ Windows
พิสูจน์สิ
กล้าไหม ถึงเวลาทดลองขับการทดสอบข้างต้น
เนื่องจาก NULL เป็นอักขระตัวเดียวที่ห้ามใช้ในชื่อพา ธ บนระบบไฟล์ที่เน้น UNIX เรามาใช้ประโยชน์จากสิ่งนั้นเพื่อแสดงให้เห็นถึงความจริงที่เย็นชาและยากโดยไม่สนใจ Windows shenanigans ที่ไม่เพิกเฉยซึ่งทำให้เบื่อและโกรธฉันในระดับที่เท่าเทียมกัน:
>>> 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
เกินความมีสติ นอกเหนือจากความเจ็บปวด คุณจะพบข้อกังวลเกี่ยวกับการพกพา Python