rtzip 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rtzip-0.1.0/PKG-INFO +5 -0
- rtzip-0.1.0/pyproject.toml +7 -0
- rtzip-0.1.0/setup.cfg +4 -0
- rtzip-0.1.0/src/rtzip/__init__.py +3 -0
- rtzip-0.1.0/src/rtzip/errors.py +10 -0
- rtzip-0.1.0/src/rtzip/models.py +244 -0
- rtzip-0.1.0/src/rtzip/remotezip.py +238 -0
- rtzip-0.1.0/src/rtzip/utils.py +15 -0
- rtzip-0.1.0/src/rtzip.egg-info/PKG-INFO +5 -0
- rtzip-0.1.0/src/rtzip.egg-info/SOURCES.txt +11 -0
- rtzip-0.1.0/src/rtzip.egg-info/dependency_links.txt +1 -0
- rtzip-0.1.0/src/rtzip.egg-info/requires.txt +1 -0
- rtzip-0.1.0/src/rtzip.egg-info/top_level.txt +1 -0
rtzip-0.1.0/PKG-INFO
ADDED
rtzip-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import struct
|
|
3
|
+
from collections.abc import Buffer
|
|
4
|
+
from typing import Annotated, Self
|
|
5
|
+
|
|
6
|
+
import obstruct as obs
|
|
7
|
+
|
|
8
|
+
EXTRA_FIELD_HEADER = struct.Struct("<HH")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_extra_fields(buffer):
|
|
12
|
+
res = {}
|
|
13
|
+
|
|
14
|
+
buffer = memoryview(buffer)
|
|
15
|
+
length = buffer.nbytes
|
|
16
|
+
offset = 0
|
|
17
|
+
|
|
18
|
+
while offset + 4 <= length:
|
|
19
|
+
header_id, n_data = EXTRA_FIELD_HEADER.unpack_from(buffer, offset)
|
|
20
|
+
|
|
21
|
+
offset += 4
|
|
22
|
+
res[header_id] = buffer[offset: offset + n_data].tobytes()
|
|
23
|
+
offset += n_data
|
|
24
|
+
|
|
25
|
+
return res
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclasses.dataclass(slots=True)
|
|
29
|
+
class EOCD(obs.Struct):
|
|
30
|
+
disk_num: obs.u16
|
|
31
|
+
cd_disk: obs.u16
|
|
32
|
+
disk_entries: obs.u16
|
|
33
|
+
total_entries: obs.u16
|
|
34
|
+
cd_size: obs.u32
|
|
35
|
+
cd_offset: obs.u32
|
|
36
|
+
comment_length: obs.u16
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_buffer(cls, buffer: Buffer, offset=4):
|
|
40
|
+
return cls.unpack_from(buffer, offset)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclasses.dataclass(slots=True)
|
|
44
|
+
class Zip64EOCD(obs.Struct):
|
|
45
|
+
# 签名部分已忽略
|
|
46
|
+
record_size: obs.u64
|
|
47
|
+
version: obs.u16
|
|
48
|
+
unzip_min_version: obs.u16
|
|
49
|
+
disk_id: obs.u32
|
|
50
|
+
cd_disk: obs.u32
|
|
51
|
+
disk_entries: obs.u64
|
|
52
|
+
total_entries: obs.u64
|
|
53
|
+
cd_size: obs.u64
|
|
54
|
+
cd_offset: obs.u64
|
|
55
|
+
extra_data: Annotated[bytes, obs.Marks.NOT_A_CFIELD]
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def extra_offset(self):
|
|
59
|
+
return self.__cstruct__.size
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def extra_length(self):
|
|
63
|
+
return self.record_size - self.__cstruct__.size
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_buffer(cls, buffer, offset=4, header_only: bool = False):
|
|
67
|
+
self = cls.unpack_from(buffer, offset)
|
|
68
|
+
|
|
69
|
+
extra_offset = self.extra_offset + offset
|
|
70
|
+
extra_length = self.extra_length
|
|
71
|
+
|
|
72
|
+
if not header_only:
|
|
73
|
+
self.extra_data = buffer[extra_offset: extra_offset + extra_length]
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclasses.dataclass(slots=True)
|
|
78
|
+
class CDEntry(obs.Struct):
|
|
79
|
+
version: obs.u16
|
|
80
|
+
unzip_min_version: obs.u16
|
|
81
|
+
flags: obs.u16
|
|
82
|
+
algorithm: obs.u16
|
|
83
|
+
last_modified: obs.u16
|
|
84
|
+
last_modified_date: obs.u16
|
|
85
|
+
crc32_raw: obs.u32
|
|
86
|
+
compressed_size: obs.u32
|
|
87
|
+
original_size: obs.u32
|
|
88
|
+
filename_len: obs.u16
|
|
89
|
+
extra_len: obs.u16
|
|
90
|
+
comment_len: obs.u16
|
|
91
|
+
disk_id: obs.u16
|
|
92
|
+
inner_file_attrs: obs.u16
|
|
93
|
+
outer_file_attrs: obs.u32
|
|
94
|
+
local_header_offset: obs.u32
|
|
95
|
+
|
|
96
|
+
filename: Annotated[bytes, obs.Marks.NOT_A_CFIELD]
|
|
97
|
+
raw_extra_fields: Annotated[bytes, obs.Marks.NOT_A_CFIELD]
|
|
98
|
+
comment: Annotated[bytes, obs.Marks.NOT_A_CFIELD]
|
|
99
|
+
|
|
100
|
+
_extra_fields: Annotated[dict | None, obs.Marks.NOT_A_CFIELD] = dataclasses.field(default=None, repr=False)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def is_encrypted(self):
|
|
104
|
+
return bool(self.flags & 0x0001)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def is_zip64(self):
|
|
108
|
+
return self.compressed_size == 0xFFFFFFFF or self.original_size == 0xFFFFFFFF
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def has_data_descriptor(self):
|
|
112
|
+
return bool(self.flags & 0b100)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def extra_fields(self) -> dict[int, bytes]:
|
|
116
|
+
if getattr(self, '_extra_fields', None) is None:
|
|
117
|
+
self._extra_fields = parse_extra_fields(self.raw_extra_fields)
|
|
118
|
+
|
|
119
|
+
return self._extra_fields
|
|
120
|
+
|
|
121
|
+
def fix_by_zip64(self):
|
|
122
|
+
zip64_data = (self.extra_fields.get(1))
|
|
123
|
+
offset = 0
|
|
124
|
+
if zip64_data:
|
|
125
|
+
zip64_data = memoryview(zip64_data)
|
|
126
|
+
length = len(zip64_data)
|
|
127
|
+
if self.original_size == 0xFFFFFFFF:
|
|
128
|
+
assert offset + 8 <= length
|
|
129
|
+
self.original_size = int.from_bytes(zip64_data[offset:offset + 8], 'little')
|
|
130
|
+
offset += 8
|
|
131
|
+
if self.compressed_size == 0xFFFFFFFF:
|
|
132
|
+
assert offset + 8 <= length
|
|
133
|
+
self.compressed_size = int.from_bytes(zip64_data[offset:offset + 8], 'little')
|
|
134
|
+
offset += 8
|
|
135
|
+
if self.local_header_offset == 0xFFFFFFFF:
|
|
136
|
+
assert offset + 8 <= length
|
|
137
|
+
self.local_header_offset = int.from_bytes(zip64_data[offset: offset + 8], 'little')
|
|
138
|
+
offset += 8
|
|
139
|
+
if self.disk_id == 0xFFFF:
|
|
140
|
+
assert offset + 4 <= length
|
|
141
|
+
self.disk_id = int.from_bytes(zip64_data[offset: offset + 4], 'little')
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_buffer(cls, buffer: Buffer, offset=4) -> tuple[Self, int]:
|
|
145
|
+
buffer = memoryview(buffer)
|
|
146
|
+
# print(buffer.tobytes())
|
|
147
|
+
header_size = cls.__cstruct__.size
|
|
148
|
+
|
|
149
|
+
self = cls.unpack_from(buffer, offset)
|
|
150
|
+
filename_len, extra_len, comment_len = self.filename_len, self.extra_len, self.comment_len
|
|
151
|
+
|
|
152
|
+
pos = offset + header_size
|
|
153
|
+
filename = bytes(buffer[pos: pos + filename_len])
|
|
154
|
+
pos += filename_len
|
|
155
|
+
|
|
156
|
+
extra_attrs = buffer[pos: pos + extra_len].tobytes()
|
|
157
|
+
pos += extra_len
|
|
158
|
+
|
|
159
|
+
comment = buffer[pos: pos + comment_len].tobytes()
|
|
160
|
+
pos += comment_len
|
|
161
|
+
|
|
162
|
+
self.filename = filename
|
|
163
|
+
self.raw_extra_fields = extra_attrs
|
|
164
|
+
self.comment = comment
|
|
165
|
+
|
|
166
|
+
# print(self)
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
self,
|
|
170
|
+
pos - offset
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclasses.dataclass()
|
|
175
|
+
class LocalFileHeader(obs.Struct):
|
|
176
|
+
unzip_min_version: obs.u16
|
|
177
|
+
flags: obs.u16
|
|
178
|
+
algorithm: obs.u16
|
|
179
|
+
last_modified_time: obs.u16
|
|
180
|
+
last_modified_date: obs.u16
|
|
181
|
+
crc32: obs.u32
|
|
182
|
+
compressed_size: obs.u32
|
|
183
|
+
uncompressed_size: obs.u32
|
|
184
|
+
filename_len: obs.u16
|
|
185
|
+
extra_len: obs.u16
|
|
186
|
+
|
|
187
|
+
filename: Annotated[bytes, obs.Marks.NOT_A_CFIELD]
|
|
188
|
+
raw_extra_fields: Annotated[bytes, obs.Marks.NOT_A_CFIELD]
|
|
189
|
+
|
|
190
|
+
_extra_fields: Annotated[dict[int, bytes] | None, obs.Marks.NOT_A_CFIELD] = None
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def extra_fields(self):
|
|
194
|
+
if self._extra_fields is None:
|
|
195
|
+
self._extra_fields = parse_extra_fields(self.raw_extra_fields)
|
|
196
|
+
|
|
197
|
+
return self._extra_fields
|
|
198
|
+
|
|
199
|
+
def fix_by_zip64(self):
|
|
200
|
+
zip64_data = self.extra_fields.get(0x0001)
|
|
201
|
+
if zip64_data:
|
|
202
|
+
original_size, compressed_size = struct.unpack('<QQ', zip64_data)
|
|
203
|
+
self.uncompressed_size = original_size
|
|
204
|
+
self.compressed_size = compressed_size
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_buffer(cls, buffer: Buffer, offset: int = 4, header_only=False) -> tuple[Self, int]:
|
|
208
|
+
"""从缓冲区解析 Local File Header,返回 (实例, 消耗字节数)"""
|
|
209
|
+
buf = memoryview(buffer)
|
|
210
|
+
header_size = cls.__cstruct__.size
|
|
211
|
+
|
|
212
|
+
# 解析固定头部
|
|
213
|
+
self = cls.unpack_from(buf, offset)
|
|
214
|
+
|
|
215
|
+
if header_only:
|
|
216
|
+
return self, header_size
|
|
217
|
+
|
|
218
|
+
# 提取长度字段
|
|
219
|
+
filename_len = self.filename_len
|
|
220
|
+
extra_len = self.extra_len
|
|
221
|
+
|
|
222
|
+
pos = offset + header_size
|
|
223
|
+
|
|
224
|
+
# 读取文件名
|
|
225
|
+
filename = bytes(buf[pos:pos + filename_len])
|
|
226
|
+
pos += filename_len
|
|
227
|
+
|
|
228
|
+
# 读取额外字段
|
|
229
|
+
extra_fields = bytes(buf[pos:pos + extra_len])
|
|
230
|
+
pos += extra_len
|
|
231
|
+
|
|
232
|
+
self.filename = filename
|
|
233
|
+
self.raw_extra_fields = extra_fields
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
self,
|
|
237
|
+
pos - offset
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def data_offset(self) -> int:
|
|
242
|
+
"""返回实际文件数据的起始偏移量(相对于 Local File Header 起始位置)"""
|
|
243
|
+
# 固定头 30 字节 + 文件名长度 + 额外字段长度
|
|
244
|
+
return self.__cstruct__.size + self.filename_len + self.extra_len + 4
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
from typing import AsyncGenerator
|
|
3
|
+
|
|
4
|
+
from .errors import UnsupportedAlgorithmError, UnsupportedCryptoError, UnsupportedDataDescriptorError
|
|
5
|
+
from .models import EOCD, Zip64EOCD, CDEntry, LocalFileHeader
|
|
6
|
+
from .utils import deflate_wrapper, single_chunk_wrapper
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DataSourceCallbacks:
|
|
10
|
+
async def read_range(self, offset, length) -> bytes | bytearray | memoryview:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
def read_range_stream(self, offset, length) -> AsyncGenerator[bytes | bytearray | memoryview]:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
async def get_total_size(self) -> int:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RemoteZip:
|
|
21
|
+
EOCD_SIGNATURE = b'PK\x05\x06'
|
|
22
|
+
CDENTITY_SIGNATURE = b'PK\x01\x02'
|
|
23
|
+
LFH_SIGNATURE = b'PK\x03\x04'
|
|
24
|
+
ZIP64_EOCD_LOCATOR_SIGNATURE = b'PK\x06\x07'
|
|
25
|
+
ZIP64_EOCD_SIGNATURE = b'PK\x06\x06'
|
|
26
|
+
|
|
27
|
+
async def _read_range(self, offset, length):
|
|
28
|
+
return await self.source.read_range(offset, length)
|
|
29
|
+
|
|
30
|
+
def _read_range_stream(self, offset, length):
|
|
31
|
+
return self.source.read_range_stream(offset, length)
|
|
32
|
+
|
|
33
|
+
async def _get_total_size(self):
|
|
34
|
+
return await self.source.get_total_size()
|
|
35
|
+
|
|
36
|
+
def __init__(self, source: DataSourceCallbacks):
|
|
37
|
+
self.source = source
|
|
38
|
+
|
|
39
|
+
self.cd_info: EOCD | Zip64EOCD | None = None
|
|
40
|
+
self.files: list[CDEntry] = []
|
|
41
|
+
self.file_mapping: dict[str, CDEntry] | None = None
|
|
42
|
+
|
|
43
|
+
self.is_zip64 = False
|
|
44
|
+
|
|
45
|
+
async def fetch_eocd(self, initial_chunk=4096, max_cnt=-1, loss_factor: int | float = 2):
|
|
46
|
+
chunk_size = initial_chunk
|
|
47
|
+
|
|
48
|
+
last_start = await self._get_total_size()
|
|
49
|
+
cnt = 0
|
|
50
|
+
|
|
51
|
+
buffer = b''
|
|
52
|
+
while last_start - chunk_size > 0:
|
|
53
|
+
cnt += 1
|
|
54
|
+
content = await self._read_range(last_start - chunk_size, chunk_size)
|
|
55
|
+
|
|
56
|
+
buffer = content + buffer[:32]
|
|
57
|
+
|
|
58
|
+
signature_pos = buffer.rfind(RemoteZip.EOCD_SIGNATURE)
|
|
59
|
+
if signature_pos != -1:
|
|
60
|
+
self.cd_info = eocd = EOCD.from_buffer(buffer[signature_pos:])
|
|
61
|
+
# return cnt
|
|
62
|
+
if eocd.total_entries == 0xFFFFFFFF or eocd.cd_offset == 0xFFFFFFFF or eocd.cd_size == 0xFFFFFFFF:
|
|
63
|
+
self.is_zip64 = True
|
|
64
|
+
|
|
65
|
+
# 继续获取 zip64 的 eocd
|
|
66
|
+
# zip64 locator 在 eocd 前的 20 字节处
|
|
67
|
+
# 字段包括 4 字节的签名,两个 8 字节数据
|
|
68
|
+
|
|
69
|
+
zip64_locator_offset = signature_pos - 20
|
|
70
|
+
# 检查需要的数据是否已经存在
|
|
71
|
+
if zip64_locator_offset < 0:
|
|
72
|
+
# print("--------------------Zero Padding")
|
|
73
|
+
buffer = (
|
|
74
|
+
await self._read_range(
|
|
75
|
+
last_start - chunk_size - abs(zip64_locator_offset),
|
|
76
|
+
abs(zip64_locator_offset)
|
|
77
|
+
)
|
|
78
|
+
+ buffer
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
zip64_locator_offset = 0
|
|
82
|
+
|
|
83
|
+
# print(buffer)
|
|
84
|
+
|
|
85
|
+
assert buffer[zip64_locator_offset:zip64_locator_offset + 4] == self.ZIP64_EOCD_LOCATOR_SIGNATURE
|
|
86
|
+
zip64_eocd_disk_id, zip64_eocd_offset, zip64_total_disks = \
|
|
87
|
+
struct.unpack_from('<4xIQI', buffer, zip64_locator_offset)
|
|
88
|
+
|
|
89
|
+
# print(zip64_eocd_disk_id, zip64_eocd_offset, zip64_total_disks)
|
|
90
|
+
|
|
91
|
+
zip64_eocd_data = await self._read_range(zip64_eocd_offset, 96)
|
|
92
|
+
# print(zip64_eocd_data[:64])
|
|
93
|
+
assert zip64_eocd_data[:4] == self.ZIP64_EOCD_SIGNATURE
|
|
94
|
+
|
|
95
|
+
zip64_eocd = Zip64EOCD.from_buffer(zip64_eocd_data, header_only=True)
|
|
96
|
+
# print(zip64_eocd)
|
|
97
|
+
|
|
98
|
+
extra_offset = zip64_eocd.extra_offset + 4
|
|
99
|
+
extra_length = zip64_eocd.extra_length
|
|
100
|
+
extra_end = extra_offset + extra_length
|
|
101
|
+
|
|
102
|
+
# print(extra_offset, extra_end, buffer[extra_offset:extra_end])
|
|
103
|
+
|
|
104
|
+
if extra_end > len(zip64_eocd_data):
|
|
105
|
+
zip64_eocd_data += await self._read_range(
|
|
106
|
+
zip64_eocd_offset + 64,
|
|
107
|
+
extra_end - len(zip64_eocd_data)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
extra_data = zip64_eocd_data[extra_offset:extra_offset + extra_length]
|
|
111
|
+
|
|
112
|
+
zip64_eocd.extra_data = extra_data
|
|
113
|
+
|
|
114
|
+
# print(zip64_eocd)
|
|
115
|
+
|
|
116
|
+
self.cd_info = zip64_eocd
|
|
117
|
+
return cnt
|
|
118
|
+
else:
|
|
119
|
+
return cnt
|
|
120
|
+
|
|
121
|
+
if max_cnt != -1 and cnt >= max_cnt:
|
|
122
|
+
raise RuntimeError
|
|
123
|
+
|
|
124
|
+
last_start -= chunk_size
|
|
125
|
+
chunk_size = int(chunk_size * loss_factor)
|
|
126
|
+
else:
|
|
127
|
+
raise RuntimeError
|
|
128
|
+
|
|
129
|
+
async def fetch_cd(self, ignore_extra: bool = False):
|
|
130
|
+
assert self.cd_info
|
|
131
|
+
eocd = self.cd_info
|
|
132
|
+
buffer = bytearray()
|
|
133
|
+
files = []
|
|
134
|
+
|
|
135
|
+
def consume_entry():
|
|
136
|
+
nonlocal buffer
|
|
137
|
+
entry, nbytes = CDEntry.from_buffer(buffer)
|
|
138
|
+
|
|
139
|
+
if self.is_zip64:
|
|
140
|
+
entry.fix_by_zip64()
|
|
141
|
+
|
|
142
|
+
files.append(entry)
|
|
143
|
+
buffer = buffer[nbytes + 4:]
|
|
144
|
+
|
|
145
|
+
async for chunk in self._read_range_stream(eocd.cd_offset, eocd.cd_size):
|
|
146
|
+
# print(chunk)
|
|
147
|
+
buffer.extend(chunk)
|
|
148
|
+
|
|
149
|
+
while buffer and buffer[:4] == self.CDENTITY_SIGNATURE:
|
|
150
|
+
try:
|
|
151
|
+
consume_entry()
|
|
152
|
+
except struct.error:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if buffer and not ignore_extra:
|
|
156
|
+
# print("Read Entries: ", len(files))
|
|
157
|
+
# print("Buffer:", buffer)
|
|
158
|
+
raise ValueError()
|
|
159
|
+
|
|
160
|
+
self.files = files
|
|
161
|
+
|
|
162
|
+
async def fetch_single_file_header(self, filename=None, entry: CDEntry | None = None):
|
|
163
|
+
filename = str(filename)
|
|
164
|
+
entry = entry or self.find_file_entry(filename)
|
|
165
|
+
buffer = await self._read_range(entry.local_header_offset, entry.__cstruct__.size + 64) # 预留64字节的变长字段
|
|
166
|
+
|
|
167
|
+
# 仅头部模式解析 LocalFileHeader
|
|
168
|
+
header, _ = LocalFileHeader.from_buffer(buffer, 4, header_only=True)
|
|
169
|
+
if header.extra_len + header.filename_len > 64:
|
|
170
|
+
_ = await self._read_range(entry.local_header_offset + 64, header.extra_len + header.filename_len - 64)
|
|
171
|
+
buffer = bytearray(buffer) + _
|
|
172
|
+
|
|
173
|
+
pos = header.__cstruct__.size + 4 # 4 字节的签名在计算 nbytes 时会被忽略,实际需要加上
|
|
174
|
+
filename = buffer[pos: pos + header.filename_len]
|
|
175
|
+
pos += header.filename_len
|
|
176
|
+
|
|
177
|
+
extra_fields = buffer[pos:pos + header.extra_len]
|
|
178
|
+
pos += header.extra_len
|
|
179
|
+
|
|
180
|
+
header.filename = filename
|
|
181
|
+
header.raw_extra_fields = extra_fields
|
|
182
|
+
|
|
183
|
+
header.fix_by_zip64()
|
|
184
|
+
return header, buffer[pos:] # 返回剩余的数据部分
|
|
185
|
+
|
|
186
|
+
def build_mapping(self):
|
|
187
|
+
m = self.file_mapping = {}
|
|
188
|
+
for entry in self.files:
|
|
189
|
+
m[entry.filename] = entry
|
|
190
|
+
|
|
191
|
+
def find_file_entry(self, filename):
|
|
192
|
+
if self.file_mapping:
|
|
193
|
+
return self.file_mapping[filename]
|
|
194
|
+
else:
|
|
195
|
+
for i in self.files:
|
|
196
|
+
if i.filename == filename:
|
|
197
|
+
return i
|
|
198
|
+
else:
|
|
199
|
+
raise FileNotFoundError(filename)
|
|
200
|
+
|
|
201
|
+
async def _stream_single_file(self, filename):
|
|
202
|
+
entry = self.find_file_entry(filename)
|
|
203
|
+
|
|
204
|
+
if entry.is_encrypted:
|
|
205
|
+
raise UnsupportedCryptoError()
|
|
206
|
+
|
|
207
|
+
if entry.has_data_descriptor:
|
|
208
|
+
raise UnsupportedDataDescriptorError()
|
|
209
|
+
|
|
210
|
+
if entry.algorithm not in {0x0000, 0x0008}:
|
|
211
|
+
raise UnsupportedAlgorithmError(entry.algorithm)
|
|
212
|
+
|
|
213
|
+
header, buffer = await self.fetch_single_file_header(entry=entry)
|
|
214
|
+
# print(header)
|
|
215
|
+
real_data_offset = entry.local_header_offset + header.data_offset
|
|
216
|
+
|
|
217
|
+
if entry.compressed_size < len(buffer):
|
|
218
|
+
raw_generator = single_chunk_wrapper(buffer[:entry.compressed_size])
|
|
219
|
+
else:
|
|
220
|
+
raw_generator = self._read_range_stream(real_data_offset, entry.compressed_size)
|
|
221
|
+
|
|
222
|
+
match entry.algorithm:
|
|
223
|
+
case 0x0000:
|
|
224
|
+
return raw_generator
|
|
225
|
+
case 0x0008:
|
|
226
|
+
return deflate_wrapper(raw_generator)
|
|
227
|
+
|
|
228
|
+
async def stream_single_file(self, filename):
|
|
229
|
+
gen = await self._stream_single_file(filename)
|
|
230
|
+
async for i in gen:
|
|
231
|
+
yield i
|
|
232
|
+
|
|
233
|
+
async def load_single_file(self, filename) -> bytearray:
|
|
234
|
+
buffer = bytearray()
|
|
235
|
+
async for i in self.stream_single_file(filename):
|
|
236
|
+
buffer.extend(i)
|
|
237
|
+
|
|
238
|
+
return buffer
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import zlib
|
|
2
|
+
from collections.abc import Buffer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
async def deflate_wrapper(gen):
|
|
6
|
+
decompresser = zlib.decompressobj(-15)
|
|
7
|
+
async for chunk in gen:
|
|
8
|
+
yield decompresser.decompress(chunk)
|
|
9
|
+
remaining = decompresser.flush()
|
|
10
|
+
if remaining:
|
|
11
|
+
yield remaining
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def single_chunk_wrapper(data: Buffer):
|
|
15
|
+
yield data
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/rtzip/__init__.py
|
|
3
|
+
src/rtzip/errors.py
|
|
4
|
+
src/rtzip/models.py
|
|
5
|
+
src/rtzip/remotezip.py
|
|
6
|
+
src/rtzip/utils.py
|
|
7
|
+
src/rtzip.egg-info/PKG-INFO
|
|
8
|
+
src/rtzip.egg-info/SOURCES.txt
|
|
9
|
+
src/rtzip.egg-info/dependency_links.txt
|
|
10
|
+
src/rtzip.egg-info/requires.txt
|
|
11
|
+
src/rtzip.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
obstruct==0.5.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rtzip
|