rustgzip 0.5.2__py3-none-win_amd64.whl

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.
Files changed (48) hide show
  1. rustgzip/__init__.py +385 -0
  2. rustgzip/experimental.py +283 -0
  3. rustgzip/rustgzip_native.dll +0 -0
  4. rustgzip-0.5.2.dist-info/METADATA +351 -0
  5. rustgzip-0.5.2.dist-info/RECORD +48 -0
  6. rustgzip-0.5.2.dist-info/WHEEL +4 -0
  7. rustgzip-0.5.2.dist-info/licenses/LICENSE +21 -0
  8. rustgzip-0.5.2.dist-info/licenses/OPTIMIZATION-030.md +82 -0
  9. rustgzip-0.5.2.dist-info/licenses/OPTIMIZATION-050.md +119 -0
  10. rustgzip-0.5.2.dist-info/licenses/PARALLEL-040.md +146 -0
  11. rustgzip-0.5.2.dist-info/licenses/PROVENANCE.md +43 -0
  12. rustgzip-0.5.2.dist-info/licenses/README.md +343 -0
  13. rustgzip-0.5.2.dist-info/licenses/REVIEW-031.md +57 -0
  14. rustgzip-0.5.2.dist-info/licenses/REVIEW-051.md +90 -0
  15. rustgzip-0.5.2.dist-info/licenses/UPSTREAM-REVIEW.md +96 -0
  16. rustgzip-0.5.2.dist-info/licenses/third-party/cfg-if-LICENSE-APACHE +201 -0
  17. rustgzip-0.5.2.dist-info/licenses/third-party/cfg-if-LICENSE-MIT +25 -0
  18. rustgzip-0.5.2.dist-info/licenses/third-party/crc32fast-LICENSE-APACHE +202 -0
  19. rustgzip-0.5.2.dist-info/licenses/third-party/crc32fast-LICENSE-MIT +21 -0
  20. rustgzip-0.5.2.dist-info/licenses/third-party/flate2-LICENSE-APACHE +201 -0
  21. rustgzip-0.5.2.dist-info/licenses/third-party/flate2-LICENSE-MIT +25 -0
  22. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/COPYRIGHT-library.html +29357 -0
  23. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/Apache-2.0.txt +73 -0
  24. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/BSD-2-Clause.txt +9 -0
  25. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/CC-BY-SA-4.0.txt +427 -0
  26. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/GCC-exception-3.1.txt +30 -0
  27. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/GPL-2.0-only.txt +133 -0
  28. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/GPL-3.0-or-later.txt +202 -0
  29. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/ISC.txt +7 -0
  30. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/LLVM-exception.txt +15 -0
  31. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/MIT.txt +9 -0
  32. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/NCSA.txt +29 -0
  33. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/OFL-1.1.txt +43 -0
  34. rustgzip-0.5.2.dist-info/licenses/third-party/rust-linux/licenses/Unicode-3.0.txt +39 -0
  35. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-COPYRIGHT-library.html +29357 -0
  36. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/Apache-2.0.txt +73 -0
  37. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/BSD-2-Clause.txt +9 -0
  38. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/CC-BY-SA-4.0.txt +427 -0
  39. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/GCC-exception-3.1.txt +30 -0
  40. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/GPL-2.0-only.txt +133 -0
  41. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/GPL-3.0-or-later.txt +202 -0
  42. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/ISC.txt +7 -0
  43. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/LLVM-exception.txt +15 -0
  44. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/MIT.txt +9 -0
  45. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/NCSA.txt +29 -0
  46. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/OFL-1.1.txt +43 -0
  47. rustgzip-0.5.2.dist-info/licenses/third-party/rust-windows-licenses/Unicode-3.0.txt +39 -0
  48. rustgzip-0.5.2.dist-info/licenses/third-party/zlib-rs-LICENSE +19 -0
rustgzip/__init__.py ADDED
@@ -0,0 +1,385 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Rust 加速的 gzip 文件接口;Python 3.4+,运行时只依赖标准库。
3
+
4
+ 压缩和 CRC 校验全部由包内动态库完成。文件接口使用固定大小缓冲,
5
+ 不会因为读取一个大 gzip 文件就自动把整个文件载入内存。
6
+ """
7
+ from __future__ import absolute_import
8
+
9
+ import ctypes
10
+ import codecs
11
+ import io
12
+ import operator
13
+ import os
14
+ import sys
15
+ import threading
16
+
17
+ __version__ = '0.5.2'
18
+ __all__ = ['open', 'compress', 'decompress', 'decompress_into', 'verify', 'GzipFile', 'backend_info']
19
+ _BUFFER = 256 * 1024
20
+ _LIB = None
21
+ _LOAD_LOCK = threading.Lock()
22
+
23
+
24
+ def _library():
25
+ """只加载随包交付的库,不从工作目录或 PATH 猜测同名动态库。"""
26
+ global _LIB
27
+ # 加载成功后的热路径不再竞争全局锁;首次加载仍在锁内二次检查。
28
+ if _LIB is not None:
29
+ return _LIB
30
+ with _LOAD_LOCK:
31
+ if _LIB is not None:
32
+ return _LIB
33
+ filename = ('rustgzip_native.dll' if sys.platform == 'win32'
34
+ else 'librustgzip_native.so')
35
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
36
+ try:
37
+ lib = ctypes.CDLL(path)
38
+ except OSError as exc:
39
+ raise ImportError('无法加载 Rust gzip 动态库;请安装匹配系统架构的完整包:%s' % exc)
40
+ # 先验证 ABI 再绑定新增函数。混用旧动态库时给出明确提示,而不是找符号失败。
41
+ try:
42
+ lib.rg_abi_version.restype = ctypes.c_uint32
43
+ lib.rg_abi_version.argtypes = []
44
+ if lib.rg_abi_version() != 4:
45
+ raise ImportError('Rust gzip 动态库接口版本不匹配,请重新安装完整包')
46
+ except AttributeError:
47
+ raise ImportError('Rust gzip 动态库缺少版本接口,请重新安装完整包')
48
+ size = ctypes.c_size_t
49
+ ptr = ctypes.c_void_p
50
+ error = [ptr, size]
51
+ signatures = {
52
+ 'rg_abi_version': (ctypes.c_uint32, []),
53
+ 'rg_open': (ctypes.c_int, [ptr, size, ctypes.c_int, ctypes.c_uint32,
54
+ ctypes.POINTER(ctypes.c_uint64)] + error),
55
+ 'rg_read': (ctypes.c_int64, [ctypes.c_uint64, ptr, size] + error),
56
+ 'rg_write': (ctypes.c_int64, [ctypes.c_uint64, ptr, size] + error),
57
+ 'rg_flush': (ctypes.c_int, [ctypes.c_uint64] + error),
58
+ 'rg_verify': (ctypes.c_int64, [ctypes.c_uint64, size] + error),
59
+ 'rg_close': (ctypes.c_int, [ctypes.c_uint64] + error),
60
+ 'rg_transform': (ctypes.c_int, [ptr, size, ctypes.c_int, ctypes.c_uint32,
61
+ size, ctypes.POINTER(ptr), ctypes.POINTER(size), ctypes.POINTER(size)] + error),
62
+ 'rg_free': (None, [ptr, size, size]),
63
+ 'rg_decompress_into': (ctypes.c_int64, [ptr, size, ptr, size] + error),
64
+ }
65
+ for name, signature in signatures.items():
66
+ try:
67
+ function = getattr(lib, name)
68
+ except AttributeError:
69
+ raise ImportError('Rust gzip 动态库缺少接口 %s,请重新安装完整包' % name)
70
+ function.restype, function.argtypes = signature
71
+ _LIB = lib
72
+ return lib
73
+
74
+
75
+ def _call(name, *args):
76
+ """每次调用独立错误缓冲;CDLL 在原生计算期间会释放 Python GIL。"""
77
+ err = ctypes.create_string_buffer(2048)
78
+ try:
79
+ result = getattr(_library(), name)(*(args + (err, len(err))))
80
+ finally:
81
+ # 异常的 traceback 可能长期保留本帧。不要让参数元组继续持有 ctypes
82
+ # from_buffer 导出的视图,否则用户在 except 中扩容 bytearray 会失败。
83
+ args = ()
84
+ if result == -1:
85
+ detail = err.value.decode('utf-8', 'replace')
86
+ raise OSError('gzip 操作失败:%s' % detail)
87
+ return result
88
+
89
+
90
+ def _level(value):
91
+ value = operator.index(value)
92
+ if not 0 <= value <= 9:
93
+ raise ValueError('compresslevel 必须是 0 到 9 的整数')
94
+ return value
95
+
96
+
97
+ def _bytes(data):
98
+ """接受缓冲协议,拒绝整数和字符串,避免 bytes(整数) 意外分配大内存。"""
99
+ # bytes 子类可以覆盖 __len__;跨 FFI 传长度前必须规范成真正的 bytes,
100
+ # 否则长度和底层分配空间可能不一致。普通 bytes 仍走不复制的快路径。
101
+ if type(data) is bytes:
102
+ return data
103
+ return memoryview(data).tobytes()
104
+
105
+
106
+ class _RawGzip(io.RawIOBase):
107
+ """一个 Rust 文件流。锁保护同一对象,独立文件流可以并发工作。
108
+
109
+ handle 是编号,不是指针;所有退出路径都通过 rg_close 释放 Rust 资源。
110
+ 不支持 seek、fileobj 或 fileno;避免把压缩文件偏移误当成解压后偏移。
111
+ """
112
+ def __init__(self, filename, mode, compresslevel):
113
+ super(_RawGzip, self).__init__()
114
+ self._lock = threading.RLock()
115
+ self._handle = 0
116
+ self._mode = mode
117
+ self.name = filename
118
+ self._position = 0
119
+ # Python 3.4 没有 os.fspath;直接支持 str/bytes,并兼容较新版 Path。
120
+ if not isinstance(filename, (str, bytes)):
121
+ if not hasattr(filename, '__fspath__'):
122
+ raise TypeError('filename 必须是路径 str/bytes/Path,不支持 fileobj')
123
+ filename = filename.__fspath__()
124
+ if not isinstance(filename, (str, bytes)):
125
+ raise TypeError('__fspath__ 必须返回 str 或 bytes')
126
+ if isinstance(filename, str):
127
+ path = filename.encode('utf-8') if sys.platform == 'win32' else os.fsencode(filename)
128
+ elif sys.platform == 'win32':
129
+ path = os.fsdecode(filename).encode('utf-8')
130
+ else:
131
+ path = filename
132
+ path = _bytes(path)
133
+ if b'\0' in path:
134
+ raise ValueError('文件路径不能包含 NUL 字符')
135
+ allocated = ctypes.c_uint64()
136
+ try:
137
+ _call('rg_open', path, len(path), 'rwax'.index(mode), _level(compresslevel),
138
+ ctypes.byref(allocated))
139
+ self._handle = allocated.value
140
+ except BaseException:
141
+ # 包括 KeyboardInterrupt。输出参数在原生函数返回前已经写好,
142
+ # 因此不必等析构/垃圾回收,也能关闭“刚打开就被中断”的文件。
143
+ self._handle = 0
144
+ if allocated.value:
145
+ try:
146
+ _call('rg_close', allocated.value)
147
+ except Exception:
148
+ pass # 保留引发清理的原始异常;句柄已由原生 close 移除。
149
+ raise
150
+
151
+ @property
152
+ def mode(self):
153
+ return self._mode + 'b'
154
+
155
+ def readable(self):
156
+ self._checkClosed()
157
+ return self._mode == 'r'
158
+
159
+ def writable(self):
160
+ self._checkClosed()
161
+ return self._mode != 'r'
162
+
163
+ def tell(self):
164
+ self._checkClosed()
165
+ return self._position
166
+
167
+ def readinto(self, buffer):
168
+ with self._lock:
169
+ self._checkClosed()
170
+ if not self.readable():
171
+ raise io.UnsupportedOperation('当前文件不可读')
172
+ view = memoryview(buffer)
173
+ byte_view, native = None, None
174
+ try:
175
+ byte_view = view.cast('B')
176
+ if not len(byte_view):
177
+ return 0
178
+ # 直接向调用方提供的可写缓冲填充结果,不创建完整返回副本。
179
+ native = (ctypes.c_ubyte * len(byte_view)).from_buffer(byte_view)
180
+ count = _call('rg_read', self._handle, native, len(byte_view))
181
+ self._position += count
182
+ return count
183
+ finally:
184
+ native = None # 先释放 ctypes 自己导出的视图,再释放外层 memoryview。
185
+ if byte_view is not None:
186
+ byte_view.release()
187
+ view.release()
188
+
189
+ def readall(self):
190
+ # RawIOBase 默认小块循环会增加 FFI 次数,这里按 1 MiB 读取。
191
+ parts = []
192
+ while True:
193
+ part = self.read(1024 * 1024)
194
+ if not part:
195
+ return b''.join(parts)
196
+ parts.append(part)
197
+
198
+ def write(self, data):
199
+ with self._lock:
200
+ self._checkClosed()
201
+ if not self.writable():
202
+ raise io.UnsupportedOperation('当前文件不可写')
203
+ data = _bytes(data)
204
+ count = _call('rg_write', self._handle, data, len(data))
205
+ self._position += count
206
+ return count
207
+
208
+ def flush(self):
209
+ with self._lock:
210
+ self._checkClosed()
211
+ # 读流没有待写数据。尤其不能让 close 中的 flush 覆盖原始 CRC 错误。
212
+ if self._handle and self.writable():
213
+ _call('rg_flush', self._handle)
214
+
215
+ def close(self):
216
+ # IOBase.close 会先 flush。无论 flush 是否失败,finally 都要关闭原生句柄。
217
+ lock = getattr(self, '_lock', None)
218
+ if lock is None:
219
+ return
220
+ with lock:
221
+ if self.closed and not self._handle:
222
+ return
223
+ try:
224
+ if not self.closed:
225
+ super(_RawGzip, self).close()
226
+ finally:
227
+ handle = self._handle
228
+ if handle:
229
+ try:
230
+ _call('rg_close', handle)
231
+ except BaseException:
232
+ # Ctrl+C 可能在 Python 已标记 closed、但尚未进入原生
233
+ # close 时到达。补做一次清理;若原生端早已移除编号,
234
+ # 重复关闭的“无效编号”会被忽略,保留第一次异常。
235
+ # 编号单调递增而不复用,所以不会误关闭另一个文件。
236
+ try:
237
+ _call('rg_close', handle)
238
+ except BaseException:
239
+ pass
240
+ raise
241
+ finally:
242
+ self._handle = 0
243
+
244
+
245
+ class _BufferedGzipWriter(io.BufferedWriter):
246
+ """把公开 flush 一直传递到 gzip 编码器和底层文件缓冲。
247
+
248
+ 标准 BufferedWriter 只保证把自身数据交给 raw.write;本包 raw 层还包含
249
+ Rust 压缩器和文件缓冲,所以这里需要显式继续 flush,不代表 fsync。
250
+ """
251
+ def flush(self):
252
+ super(_BufferedGzipWriter, self).flush()
253
+ self.raw.flush()
254
+
255
+
256
+ def open(filename, mode='rb', compresslevel=6, encoding=None, errors=None,
257
+ newline=None, buffering=_BUFFER):
258
+ """打开路径形式的 gzip 文件,支持 rb/wb/ab/xb 及 rt/wt/at/xt。
259
+
260
+ 返回标准 BufferedReader / BufferedWriter / TextIOWrapper,可迭代、readinto、
261
+ read、readline、write 和 with 自动关闭。读到 EOF 时才完成各 member 的 CRC 校验。
262
+ 默认压缩级别 6;只支持顺序流,不宣称完整替代标准库 gzip 的所有扩展行为。
263
+ """
264
+ if not isinstance(mode, str) or mode not in (
265
+ 'r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', 'at', 'x', 'xb', 'xt'):
266
+ raise ValueError('mode 必须为 rb/wb/ab/xb 或 rt/wt/at/xt')
267
+ binary = 't' not in mode
268
+ if binary and any(v is not None for v in (encoding, errors, newline)):
269
+ raise ValueError('二进制模式不能指定 encoding/errors/newline')
270
+ buffering = operator.index(buffering)
271
+ if buffering <= 0:
272
+ raise ValueError('buffering 必须大于 0')
273
+ if buffering > sys.maxsize:
274
+ raise OverflowError('buffering 超出当前 Python 可表示的缓冲长度')
275
+ if not binary:
276
+ # 在任何可能创建/截断文件的操作之前校验文本参数。错误编码或 newline
277
+ # 不应让一个尚未成功打开的调用破坏原文件。BytesIO 探针不访问磁盘。
278
+ probe = io.TextIOWrapper(io.BytesIO(), encoding=encoding, errors=errors, newline=newline)
279
+ try:
280
+ codecs.lookup_error(probe.errors)
281
+ finally:
282
+ probe.close()
283
+ raw = _RawGzip(filename, mode[0], compresslevel)
284
+ stream = None
285
+ try:
286
+ stream = (io.BufferedReader(raw, buffering) if mode[0] == 'r'
287
+ else _BufferedGzipWriter(raw, buffering))
288
+ if binary:
289
+ return stream
290
+ return io.TextIOWrapper(stream, encoding=encoding, errors=errors, newline=newline)
291
+ except BaseException:
292
+ (stream or raw).close()
293
+ raise
294
+
295
+
296
+ def GzipFile(filename=None, mode='rb', compresslevel=6, **kwargs):
297
+ """常用路径式 GzipFile 入口;不接受 fileobj/mtime 参数,也不是标准库同名类。"""
298
+ return open(filename, mode, compresslevel, **kwargs)
299
+
300
+
301
+ def _transform(data, decode, level, limit):
302
+ data = _bytes(data)
303
+ output = ctypes.c_void_p()
304
+ length = ctypes.c_size_t()
305
+ capacity = ctypes.c_size_t()
306
+ lib = _library()
307
+ try:
308
+ # 必须连原生调用本身也放入 try:C 已分配返回缓冲之后,Python 可能
309
+ # 在下一条字节码前处理 Ctrl+C,此时也要使用输出参数执行配对释放。
310
+ _call('rg_transform', data, len(data), decode, _level(level), limit,
311
+ ctypes.byref(output), ctypes.byref(length), ctypes.byref(capacity))
312
+ return ctypes.string_at(output, length.value)
313
+ finally:
314
+ # string_at 返回 Python 自有副本,原生分配随后释放,不能交给 Python 的 free。
315
+ if output.value:
316
+ lib.rg_free(output, length.value, capacity.value)
317
+
318
+
319
+ def compress(data, compresslevel=6):
320
+ """将 bytes-like 压缩为标准 gzip,固定时间戳 0,不嵌入文件名。"""
321
+ return _transform(data, 0, compresslevel, 0)
322
+
323
+
324
+ def decompress(data, max_output_size=512 * 1024 * 1024):
325
+ """内存解压,支持串接 member;默认最多输出 512 MiB,可显式调整。
326
+
327
+ 超过上限或 CRC/长度/格式错误直接抛出 OSError,不返回部分成功结果。
328
+ 大文件建议使用 open 分块读取,以保持内存稳定。
329
+ """
330
+ limit = operator.index(max_output_size)
331
+ if not 0 <= limit < sys.maxsize:
332
+ raise ValueError('max_output_size 必须在 0 到 sys.maxsize-1 之间')
333
+ return _transform(data, 1, 6, limit)
334
+
335
+
336
+ def decompress_into(data, output):
337
+ """完整解压到调用方已有的可写连续缓冲,返回有效字节数。
338
+
339
+ output 可以是 bytearray 或可写的连续 memoryview;容量就是输出上限。
340
+ 不创建完整的 Rust 输出 Vec,也不创建一个额外的 Python bytes 副本。
341
+ 只在成功验证所有 member 的 CRC/长度及 EOF 后返回。失败时 output
342
+ 可能已被部分改写,必须丢弃其内容。调用期间其他线程不可访问此缓冲。
343
+ """
344
+ data = _bytes(data)
345
+ view = memoryview(output)
346
+ byte_view, native = None, None
347
+ try:
348
+ if view.readonly:
349
+ raise TypeError('output 必须是可写缓冲,例如 bytearray')
350
+ if not view.c_contiguous:
351
+ raise ValueError('output 必须是 C 连续缓冲,不能使用跨步切片')
352
+ byte_view = view.cast('B')
353
+ if not len(byte_view):
354
+ return _call('rg_decompress_into', data, len(data), None, 0)
355
+ native = (ctypes.c_ubyte * len(byte_view)).from_buffer(byte_view)
356
+ return _call('rg_decompress_into', data, len(data), native, len(byte_view))
357
+ finally:
358
+ native = None
359
+ if byte_view is not None:
360
+ byte_view.release()
361
+ view.release()
362
+
363
+
364
+ def verify(filename, chunk_size=1024 * 1024):
365
+ """完整读取并校验 gzip,返回解压后的字节总数,不写出解压文件。
366
+
367
+ 循环和复用缓冲都在 Rust 内部,适合“逐块读取直到末尾”的完整性检查场景。
368
+ chunk_size 可选 1 字节到 64 MiB;校验函数不保存解压后的内容。
369
+ """
370
+ chunk_size = operator.index(chunk_size)
371
+ if not 1 <= chunk_size <= 64 * 1024 * 1024:
372
+ raise ValueError('chunk_size 必须在 1 字节到 64 MiB 之间')
373
+ with _RawGzip(filename, 'r', 6) as raw:
374
+ return _call('rg_verify', raw._handle, chunk_size)
375
+
376
+
377
+ def backend_info():
378
+ """加载后端并返回诊断信息;不会上传路径、数据或环境信息。"""
379
+ return {'package_version': __version__, 'abi_version': _library().rg_abi_version(),
380
+ 'backend': 'flate2 1.1.10 / zlib-rs', 'python': sys.version.split()[0],
381
+ 'memory_decoder': 'zlib-rs direct for simple headers; flate2 fallback',
382
+ 'member_state_reuse': True,
383
+ 'experimental_parallel_compression': hasattr(_library(), 'rg_compress_parallel'),
384
+ 'experimental_parallel_compression_into': hasattr(_library(), 'rg_compress_parallel_into'),
385
+ 'parallel_single_member_decode': False}
@@ -0,0 +1,283 @@
1
+ # -*- coding: utf-8 -*-
2
+ """显式启用的并行实验,兼容 Python 3.4;不改变主模块默认行为。
3
+
4
+ compress_parallel 把压缩分到 Rust 工作线程,生成一个普通 gzip member。
5
+ prefetch 只有一个解码线程,作用是把解压与调用方处理重叠,不是并行解码。
6
+ """
7
+ from __future__ import absolute_import
8
+ import ctypes
9
+ import operator
10
+ import queue
11
+ import threading
12
+ from . import _call, _level, _library, open as gzip_open
13
+
14
+ _BIND_LOCK = threading.Lock()
15
+ _MAX_INPUT_SIZE = 512 * 1024 * 1024
16
+
17
+
18
+ def _bounded_bytes(data):
19
+ """先按真实缓冲大小拒绝超限输入,避免先复制数百 MiB 才报错。
20
+
21
+ 不调用对象可覆盖的 __len__;bytes 子类和跨步视图也按实际 nbytes 校验。
22
+ 原始 bytes 保留不复制路径,其它缓冲在持有视图期间复制,之后及时释放视图。
23
+ """
24
+ if type(data) is bytes:
25
+ if len(data) > _MAX_INPUT_SIZE:
26
+ raise ValueError('实验接口输入不能超过 512 MiB;大文件请继续使用 open')
27
+ return data
28
+ view = memoryview(data)
29
+ try:
30
+ if view.nbytes > _MAX_INPUT_SIZE:
31
+ raise ValueError('实验接口输入不能超过 512 MiB;大文件请继续使用 open')
32
+ return view.tobytes()
33
+ finally:
34
+ view.release()
35
+
36
+
37
+ def _options(data, compresslevel, workers, chunk_size, use_dictionary):
38
+ """两个压缩入口共用校验,避免 into 接口遗漏输入大小或参数限制。"""
39
+ workers, chunk_size = operator.index(workers), operator.index(chunk_size)
40
+ if not 1 <= workers <= 32:
41
+ raise ValueError('workers 必须在 1 到 32 之间')
42
+ if not 32768 <= chunk_size <= 16 * 1024 * 1024:
43
+ raise ValueError('chunk_size 必须在 32 KiB 到 16 MiB 之间')
44
+ if type(use_dictionary) is not bool:
45
+ raise TypeError('use_dictionary 必须是 bool')
46
+ level = _level(compresslevel)
47
+ data = _bounded_bytes(data)
48
+ return data, level, workers, chunk_size
49
+
50
+
51
+ def compress_parallel(data, compresslevel=6, workers=2, chunk_size=1024 * 1024,
52
+ use_dictionary=True):
53
+ """内存并行压缩;返回标准单 member gzip,不要求特殊解码器或外部字典。
54
+
55
+ workers 范围 1..32,chunk_size 范围 32 KiB..16 MiB,输入至多 512 MiB。
56
+ 默认后块预置前 32 KiB 原文。禁用字典只用于对照,不保证压缩率不变。
57
+ 整体输入和结果仍在内存;不是大文件流式写入接口。异常不返回部分结果。
58
+ """
59
+ data, level, workers, chunk_size = _options(data, compresslevel, workers, chunk_size, use_dictionary)
60
+ lib = _library()
61
+ # 这是对 ABI 4 的可选扩展,不更改旧函数。旧 0.3.1 动态库仍可供
62
+ # 主 API 使用;使用本实验接口时缺少新符号,则明确要求安装完整新包。
63
+ with _BIND_LOCK:
64
+ try:
65
+ function = lib.rg_compress_parallel
66
+ except AttributeError:
67
+ raise ImportError('当前动态库不含并行实验,请安装完整 0.4.0 包并重启 Python')
68
+ function.restype = ctypes.c_int
69
+ ptr, size = ctypes.c_void_p, ctypes.c_size_t
70
+ function.argtypes = [ptr, size, ctypes.c_uint32, size, size, ctypes.c_int,
71
+ ctypes.POINTER(ptr), ctypes.POINTER(size),
72
+ ctypes.POINTER(size), ptr, size]
73
+ output, length, capacity = ctypes.c_void_p(), ctypes.c_size_t(), ctypes.c_size_t()
74
+ try:
75
+ _call('rg_compress_parallel', data, len(data), level, workers, chunk_size,
76
+ int(use_dictionary), ctypes.byref(output), ctypes.byref(length),
77
+ ctypes.byref(capacity))
78
+ return ctypes.string_at(output, length.value)
79
+ finally:
80
+ if output.value:
81
+ lib.rg_free(output, length.value, capacity.value)
82
+
83
+
84
+ def compress_parallel_into(data, output, compresslevel=6, workers=2,
85
+ chunk_size=1024 * 1024, use_dictionary=True):
86
+ """直接压缩到已有可写连续缓冲,返回有效字节数,减少完整结果的分配与复制。
87
+
88
+ 参数限制与 compress_parallel 相同。output 的容量由调用方预留;不足时
89
+ 抛出 OSError 并说明实际需要大小,可扩容后重试,但本次压缩计算不能复用。
90
+ 只使用成功返回的 [:count];尾部不改写。失败/中断后不把缓冲视为有效结果。
91
+ 调用期间其他线程不得读写/扩容 output;持有自己的 memoryview 也会限制扩容。
92
+ """
93
+ data, level, workers, chunk_size = _options(data, compresslevel, workers, chunk_size, use_dictionary)
94
+ lib = _library()
95
+ with _BIND_LOCK:
96
+ try:
97
+ function = lib.rg_compress_parallel_into
98
+ except AttributeError:
99
+ raise ImportError('当前动态库不含压缩 into 接口,请安装完整 0.5.0 包并重启 Python')
100
+ function.restype = ctypes.c_int64
101
+ ptr, size = ctypes.c_void_p, ctypes.c_size_t
102
+ function.argtypes = [ptr, size, ctypes.c_uint32, size, size, ctypes.c_int,
103
+ ptr, size, ptr, size]
104
+ view = memoryview(output)
105
+ byte_view, native = None, None
106
+ try:
107
+ if view.readonly:
108
+ raise TypeError('output 必须是可写缓冲,例如 bytearray')
109
+ if not view.c_contiguous:
110
+ raise ValueError('output 必须是 C 连续缓冲')
111
+ byte_view = view.cast('B')
112
+ if len(byte_view):
113
+ native = (ctypes.c_ubyte * len(byte_view)).from_buffer(byte_view)
114
+ return _call('rg_compress_parallel_into', data, len(data), level, workers,
115
+ chunk_size, int(use_dictionary), native, len(byte_view))
116
+ finally:
117
+ # 包括异常 traceback 仍被用户保留的情况,必须解除本接口持有的导出。
118
+ native = None
119
+ if byte_view is not None:
120
+ byte_view.release()
121
+ view.release()
122
+
123
+
124
+ class _PrefetchedChunks(object):
125
+ """只在 with 期间拥有工作线程与文件;提前退出也会取消并 join。
126
+
127
+ 队列最多保存 queue_depth 个不可变 bytes;另外允许生产/消费各持有一块。
128
+ 不在调用线程关闭仍被工作线程读取的 raw,避免 close/read 锁竞争。
129
+ close 会等待当前原生 read 返回,不能强行取消阻塞的网络文件系统 I/O。
130
+ 同一个对象仅供一个消费线程使用,不支持重复进入或跨线程 close。
131
+ """
132
+ def __init__(self, filename, chunk_size, queue_depth):
133
+ self.filename = filename
134
+ self.chunk_size = operator.index(chunk_size)
135
+ self.queue_depth = operator.index(queue_depth)
136
+ if not 1 <= self.chunk_size <= 16 * 1024 * 1024:
137
+ raise ValueError('chunk_size 必须在 1 字节到 16 MiB 之间')
138
+ if not 1 <= self.queue_depth <= 16:
139
+ raise ValueError('queue_depth 必须在 1 到 16 之间')
140
+ self._queue = queue.Queue(self.queue_depth)
141
+ self._stop = threading.Event()
142
+ # 不单凭 Thread.is_alive / join 的状态认定资源已释放;读取线程在
143
+ # finally 中关闭文件后才置位,主线程被 Ctrl+C 中断也不能越过此屏障。
144
+ self._done = threading.Event()
145
+ self._thread = None
146
+ self._entered = False
147
+ self._closed = False
148
+ self._ended = False
149
+ self._owner = None
150
+
151
+ def _send(self, item):
152
+ # put 不能永久阻塞,否则调用方提前 break 时 close.join 会死锁。
153
+ while not self._stop.is_set():
154
+ try:
155
+ self._queue.put(item, timeout=0.05)
156
+ return
157
+ except queue.Full:
158
+ pass
159
+
160
+ def _produce(self):
161
+ try:
162
+ if self._stop.is_set():
163
+ return # start 中断后才获得调度的线程,不再打开文件。
164
+ with gzip_open(self.filename, 'rb') as source:
165
+ while not self._stop.is_set():
166
+ block = source.read(self.chunk_size)
167
+ if not block:
168
+ break
169
+ self._send((block, None))
170
+ self._send((None, None))
171
+ except BaseException as exc:
172
+ # 不跨线程保留 traceback 的 source/大缓冲;下一次 next 原样报错。
173
+ if isinstance(exc, StopIteration):
174
+ # 迭代器协议会把 StopIteration 当成正常 EOF。路径/读取钩子
175
+ # 的意外 StopIteration 必须转换,不能让损坏的读取流程假成功。
176
+ exc = RuntimeError('预取读取意外抛出 StopIteration:%s' % exc)
177
+ self._send((None, exc.with_traceback(None)))
178
+ finally:
179
+ self._done.set()
180
+
181
+ def __enter__(self):
182
+ if self._entered or self._closed:
183
+ raise RuntimeError('prefetch 对象只能在一个 with 中使用一次')
184
+ self._entered = True
185
+ self._owner = threading.current_thread().ident
186
+ try:
187
+ self._thread = threading.Thread(target=self._produce, name='rustgzip-prefetch')
188
+ self._thread.daemon = True
189
+ self._thread.start()
190
+ except BaseException:
191
+ # start 可能已经启动线程,随后才收到 Ctrl+C;按 ident 判断是否 join。
192
+ self._stop.set()
193
+ if self._thread is not None and self._thread.ident is not None:
194
+ self.close()
195
+ else:
196
+ self._closed = True
197
+ raise
198
+ return self
199
+
200
+ def _check_owner(self):
201
+ if not self._entered:
202
+ raise RuntimeError('请使用 with prefetch(...) as chunks')
203
+ if threading.current_thread().ident != self._owner:
204
+ raise RuntimeError('prefetch 只能由进入 with 的线程消费和关闭')
205
+
206
+ def __iter__(self):
207
+ self._check_owner()
208
+ return self
209
+
210
+ def __next__(self):
211
+ self._check_owner()
212
+ if self._closed or self._ended:
213
+ raise StopIteration
214
+ while True:
215
+ try:
216
+ block, error = self._queue.get(timeout=0.05)
217
+ break
218
+ except queue.Empty:
219
+ if self._done.is_set():
220
+ # Queue.get 超时与 is_alive 之间仍可能刚好入队并结束。
221
+ # 再检查一次队列,避免把已经到达的 EOF/错误误判为线程丢失。
222
+ try:
223
+ block, error = self._queue.get_nowait()
224
+ break
225
+ except queue.Empty:
226
+ raise RuntimeError('预取工作线程意外终止,不能视为正常 EOF')
227
+ if block is None:
228
+ self._ended = True
229
+ if error is not None:
230
+ raise error
231
+ raise StopIteration
232
+ return block
233
+
234
+ def close(self):
235
+ if not self._entered:
236
+ self._closed = True
237
+ return
238
+ self._check_owner()
239
+ if self._closed:
240
+ return
241
+ self._stop.set()
242
+ interrupted = None
243
+ if self._thread is not None and self._thread.ident is not None:
244
+ # Ctrl+C 先记住,等源文件退出 with 并释放队列后再向调用方抛出。
245
+ # Event.wait 不会因 join 的中断而误报“工作线程已经退出”。
246
+ while not self._done.is_set():
247
+ try:
248
+ self._done.wait(0.05)
249
+ except KeyboardInterrupt as exc:
250
+ if interrupted is None:
251
+ interrupted = exc.with_traceback(None)
252
+ while True:
253
+ try:
254
+ self._thread.join()
255
+ break
256
+ except KeyboardInterrupt as exc:
257
+ if interrupted is None:
258
+ interrupted = exc.with_traceback(None)
259
+ while True:
260
+ try:
261
+ self._queue.get_nowait()
262
+ except queue.Empty:
263
+ break
264
+ except KeyboardInterrupt as exc:
265
+ if interrupted is None:
266
+ interrupted = exc.with_traceback(None)
267
+ self._closed = True
268
+ if interrupted is not None:
269
+ raise interrupted
270
+
271
+ def __exit__(self, exc_type, exc, traceback):
272
+ self.close()
273
+ return False
274
+
275
+
276
+ def prefetch(filename, chunk_size=1024 * 1024, queue_depth=2):
277
+ """with prefetch(path) as chunks: 按顺序迭代解压块。
278
+
279
+ 读到迭代结束才完成 CRC/长度检查。提前 break 仅取消读取,不宣称已验证
280
+ 全文件;close 不自动读取尾部。队列等待释放 GIL,纯 Python CPU 循环的
281
+ 业务代码仍受 GIL 限制,实际收益必须用自己的数据和处理逻辑测量。
282
+ """
283
+ return _PrefetchedChunks(filename, chunk_size, queue_depth)
Binary file