From 77c6c286cdc5d760251e5489018a79219cb84028 Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:54:55 +1100 Subject: [PATCH] Split merge: accept 64-bit data descriptors on small streamed ZIP entries Streaming writers (Python's unseekable ZipFile, force_zip64) write ZIP64 data descriptors even for small files, so the descriptor width can't be taken from the final sizes. The reader now accepts the signed or unsigned, 32- or 64-bit layout that matches the central directory and fits before the next entry, preferring one that ends exactly there, then the width the local header's ZIP64 metadata implies. Format tests cover streamed ZIP64 stored and deflated entries. Co-Authored-By: Claude Opus 5.5 --- tests/test_quest.py | 9 +++++++++ ui/frame_android.py | 46 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/tests/test_quest.py b/tests/test_quest.py index 4a8edfd..2ff008f 100644 --- a/tests/test_quest.py +++ b/tests/test_quest.py @@ -887,10 +887,19 @@ class Round3(unittest.TestCase): sink = Sink() with zipfile.ZipFile(sink, 'w', method) as z: z.writestr('assets/streamed', content) + # Streamed with ZIP64 forced: a 64-bit data descriptor for a small file. + with z.open('assets/streamed64', 'w', force_zip64=True) as f: + f.write(content) + z.writestr('assets/after', b'next entry') path = self.tmpfile(sink.buf.getvalue()) with zipfile.ZipFile(path) as z: self.assertTrue(z.getinfo('assets/streamed').flag_bits & 0x08) + self.assertTrue(z.getinfo('assets/streamed64').flag_bits & 0x08) + self.assertLess(z.getinfo('assets/streamed64').file_size, 0xffffffff) + self.assertEqual(z.read('assets/streamed64'), content) # zipfile accepts it self.assertEqual(android.read_member(path, 'assets/streamed'), content) + self.assertEqual(android.read_member(path, 'assets/streamed64'), content) + self.assertEqual(android.read_member(path, 'assets/after'), b'next entry') def test_central_sizes_and_checksum_forged(self): # The reviewer's case: stored 'abcdef', central sizes 3 and the prefix's checksum, local untouched. diff --git a/ui/frame_android.py b/ui/frame_android.py index 3db689c..9dc5715 100644 --- a/ui/frame_android.py +++ b/ui/frame_android.py @@ -331,6 +331,16 @@ def _zip64_sizes(extra, usize, csize): return usize, csize +def _has_zip64_extra(extra): + at = 0 + while at + 4 <= len(extra): + kind, length = struct.unpack_from('= 0xffffffff or info.compress_size >= 0xffffffff else 12 - tail = raw.read(4 + want) - signed = tail[:4] == b'PK\7\x08' - body = tail[4:4 + want] if signed else tail[:want] - if len(body) < want: + # Sizes and checksum follow the data: (signature,) crc, then sizes of 4 bytes each, + # or 8 for ZIP64. Streaming writers use the ZIP64 form even for small files when + # sizes weren't known up front, so the width isn't implied by the final sizes: take + # the layout that matches the central directory and fits before the next entry, + # preferring one that ends exactly there, then the width the local header implies. + zip64 = 0xffffffff in (csize, usize) or _has_zip64_extra(extra) + tail = raw.read(24) + if len(tail) < 12: raise FrameError(f'{name}: truncated data descriptor') - crc, csize, usize = struct.unpack_from('