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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1 @@
1
+ pith_zip