งูต้นฉบับเปิดช่องเล็กในตู้เอกสารอัดแน่นเพื่อหยิบกระดาษเพียงใบเดียวโดยไม่เททั้งตู้

Python zipfile.Path: อ่านไฟล์ใน ZIP โดยไม่แตกทั้งก้อน

พี่โชว์Engineering

บางงานต้องอ่าน manifest ใบเดียวจาก ZIP ไม่ได้ต้องแตกทุกไฟล์ลง disk zipfile.Path ให้ API แบบ path-like สำหรับเดิน directory และอ่านสมาชิกตรง ๆ ลดไฟล์ชั่วคราวและทำให้ขอบเขตงานชัดขึ้นครับ

WHY: เปิดลิ้นชักเดียวแทนเทตู้ทั้งใบ

การ extract ทั้ง archive เหมือนเทเอกสารทุกแผ่นลงพื้นเพื่อหาใบเดียว นอกจากช้า ยังเพิ่มพื้นที่ที่ path แปลก ๆ จะเขียนไฟล์ zipfile.Path ช่วยอ่านสมาชิกเฉพาะจุดจาก archive แต่ผู้ใช้ยังต้องตรวจชื่อและขนาด เพราะ class นี้ไม่ได้ sanitize filename ให้เอง

import io
import unittest
import zipfile


def read_report(payload: bytes) -> str:
    with zipfile.ZipFile(io.BytesIO(payload)) as archive:
        target = zipfile.Path(archive, at="reports/today.txt")
        if not target.is_file():
            raise FileNotFoundError("reports/today.txt")
        return target.read_text(encoding="utf-8")


class ZipPathTests(unittest.TestCase):
    def test_reads_one_member(self) -> None:
        buffer = io.BytesIO()
        with zipfile.ZipFile(buffer, "w") as archive:
            archive.writestr("reports/today.txt", "ready")
            archive.writestr("large/unused.txt", "x" * 1000)
        self.assertEqual(read_report(buffer.getvalue()), "ready")

    def test_missing_member(self) -> None:
        buffer = io.BytesIO()
        with zipfile.ZipFile(buffer, "w"):
            pass
        with self.assertRaises(FileNotFoundError):
            read_report(buffer.getvalue())


if __name__ == "__main__":
    unittest.main()

HOW: อ่านตรง ๆ ไม่ได้แปลว่าปลอดภัยทุกด้าน

ก่อนรับ archive ที่ไม่เชื่อถือ ให้จำกัดขนาดไฟล์บีบอัด จำนวนสมาชิก ขนาดรวมหลังคลาย และ compression ratio เพื่อกัน zip bomb หากชื่อสมาชิกมาจาก input ให้ reject absolute path, .. และ path ที่ไม่ตรง allowlist แม้ไม่ได้ extract เพราะชื่อเหล่านี้อาจถูกนำไปใช้ในขั้นตอนต่อไป

Path.open() และ read_text() จะคลายข้อมูลสมาชิกที่เลือกอยู่ดี จึงต้องเช็ก ZipInfo.file_size ก่อนอ่าน อย่าโหลดสมาชิกหลายกิกะไบต์เข้า memory เพียงเพราะไม่ได้เขียนลง disk และปิด ZipFile ด้วย context manager เสมอ

Recap

  • ใช้ zipfile.Path เมื่ออ่านสมาชิกเฉพาะจุด
  • ตรวจว่า target เป็นไฟล์และมีอยู่จริง
  • จำกัดจำนวนสมาชิก ขนาดรวม และ compression ratio
  • validate ชื่อสมาชิก; zipfile.Path ไม่ sanitize ให้
  • เช็ก ZipInfo.file_size ก่อนอ่านไฟล์ใหญ่