pith-zip 0.1.1b1__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.
pith_zip/__init__.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 pith-hash
|
|
3
|
+
"""pith-zip SDK: ZIP archive reading through ctypes.
|
|
4
|
+
|
|
5
|
+
The single Rust core (the ``pith-zip`` cdylib built by
|
|
6
|
+
``cargo build --release``) is loaded at runtime; this package carries
|
|
7
|
+
no third-party dependency — ``ctypes`` is the standard library.
|
|
8
|
+
|
|
9
|
+
Discovery order (the suite's cdylib convention):
|
|
10
|
+
|
|
11
|
+
1. ``PITH_CDYLIB`` — an explicit cdylib *file* path;
|
|
12
|
+
2. ``PITH_CDYLIB_DIR`` — a *directory* scanned for the cdylib names
|
|
13
|
+
(the CD pipeline points this at ``target/release``);
|
|
14
|
+
3. the package directory itself (the built wheel ships the cdylib as
|
|
15
|
+
package data);
|
|
16
|
+
4. ``<repo root>/target/release`` — the repository working-tree layout,
|
|
17
|
+
so a source checkout runs against a local cargo build with no
|
|
18
|
+
configuration.
|
|
19
|
+
|
|
20
|
+
The FFI surface is one member-enumeration operation plus one free:
|
|
21
|
+
``pith_zip_members`` parses the archive, extracts and CRC-verifies
|
|
22
|
+
every member, and hands out the canonical member stream (member count,
|
|
23
|
+
then per member: name, method, data-descriptor flag, both sizes,
|
|
24
|
+
CRC-32, content length and the extracted content), and
|
|
25
|
+
``pith_zip_free`` releases the handed-out buffer.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import ctypes
|
|
31
|
+
import os
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Archive",
|
|
37
|
+
"Member",
|
|
38
|
+
"FfiError",
|
|
39
|
+
"LibraryNotFoundError",
|
|
40
|
+
"find_cdylib",
|
|
41
|
+
"members",
|
|
42
|
+
"parse_members",
|
|
43
|
+
"STATUS_OK",
|
|
44
|
+
"STATUS_INVALID",
|
|
45
|
+
"STATUS_REJECTED",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
#: Status: success.
|
|
49
|
+
STATUS_OK = 0
|
|
50
|
+
#: Status: a caller argument is invalid (a null pointer).
|
|
51
|
+
STATUS_INVALID = -1
|
|
52
|
+
#: Status: the core reader refused the input (malformed ZIP).
|
|
53
|
+
STATUS_REJECTED = -2
|
|
54
|
+
|
|
55
|
+
#: Every cdylib file name cargo may drop into the build directory, per
|
|
56
|
+
#: platform (windows / linux / macOS).
|
|
57
|
+
CDYLIB_NAMES = ("pith_zip.dll", "libpith_zip.so", "libpith_zip.dylib")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Member:
|
|
62
|
+
"""One archive member, re-expressed from the canonical stream.
|
|
63
|
+
|
|
64
|
+
``content`` is the extracted (and CRC-verified) member body —
|
|
65
|
+
exactly the bytes the per-member ``sha256`` digest in
|
|
66
|
+
``reference.json`` covers.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
#: The member name as recorded in the central directory.
|
|
70
|
+
name: str
|
|
71
|
+
#: The compression method: ``0`` (stored) or ``8`` (DEFLATE).
|
|
72
|
+
method: int
|
|
73
|
+
#: Whether the entry carries a data descriptor (flags bit 3).
|
|
74
|
+
data_descriptor: bool
|
|
75
|
+
#: Payload length on disk, in bytes.
|
|
76
|
+
compressed_size: int
|
|
77
|
+
#: Decompressed length, in bytes.
|
|
78
|
+
uncompressed_size: int
|
|
79
|
+
#: CRC-32 of the uncompressed payload.
|
|
80
|
+
crc32: int
|
|
81
|
+
#: The extracted member content.
|
|
82
|
+
content: bytes
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class Archive:
|
|
87
|
+
"""The whole archive, re-expressed from the canonical stream."""
|
|
88
|
+
|
|
89
|
+
#: The members, in central-directory order.
|
|
90
|
+
members: tuple[Member, ...]
|
|
91
|
+
#: The canonical byte stream the FFI handed out.
|
|
92
|
+
raw: bytes
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class LibraryNotFoundError(OSError):
|
|
96
|
+
"""No cdylib was found through the discovery chain."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class FfiError(Exception):
|
|
100
|
+
"""A non-zero status code came back from the cdylib."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, op: str, status: int) -> None:
|
|
103
|
+
kind = {
|
|
104
|
+
STATUS_INVALID: "invalid argument",
|
|
105
|
+
STATUS_REJECTED: "input rejected",
|
|
106
|
+
}.get(status, "unknown failure")
|
|
107
|
+
super().__init__(f"{op} failed: {kind} (status {status})")
|
|
108
|
+
#: The raw status code the FFI returned.
|
|
109
|
+
self.status = status
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def find_cdylib() -> Path:
|
|
113
|
+
"""Locates the cdylib through the suite's discovery chain."""
|
|
114
|
+
explicit = os.environ.get("PITH_CDYLIB")
|
|
115
|
+
if explicit:
|
|
116
|
+
p = Path(explicit)
|
|
117
|
+
if p.is_file():
|
|
118
|
+
return p
|
|
119
|
+
env_dir = os.environ.get("PITH_CDYLIB_DIR")
|
|
120
|
+
candidates: list[Path] = []
|
|
121
|
+
if env_dir:
|
|
122
|
+
env_dir_path = Path(env_dir)
|
|
123
|
+
candidates.append(env_dir_path)
|
|
124
|
+
if not env_dir_path.is_absolute():
|
|
125
|
+
# CD and local runs invoke tools from the repository root or
|
|
126
|
+
# from sdk/<lang>; resolve the env value against both.
|
|
127
|
+
candidates.append(Path.cwd() / env_dir_path)
|
|
128
|
+
candidates.append(Path(__file__).resolve().parents[3] / env_dir_path)
|
|
129
|
+
candidates.append(Path(__file__).resolve().parent) # packaged wheel
|
|
130
|
+
candidates.append(Path(__file__).resolve().parents[3] / "target" / "release")
|
|
131
|
+
for directory in candidates:
|
|
132
|
+
for name in CDYLIB_NAMES:
|
|
133
|
+
p = directory / name
|
|
134
|
+
if p.is_file():
|
|
135
|
+
return p
|
|
136
|
+
raise LibraryNotFoundError(
|
|
137
|
+
"no pith-zip cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, "
|
|
138
|
+
"the package directory and <repo>/target/release); "
|
|
139
|
+
"run `cargo build --release` first"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_lib: ctypes.CDLL | None = None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _load() -> ctypes.CDLL:
|
|
147
|
+
global _lib
|
|
148
|
+
if _lib is None:
|
|
149
|
+
lib = ctypes.CDLL(str(find_cdylib()))
|
|
150
|
+
lib.pith_zip_members.argtypes = [
|
|
151
|
+
ctypes.c_void_p, # data
|
|
152
|
+
ctypes.c_size_t, # len
|
|
153
|
+
ctypes.POINTER(ctypes.c_void_p), # out buffer
|
|
154
|
+
ctypes.POINTER(ctypes.c_size_t), # out length
|
|
155
|
+
]
|
|
156
|
+
lib.pith_zip_members.restype = ctypes.c_int32
|
|
157
|
+
lib.pith_zip_free.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
|
158
|
+
lib.pith_zip_free.restype = None
|
|
159
|
+
_lib = lib
|
|
160
|
+
return _lib
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def members(data: bytes) -> bytes:
|
|
164
|
+
"""Enumerates a complete ZIP archive into the canonical member
|
|
165
|
+
stream the ``reference.json`` vectors are defined over.
|
|
166
|
+
|
|
167
|
+
Every member is extracted and CRC-verified by the core before the
|
|
168
|
+
stream is handed out. Raises :class:`FfiError` with
|
|
169
|
+
``status == STATUS_REJECTED`` for any malformed input — no end of
|
|
170
|
+
central directory, a bad directory offset, an unsupported feature
|
|
171
|
+
or a CRC mismatch — and ``status == STATUS_INVALID`` for a null
|
|
172
|
+
argument; the core never panics through this boundary.
|
|
173
|
+
"""
|
|
174
|
+
out = ctypes.c_void_p()
|
|
175
|
+
out_len = ctypes.c_size_t()
|
|
176
|
+
status = _load().pith_zip_members(data, len(data), ctypes.byref(out), ctypes.byref(out_len))
|
|
177
|
+
if status != STATUS_OK:
|
|
178
|
+
raise FfiError("pith_zip_members", status)
|
|
179
|
+
try:
|
|
180
|
+
return ctypes.string_at(out, out_len.value)
|
|
181
|
+
finally:
|
|
182
|
+
_load().pith_zip_free(out, out_len.value)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _need(raw: bytes, pos: int, n: int) -> None:
|
|
186
|
+
if pos + n > len(raw):
|
|
187
|
+
raise ValueError(f"canonical stream is truncated at byte {pos} (need {n} more)")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _be(raw: bytes, pos: int, n: int) -> tuple[int, int]:
|
|
191
|
+
_need(raw, pos, n)
|
|
192
|
+
return int.from_bytes(raw[pos : pos + n], "big"), pos + n
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def parse_members(raw: bytes) -> Archive:
|
|
196
|
+
"""Re-expresses the canonical byte stream as an :class:`Archive`."""
|
|
197
|
+
count, pos = _be(raw, 0, 4)
|
|
198
|
+
parsed: list[Member] = []
|
|
199
|
+
for _ in range(count):
|
|
200
|
+
name_len, pos = _be(raw, pos, 4)
|
|
201
|
+
_need(raw, pos, name_len + 15)
|
|
202
|
+
name = raw[pos : pos + name_len].decode("utf-8")
|
|
203
|
+
pos += name_len
|
|
204
|
+
method, pos = _be(raw, pos, 2)
|
|
205
|
+
if raw[pos] not in (0, 1):
|
|
206
|
+
raise ValueError(f"unknown data-descriptor byte {raw[pos]}")
|
|
207
|
+
descriptor = raw[pos] == 1
|
|
208
|
+
pos += 1
|
|
209
|
+
compressed_size, pos = _be(raw, pos, 8)
|
|
210
|
+
uncompressed_size, pos = _be(raw, pos, 8)
|
|
211
|
+
crc32, pos = _be(raw, pos, 4)
|
|
212
|
+
content_len, pos = _be(raw, pos, 8)
|
|
213
|
+
_need(raw, pos, content_len)
|
|
214
|
+
content = raw[pos : pos + content_len]
|
|
215
|
+
pos += content_len
|
|
216
|
+
parsed.append(
|
|
217
|
+
Member(
|
|
218
|
+
name=name,
|
|
219
|
+
method=method,
|
|
220
|
+
data_descriptor=descriptor,
|
|
221
|
+
compressed_size=compressed_size,
|
|
222
|
+
uncompressed_size=uncompressed_size,
|
|
223
|
+
crc32=crc32,
|
|
224
|
+
content=content,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
if pos != len(raw):
|
|
228
|
+
raise ValueError(f"{len(raw) - pos} trailing bytes after the last member")
|
|
229
|
+
return Archive(members=tuple(parsed), raw=raw)
|
pith_zip/pith_zip.dll
ADDED
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pith-zip
|
|
3
|
+
Version: 0.1.1b1
|
|
4
|
+
Summary: ZIP archive reading SDK: stored and deflated members, data descriptors and CRC verification via the pith-zip Rust cdylib (ctypes, zero-dep)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/pith-hash/pith-zip
|
|
7
|
+
Project-URL: Homepage, https://github.com/pith-hash/pith-zip
|
|
8
|
+
Keywords: zip,archive,extractor,deflate,hashing
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: Topic :: System :: Archiving
|
|
12
|
+
Requires-Python: >=3.9
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pith_zip/__init__.py,sha256=WdyP06q-C2OaPWrFP6LlHX5GQ89bdbEEWk9TA0HR5nk,7866
|
|
2
|
+
pith_zip/pith_zip.dll,sha256=RZYvEWeNwXLt_lpxvI_VV4BDn1eUDz7heMlxJWLE0Dc,135168
|
|
3
|
+
pith_zip-0.1.1b1.dist-info/METADATA,sha256=Kdhx_xUGiE32pMjQJVmhjiwBlkpkUsT-uteWntvieb4,564
|
|
4
|
+
pith_zip-0.1.1b1.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
|
|
5
|
+
pith_zip-0.1.1b1.dist-info/top_level.txt,sha256=Y7daHUZvKfj_sooQDetU7OiFNoYPh8zfwS4S9nOEVzU,9
|
|
6
|
+
pith_zip-0.1.1b1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pith_zip
|