Repository navigation
Expand file tree
/
Copy patharchive.py
More file actions
39 lines (34 loc) · 1.54 KB
/
Copy patharchive.py
File metadata and controls
39 lines (34 loc) · 1.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import hashlib
import zipfile
_CHUNK_SIZE = 64 * 1024
def _member_sha256(zf, info):
digest = hashlib.sha256()
with zf.open(info) as member:
for chunk in iter(lambda: member.read(_CHUNK_SIZE), b""):
digest.update(chunk)
return digest.hexdigest()
def contents_sha256(fileobj):
"""
Identity hash of a zip archive's member paths and bytes, independent of compression and metadata.
FROZEN: ricecooker and Studio's upload Cloud Function index archives by this digest.
:param fileobj: A seekable binary file object of a zip archive
:return: A hex SHA-256 digest
"""
with zipfile.ZipFile(fileobj) as zf:
members = {}
for info in zf.infolist():
# Not .filename: Windows rewrites "\\" in it and Python >= 3.12 overrides it from 0x7075 extra fields.
path = info.orig_filename
# Before the directory skip: zipfile reads "x\0/" as file "x".
if "\0" in path:
raise ValueError(f"NUL in member path {path!r}")
if path.endswith("/"):
continue
if path in members:
raise ValueError(f"Duplicate member path {path!r}")
members[path] = info
member_digests = {path: _member_sha256(zf, info) for path, info in sorted(members.items(), key=lambda item: item[1].header_offset)}
digest = hashlib.sha256()
for path, member_digest in sorted(member_digests.items()):
digest.update(f"{path}\0{member_digest}\n".encode("utf-8"))
return digest.hexdigest()