Skip to content

Commit 73660e5

Browse files
agattidpgeorge
authored andcommitted
python-stdlib/zipfile: Add read-only zipfile module.
This commit adds an implementation for the `zipfile` module that is able to open Zip archives, enumerate their contents, and read data out of the single files present in the archive. The `zipfile` implementation in this commit is relatively limited, but should be enough for most cases in which data is being read out of a Zip archive. Things like file extraction, or CRC checking are not implemented as they are probably better handled on a per-case basis on embedded targets anyway. Other features not supported are additional compression modes besides STORED and DEFLATED, encryption, large files, and files that contain an additional data descriptor with the proper file size and CRC. Most of the CPython API is implemented, except for `zipfile.Path`, `zipfile.PyZipFile` and other specific exceptions besides `zipfile.BadZipFile`. Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
1 parent 7c1a527 commit 73660e5

8 files changed

Lines changed: 531 additions & 0 deletions

File tree

‎python-stdlib/zipfile/README.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# zipfile
2+
3+
This library implements part of Python's `zipfile` module, namely allowing you
4+
to open archives and read data out of them - as long as files are not compressed
5+
with an unsupported compressor or encrypted.
6+
7+
One difference between CPython's `zipfile` implementation of `ZipFile` and this
8+
module's is that the former does enumerate the files contained in an archive
9+
when opening it, and this information is cached and still available even after
10+
the archive has been closed via `ZipFile.close`. To save memory, this
11+
implementation neither performs any automatic enumeration on open, nor caches
12+
the files list. Attempting to interact with a `ZipFile` instance once it's been
13+
closed will raise a `ValueError` exception.
14+
15+
## Installation
16+
17+
Use `mip` via `mpremote`:
18+
19+
```bash
20+
> mpremote mip install zipfile
21+
```
22+
23+
See [Package
24+
management](https://docs.micropython.org/en/latest/reference/packages.html) for
25+
more details on using `mip` and `mpremote`.

‎python-stdlib/zipfile/manifest.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
metadata(version="0.1.0")
2+
3+
module("zipfile.py")
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import io
2+
import sys
3+
import unittest
4+
import zipfile
5+
6+
# TODO: Find out which archiver can create ZSTD blobs
7+
null_zip_contents = (
8+
("null_bzip2.bin", 12, 4096, 46, (2026, 9, 4, 10, 1, 28), 0xC71C0011),
9+
("null_deflate.bin", zipfile.ZIP_DEFLATED, 4096, 20, (2026, 9, 4, 10, 1, 32), 0xC71C0011),
10+
("null_lzma.bin", 14, 4096, 41, (2026, 9, 4, 10, 1, 32), 0xC71C0011),
11+
("null_stored.bin", zipfile.ZIP_STORED, 4096, 4096, (2026, 9, 4, 10, 1, 20), 0xC71C0011),
12+
)
13+
14+
deflate_zip_contents = (
15+
("directory/", True, 0, 0, 0, None),
16+
("directory/hello_again.txt", False, 6656, 66, 0x946F8D8C, b"Hello again, MicroPython!\n"),
17+
("empty", False, 0, 0, 0, None),
18+
("hello.txt", False, 5120, 57, 0xA89FD9B3, b"Hello, MicroPython!\n"),
19+
)
20+
21+
stored_zip_contents = (
22+
("directory/", True, 0, 0, 0, None),
23+
("directory/hello_again.txt", False, 6656, 6656, 0x946F8D8C, b"Hello again, MicroPython!\n"),
24+
("empty", False, 0, 0, 0, None),
25+
("hello.txt", False, 5120, 5120, 0xA89FD9B3, b"Hello, MicroPython!\n"),
26+
)
27+
28+
TEST_NULL = "tests/null.zip"
29+
TEST_STORED = "tests/stored.zip"
30+
TEST_DEFLATED = "tests/deflated.zip"
31+
32+
class TestZipFile(unittest.TestCase):
33+
def check_header(self, file, expected):
34+
name, method, uncompressed_size, compressed_size, timestamp, crc32 = expected
35+
self.assertEqual(name, file.filename)
36+
self.assertEqual(method, file.compress_type)
37+
self.assertEqual(uncompressed_size, file.file_size)
38+
self.assertEqual(compressed_size, file.compress_size)
39+
self.assertEqual(timestamp, file.date_time)
40+
self.assertEqual(crc32, file.CRC)
41+
42+
def test_null_iter(self):
43+
zf = zipfile.ZipFile(TEST_NULL)
44+
for index, info in enumerate(zf.infolist()):
45+
self.check_header(info, null_zip_contents[index])
46+
zf.close()
47+
48+
def test_null_missing(self):
49+
with zipfile.ZipFile(TEST_NULL) as zf:
50+
with self.assertRaises(KeyError):
51+
zf.getinfo("missing.txt")
52+
53+
def test_null_stored_read(self):
54+
zf = zipfile.ZipFile(TEST_NULL)
55+
expected = bytes(4096)
56+
for index, name in enumerate(zf.namelist()):
57+
self.assertEqual(name, null_zip_contents[index][0])
58+
if null_zip_contents[index][1] != zipfile.ZIP_STORED:
59+
continue
60+
self.assertEqual(zf.read(name), expected)
61+
zf.close()
62+
63+
def test_null_deflated_read(self):
64+
if sys.implementation.name == "micropython":
65+
try:
66+
import deflate
67+
except ImportError:
68+
self.skipTest("no deflate module")
69+
zf = zipfile.ZipFile(TEST_NULL)
70+
expected = bytes(4096)
71+
for index, name in enumerate(zf.namelist()):
72+
self.assertEqual(name, null_zip_contents[index][0])
73+
if null_zip_contents[index][1] != zipfile.ZIP_DEFLATED:
74+
continue
75+
self.assertEqual(zf.read(name), expected)
76+
zf.close()
77+
78+
def test_null_unsupported_read(self):
79+
if sys.implementation.name != "micropython":
80+
self.skipTest("cannot guarantee lack of methods support")
81+
zf = zipfile.ZipFile(TEST_NULL)
82+
for index, name in enumerate(zf.namelist()):
83+
self.assertEqual(name, null_zip_contents[index][0])
84+
if null_zip_contents[index][1] not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED):
85+
with self.assertRaises(NotImplementedError):
86+
zf.read(name)
87+
zf.close()
88+
89+
def test_null_stored_open(self):
90+
zf = zipfile.ZipFile(TEST_NULL)
91+
expected = bytes(4096)
92+
for index, name in enumerate(zf.namelist()):
93+
self.assertEqual(name, null_zip_contents[index][0])
94+
if null_zip_contents[index][1] != zipfile.ZIP_STORED:
95+
continue
96+
with zf.open(name) as f:
97+
self.assertEqual(expected, f.read())
98+
zf.close()
99+
100+
def test_null_deflated_open(self):
101+
if sys.implementation.name == "micropython":
102+
try:
103+
import deflate
104+
except ImportError:
105+
self.skipTest("no deflate module")
106+
zf = zipfile.ZipFile(TEST_NULL)
107+
expected = bytes(4096)
108+
for index, name in enumerate(zf.namelist()):
109+
self.assertEqual(name, null_zip_contents[index][0])
110+
if null_zip_contents[index][1] != zipfile.ZIP_DEFLATED:
111+
continue
112+
with zf.open(name) as f:
113+
self.assertEqual(expected, f.read())
114+
zf.close()
115+
116+
def test_null_unsupported_open(self):
117+
if sys.implementation.name != "micropython":
118+
self.skipTest("cannot guarantee lack of methods support")
119+
zf = zipfile.ZipFile(TEST_NULL)
120+
for index, name in enumerate(zf.namelist()):
121+
self.assertEqual(name, null_zip_contents[index][0])
122+
if null_zip_contents[index][1] not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED):
123+
with self.assertRaises(NotImplementedError):
124+
with zf.open(name):
125+
self.fail()
126+
zf.close()
127+
128+
def test_null_getinfo(self):
129+
zf = zipfile.ZipFile(TEST_NULL)
130+
for zc in null_zip_contents:
131+
info = zf.getinfo(zc[0])
132+
self.check_header(info, zc)
133+
zf.close()
134+
135+
def test_context(self):
136+
with zipfile.ZipFile(TEST_NULL) as zf:
137+
for index, info in enumerate(zf.infolist()):
138+
self.check_header(info, null_zip_contents[index])
139+
140+
def test_close(self):
141+
zf = zipfile.ZipFile(TEST_NULL)
142+
zf.close()
143+
zf.close() # check this doesn't raise
144+
145+
# CPython doesn't raise as the archive directory is cached and not
146+
# flushed once `close` is called. `namelist` and `infolist` do not
147+
# raise on CPython.
148+
if sys.implementation.name == "micropython":
149+
with self.assertRaises(ValueError):
150+
zf.namelist()
151+
with self.assertRaises(ValueError):
152+
zf.infolist()
153+
with self.assertRaises(ValueError):
154+
zf.open(null_zip_contents[0][0])
155+
with self.assertRaises(ValueError):
156+
with zf.open(null_zip_contents[0][0]):
157+
self.fail()
158+
with self.assertRaises(ValueError):
159+
zf.read(null_zip_contents[0][0])
160+
161+
def check_contents(self, zf, found, expected, method):
162+
self.assertEqual(found.is_dir(), expected[1])
163+
self.assertEqual(found.file_size, expected[2])
164+
self.assertEqual(found.compress_size, expected[3])
165+
self.assertEqual(found.CRC, expected[4])
166+
self.assertEqual(found.compress_type, zipfile.ZIP_STORED if expected[2] == 0 else method)
167+
if payload := expected[5]:
168+
data = payload * 256
169+
with zf.open(expected[0]) as f:
170+
self.assertEqual(f.read(), data)
171+
self.assertEqual(zf.read(expected[0]), data)
172+
173+
def test_read_deflate(self):
174+
if sys.implementation.name == "micropython":
175+
try:
176+
import deflate
177+
except ImportError:
178+
self.skipTest("no deflate module")
179+
with zipfile.ZipFile(TEST_DEFLATED) as zf:
180+
for file in deflate_zip_contents:
181+
info = zf.getinfo(file[0])
182+
self.check_contents(zf, info, file, zipfile.ZIP_DEFLATED)
183+
184+
def test_read_stored(self):
185+
with zipfile.ZipFile(TEST_STORED) as zf:
186+
for file in stored_zip_contents:
187+
info = zf.getinfo(file[0])
188+
self.check_contents(zf, info, file, zipfile.ZIP_STORED)
189+
190+
def test_is_zipfile(self):
191+
for n in (TEST_STORED, TEST_NULL, TEST_DEFLATED):
192+
self.assertEqual(zipfile.is_zipfile(n), True)
193+
with open(n, "rb") as f:
194+
old_offset = f.tell()
195+
self.assertEqual(zipfile.is_zipfile(f), True)
196+
self.assertEqual(f.tell(), old_offset)
197+
198+
self.assertEqual(zipfile.is_zipfile("test_zipfile.py"), False)
199+
with open("test_zipfile.py", "rb") as f:
200+
old_offset = f.tell()
201+
self.assertEqual(zipfile.is_zipfile(f), False)
202+
self.assertEqual(f.tell(), old_offset)
203+
204+
self.assertEqual(zipfile.is_zipfile("missing.zip"), False)
205+
self.assertEqual(zipfile.is_zipfile("zipfile"), False)
206+
207+
with io.BytesIO() as empty:
208+
self.assertEqual(zipfile.is_zipfile(empty), False)
691 Bytes
Binary file not shown.
4.68 KB
Binary file not shown.
12.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)