openrar 2.0.1__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.
openrar-2.0.1/PKG-INFO ADDED
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: openrar
3
+ Version: 2.0.1
4
+ Summary: OpenRAR Python SDK — ctypes binding over the frozen OpenRAR C ABI (RAR 5.0 read/create/mutate)
5
+ Author: OpenRAR Project
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/geometric-dev/openrar
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: Microsoft :: Windows
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Operating System :: MacOS
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+
15
+ # OpenRAR Python SDK
16
+
17
+ ctypes binding over the frozen OpenRAR C ABI (`docs/abi-freeze.md`) — no
18
+ compiled extension, the C ABI is the whole contract.
19
+
20
+ ```python
21
+ from openrar import Archive, create, package_version
22
+
23
+ create("backup.rar", ["data.bin"], ["data.bin"], method=3)
24
+ with Archive("backup.rar") as a:
25
+ for entry in a.list():
26
+ print(entry["path"], entry["size"])
27
+ a.extract_to_path(0, "data-out.bin")
28
+ assert a.test(0)
29
+ ```
30
+
31
+ ## Conformance
32
+
33
+ The normative cross-binding case list lives in `tests/conformance_test.py`
34
+ (freeze prereq 3). It runs as the `python_conformance` ctest gate against
35
+ the freshly built library on every CI leg, and can run standalone:
36
+
37
+ OPENRAR_LIB=/path/to/libopenrar.so pytest tests/ -q
38
+
39
+ The wheel bundles the platform shared library beside the package; the
40
+ loader performs the documented startup probes (ABI version strict equality
41
+ + feature-mask negotiation).
@@ -0,0 +1,27 @@
1
+ # OpenRAR Python SDK
2
+
3
+ ctypes binding over the frozen OpenRAR C ABI (`docs/abi-freeze.md`) — no
4
+ compiled extension, the C ABI is the whole contract.
5
+
6
+ ```python
7
+ from openrar import Archive, create, package_version
8
+
9
+ create("backup.rar", ["data.bin"], ["data.bin"], method=3)
10
+ with Archive("backup.rar") as a:
11
+ for entry in a.list():
12
+ print(entry["path"], entry["size"])
13
+ a.extract_to_path(0, "data-out.bin")
14
+ assert a.test(0)
15
+ ```
16
+
17
+ ## Conformance
18
+
19
+ The normative cross-binding case list lives in `tests/conformance_test.py`
20
+ (freeze prereq 3). It runs as the `python_conformance` ctest gate against
21
+ the freshly built library on every CI leg, and can run standalone:
22
+
23
+ OPENRAR_LIB=/path/to/libopenrar.so pytest tests/ -q
24
+
25
+ The wheel bundles the platform shared library beside the package; the
26
+ loader performs the documented startup probes (ABI version strict equality
27
+ + feature-mask negotiation).
@@ -0,0 +1,85 @@
1
+ """OpenRAR Python SDK — ctypes binding over the frozen C ABI (v1.30.0).
2
+
3
+ The binding exposes the file-mode handle family of the frozen surface
4
+ (docs/abi-freeze.md): open → list → extract-to-path / test, creation, and
5
+ mutation. Additive capability is negotiated at load time via
6
+ openrar_abi_features(); errors raise OpenRARError with the documented
7
+ RarError codes.
8
+ """
9
+
10
+ from . import _native
11
+ from ._native import ( # noqa: F401
12
+ ArchiveEntry,
13
+ Native,
14
+ OpenRARError,
15
+ RAR_ERR_ABORTED,
16
+ RAR_ERR_BAD_PASSWORD,
17
+ RAR_ERR_BUSY,
18
+ RAR_ERR_CRC_MISMATCH,
19
+ RAR_ERR_ENCRYPTED,
20
+ RAR_ERR_INVALID_ARG,
21
+ RAR_ERR_IO,
22
+ RAR_ERR_LIB_NOT_FOUND,
23
+ RAR_ERR_LIMIT_EXCEEDED,
24
+ RAR_ERR_MISSING_VOLUME,
25
+ RAR_ERR_NOMEM,
26
+ RAR_ERR_NOT_RAR,
27
+ RAR_ERR_PARTIAL_OK,
28
+ RAR_ERR_PASSWORD_TIMEOUT,
29
+ RAR_ERR_CONTAINMENT_BREACH,
30
+ RAR_ERR_DECRYPT,
31
+ RAR_ERR_RESOURCE_EXHAUSTION,
32
+ RAR_ERR_TRUNCATED,
33
+ RAR_ERR_UNSUPPORTED_FEATURE,
34
+ RAR_OK,
35
+ load_native,
36
+ )
37
+
38
+ # Kept in lockstep with pyproject.toml by the CI version gate. This literal
39
+ # had drifted to 1.30.0 while the project was at 1.40.5.
40
+ __version__ = "2.0.1"
41
+
42
+
43
+ class Archive:
44
+ """A file-mode handle (scan-once). Context-manager close semantics."""
45
+
46
+ def __init__(self, path: str, password: str = "", native: Native = None):
47
+ self._n = native or load_native()
48
+ self._h = self._n.open(path, password)
49
+
50
+ def __enter__(self):
51
+ return self
52
+
53
+ def __exit__(self, *exc):
54
+ self.close()
55
+ return False
56
+
57
+ def list(self):
58
+ return self._n.list(self._h)
59
+
60
+ def extract_to_path(self, index: int, dest: str):
61
+ self._n.extract_to_path(self._h, index, dest)
62
+
63
+ def test(self, index: int) -> bool:
64
+ return self._n.test(self._h, index)
65
+
66
+ def set_limits(self, member: int, total: int, header_count: int, header_bytes: int):
67
+ self._n.set_limits(self._h, member, total, header_count, header_bytes)
68
+
69
+ def close(self):
70
+ if getattr(self, "_h", 0):
71
+ self._n.close(self._h)
72
+ self._h = 0
73
+
74
+
75
+ def create(arc_path: str, src_paths, arc_names, method: int = 3, dict_size: int = 0):
76
+ """Create a RAR 5.0 archive from files on disk (method 0..5)."""
77
+ load_native().create(arc_path, src_paths, arc_names, method, dict_size)
78
+
79
+
80
+ def package_version() -> str:
81
+ return load_native().package_version
82
+
83
+
84
+ def abi_features() -> int:
85
+ return load_native().features
@@ -0,0 +1,362 @@
1
+ """openrar._native — ctypes binding over the frozen OpenRAR C ABI.
2
+
3
+ The C ABI (include/openrar/openrar_dll.h, docs/abi-freeze.md) is the whole
4
+ contract: ctypes needs no compiled extension, and the loader performs the
5
+ documented startup probes (openrar_version strict equality +
6
+ openrar_abi_features() negotiation, docs/dll-integration-spec.md §3).
7
+
8
+ Library discovery order (the wheel bundles the shared library beside this
9
+ package; source builds find it via OPENRAR_LIB or the build tree):
10
+ 1. OPENRAR_LIB environment variable (absolute path to the library)
11
+ 2. beside this package (the wheel layout)
12
+ 3. platform system search paths
13
+ """
14
+
15
+ import ctypes
16
+ import os
17
+ import sys
18
+ import threading
19
+ from pathlib import Path
20
+
21
+ __all__ = ["RarError", "OpenRARError", "ArchiveEntry", "Native", "load_native"]
22
+
23
+ # ── RarError mirror (parity-gated: tools/check_js_error_mirror.py pins the JS
24
+ # mirror; the conformance suite pins THIS one against the C enum) ─────────
25
+ RAR_OK = 0
26
+ RAR_ERR_PARTIAL_OK = 1
27
+ RAR_ERR_NOT_RAR = -1
28
+ RAR_ERR_UNSUPPORTED_FEATURE = -2
29
+ RAR_ERR_TRUNCATED = -3
30
+ RAR_ERR_CRC_MISMATCH = -4
31
+ RAR_ERR_NOMEM = -5
32
+ RAR_ERR_IO = -6
33
+ RAR_ERR_BAD_PASSWORD = -7
34
+ RAR_ERR_INVALID_ARG = -9
35
+ RAR_ERR_ABORTED = -11
36
+ RAR_ERR_ENCRYPTED = -12
37
+ RAR_ERR_MISSING_VOLUME = -13
38
+ RAR_ERR_BUSY = -14
39
+ RAR_ERR_LIMIT_EXCEEDED = -15
40
+ RAR_ERR_PASSWORD_TIMEOUT = -16
41
+ RAR_ERR_CONTAINMENT_BREACH = -17
42
+ RAR_ERR_DECRYPT = -18
43
+ RAR_ERR_RESOURCE_EXHAUSTION = -19
44
+ # Not a RAR error code: raised when the shared library itself cannot be
45
+ # loaded. Kept out of the -1..-19 range so it can never be confused with an
46
+ # archive-format failure ("NOT_RAR" sent people hunting a corrupt archive).
47
+ RAR_ERR_LIB_NOT_FOUND = -100
48
+
49
+ # Error-name mirror (CODE_MAP parity, minus OK/PARTIAL_OK which are never
50
+ # raised as errors).
51
+ _ERRORS = {
52
+ RAR_ERR_NOT_RAR: "NOT_RAR",
53
+ RAR_ERR_UNSUPPORTED_FEATURE: "UNSUPPORTED_FEATURE",
54
+ RAR_ERR_TRUNCATED: "TRUNCATED",
55
+ RAR_ERR_CRC_MISMATCH: "CRC_MISMATCH",
56
+ RAR_ERR_NOMEM: "NOMEM",
57
+ RAR_ERR_IO: "IO",
58
+ RAR_ERR_BAD_PASSWORD: "BAD_PASSWORD",
59
+ RAR_ERR_INVALID_ARG: "INVALID_ARG",
60
+ RAR_ERR_ABORTED: "ABORTED",
61
+ RAR_ERR_ENCRYPTED: "ENCRYPTED",
62
+ RAR_ERR_MISSING_VOLUME: "MISSING_VOLUME",
63
+ RAR_ERR_BUSY: "BUSY",
64
+ RAR_ERR_LIMIT_EXCEEDED: "LIMIT_EXCEEDED",
65
+ RAR_ERR_PASSWORD_TIMEOUT: "PASSWORD_TIMEOUT",
66
+ RAR_ERR_CONTAINMENT_BREACH: "CONTAINMENT_BREACH",
67
+ RAR_ERR_DECRYPT: "DECRYPT",
68
+ RAR_ERR_RESOURCE_EXHAUSTION: "RESOURCE_EXHAUSTION",
69
+ RAR_ERR_LIB_NOT_FOUND: "LIB_NOT_FOUND",
70
+ }
71
+
72
+ _DLL_API_VERSION = 1
73
+
74
+
75
+ class OpenRARError(Exception):
76
+ """Raised for a nonzero RarError return. .code is the numeric value,
77
+ .name the documented error name, .detail the thread-local message."""
78
+
79
+ def __init__(self, code: int, detail: str = ""):
80
+ self.code = code
81
+ self.name = _ERRORS.get(code, f"UNKNOWN_{code}")
82
+ self.detail = detail or ""
83
+ super().__init__(f"{self.name}: {self.detail}" if self.detail else self.name)
84
+
85
+
86
+ class ArchiveEntry(ctypes.Structure):
87
+ """The frozen 64-byte openrar_archive_entry_t (docs/abi-freeze.md §3)."""
88
+
89
+ _fields_ = [
90
+ ("path_offset", ctypes.c_uint32),
91
+ ("path_len", ctypes.c_uint32),
92
+ ("is_dir", ctypes.c_uint32),
93
+ ("method", ctypes.c_uint32),
94
+ ("is_encrypted", ctypes.c_uint32),
95
+ ("crc32", ctypes.c_uint32),
96
+ ("size", ctypes.c_uint64),
97
+ ("packed_size", ctypes.c_uint64),
98
+ ("mtime", ctypes.c_uint64),
99
+ ("_pad", ctypes.c_uint64 * 2),
100
+ ]
101
+
102
+
103
+ assert ctypes.sizeof(ArchiveEntry) == 64, "entry layout drifted from abi-freeze.md"
104
+
105
+
106
+ def _candidate_paths() -> list:
107
+ here = Path(__file__).resolve().parent
108
+ names = {
109
+ "win32": ["openrar.dll"],
110
+ "darwin": ["libopenrar.dylib"],
111
+ }.get(sys.platform, ["libopenrar.so"])
112
+ out = []
113
+ env = os.environ.get("OPENRAR_LIB")
114
+ if env:
115
+ out.append(Path(env))
116
+ for n in names:
117
+ out.append(here / n)
118
+ out.append(here.parent / n)
119
+ return out
120
+
121
+
122
+ class Native:
123
+ """Loaded C ABI + the startup probes. Construct once per process."""
124
+
125
+ def __init__(self, path: str = ""):
126
+ candidates = [Path(path)] if path else _candidate_paths()
127
+ self.lib = None
128
+ tried = []
129
+ for c in candidates:
130
+ if c.exists():
131
+ try:
132
+ self.lib = ctypes.CDLL(str(c))
133
+ self.path = str(c)
134
+ break
135
+ except OSError as e: # pragma: no cover - load failure details
136
+ tried.append(f"{c}: {e}")
137
+ else:
138
+ tried.append(str(c))
139
+ if self.lib is None:
140
+ raise OpenRARError(RAR_ERR_LIB_NOT_FOUND,
141
+ "OpenRAR shared library not found: " + "; ".join(tried))
142
+ self._declare()
143
+ self._probe()
144
+
145
+ def _declare(self):
146
+ lib = self.lib
147
+ lib.openrar_version.restype = ctypes.c_int
148
+ lib.openrar_archive_version.restype = ctypes.c_int
149
+ lib.openrar_package_version_string.restype = ctypes.c_char_p
150
+ lib.openrar_abi_features.restype = ctypes.c_uint64
151
+ lib.openrar_alloc.restype = ctypes.c_void_p
152
+ lib.openrar_alloc.argtypes = [ctypes.c_size_t]
153
+ lib.openrar_free.restype = None
154
+ lib.openrar_free.argtypes = [ctypes.c_void_p]
155
+ lib.openrar_archive_get_error.restype = ctypes.c_int
156
+ lib.openrar_archive_get_error.argtypes = [ctypes.c_char_p, ctypes.c_int]
157
+ lib.openrar_archive_open_file.restype = ctypes.c_uint32
158
+ lib.openrar_archive_open_file.argtypes = [
159
+ ctypes.c_char_p,
160
+ ctypes.c_char_p,
161
+ ctypes.c_void_p,
162
+ ctypes.c_void_p,
163
+ ctypes.c_void_p,
164
+ ]
165
+ lib.openrar_archive_close.restype = None
166
+ lib.close = lib.openrar_archive_close
167
+ lib.close.argtypes = [ctypes.c_uint32]
168
+ lib.openrar_archive_handle_list.restype = ctypes.c_int
169
+ lib.openrar_archive_handle_list.argtypes = [
170
+ ctypes.c_uint32,
171
+ ctypes.POINTER(ctypes.c_uint32),
172
+ ctypes.POINTER(ctypes.c_void_p),
173
+ ctypes.POINTER(ctypes.c_void_p),
174
+ ctypes.POINTER(ctypes.c_size_t),
175
+ ]
176
+ lib.openrar_archive_handle_extract_to_path.restype = ctypes.c_int
177
+ lib.openrar_archive_handle_extract_to_path.argtypes = [
178
+ ctypes.c_uint32,
179
+ ctypes.c_uint32,
180
+ ctypes.c_char_p,
181
+ ctypes.c_void_p,
182
+ ctypes.c_void_p,
183
+ ctypes.c_void_p,
184
+ ]
185
+ lib.openrar_archive_handle_test.restype = ctypes.c_int
186
+ lib.openrar_archive_handle_test.argtypes = [
187
+ ctypes.c_uint32,
188
+ ctypes.c_uint32,
189
+ ctypes.c_void_p,
190
+ ctypes.c_void_p,
191
+ ctypes.c_void_p,
192
+ ]
193
+ lib.openrar_archive_handle_set_limits.restype = ctypes.c_int
194
+ lib.openrar_archive_handle_set_limits.argtypes = [
195
+ ctypes.c_uint32,
196
+ ctypes.c_uint64,
197
+ ctypes.c_uint64,
198
+ ctypes.c_uint64,
199
+ ctypes.c_uint64,
200
+ ]
201
+ lib.openrar_archive_create_file.restype = ctypes.c_int
202
+ lib.openrar_archive_create_file.argtypes = [
203
+ ctypes.c_char_p,
204
+ ctypes.POINTER(ctypes.c_char_p),
205
+ ctypes.POINTER(ctypes.c_char_p),
206
+ ctypes.c_uint32,
207
+ ctypes.c_int,
208
+ ctypes.c_uint64,
209
+ ]
210
+ lib.openrar_archive_delete_entries_file.restype = ctypes.c_int
211
+ lib.openrar_archive_delete_entries_file.argtypes = [
212
+ ctypes.c_char_p,
213
+ ctypes.POINTER(ctypes.c_uint32),
214
+ ctypes.c_uint32,
215
+ ]
216
+ # Was called but never declared, so ctypes inferred the signature and
217
+ # read a garbage int return from a void function.
218
+ lib.openrar_archive_list_free.restype = None
219
+ lib.openrar_archive_list_free.argtypes = [
220
+ ctypes.c_void_p,
221
+ ctypes.c_void_p,
222
+ ctypes.c_size_t,
223
+ ]
224
+ lib.openrar_last_error.restype = ctypes.c_int
225
+ lib.openrar_last_error.argtypes = [ctypes.c_char_p, ctypes.c_int]
226
+
227
+ def _probe(self):
228
+ # docs/dll-integration-spec.md §3: strict-equality probes at startup.
229
+ v = self.lib.openrar_version()
230
+ if v != _DLL_API_VERSION:
231
+ raise OpenRARError(RAR_ERR_UNSUPPORTED_FEATURE,
232
+ f"ABI version mismatch: host={_DLL_API_VERSION} library={v}")
233
+ av = self.lib.openrar_archive_version()
234
+ if av != 1:
235
+ raise OpenRARError(RAR_ERR_UNSUPPORTED_FEATURE, f"archive version {av}")
236
+ self.package_version = self.lib.openrar_package_version_string().decode("utf-8")
237
+ self.features = self.lib.openrar_abi_features()
238
+ # Registered registry bits (docs/abi-freeze.md §5): 0..18 all set (v2.0).
239
+ self.expected_features = (1 << 19) - 1
240
+
241
+ # ── helpers ───────────────────────────────────────────────────────────
242
+ def last_error(self) -> str:
243
+ buf = ctypes.create_string_buffer(1024)
244
+ n = self.lib.openrar_archive_get_error(buf, 1024)
245
+ return buf.value.decode("utf-8", "replace") if n > 0 else ""
246
+
247
+ def check(self, rc: int, ok_codes=(RAR_OK,)):
248
+ if rc not in ok_codes:
249
+ raise OpenRARError(rc, self.last_error())
250
+
251
+ def open(self, path: str, password: str = "") -> int:
252
+ h = self.lib.openrar_archive_open_file(
253
+ os.fspath(path).encode("utf-8"),
254
+ password.encode("utf-8") if password else None,
255
+ None, None, None,
256
+ )
257
+ if h == 0:
258
+ raise OpenRARError(RAR_ERR_IO, self.last_error())
259
+ return h
260
+
261
+ def list(self, handle: int):
262
+ count = ctypes.c_uint32(0)
263
+ entries = ctypes.c_void_p()
264
+ paths = ctypes.c_void_p()
265
+ paths_size = ctypes.c_size_t(0)
266
+ rc = self.lib.openrar_archive_handle_list(
267
+ handle, ctypes.byref(count), ctypes.byref(entries), ctypes.byref(paths),
268
+ ctypes.byref(paths_size),
269
+ )
270
+ self.check(rc)
271
+ try:
272
+ raw = ctypes.string_at(entries, count.value * ctypes.sizeof(ArchiveEntry))
273
+ blob = ctypes.string_at(paths, paths_size.value) if paths.value else b""
274
+ out = []
275
+ for i in range(count.value):
276
+ e = ArchiveEntry.from_buffer_copy(raw, i * ctypes.sizeof(ArchiveEntry))
277
+ # path_offset/path_len come from the (possibly crafted)
278
+ # archive. Python slicing clamps silently, so an out-of-range
279
+ # range used to yield "" instead of an error - reject it.
280
+ if e.path_offset + e.path_len > len(blob):
281
+ raise OpenRARError(
282
+ RAR_ERR_TRUNCATED,
283
+ f"entry {i}: path range [{e.path_offset},"
284
+ f"{e.path_offset + e.path_len}) exceeds blob size {len(blob)}")
285
+ path = blob[e.path_offset:e.path_offset + e.path_len].decode("utf-8", "replace")
286
+ out.append({
287
+ "path": path,
288
+ "is_dir": bool(e.is_dir),
289
+ "method": e.method,
290
+ "is_encrypted": bool(e.is_encrypted),
291
+ "crc32": e.crc32,
292
+ "size": e.size,
293
+ "packed_size": e.packed_size,
294
+ "mtime": e.mtime,
295
+ })
296
+ return out
297
+ finally:
298
+ self.lib.openrar_archive_list_free(entries, paths, paths_size)
299
+
300
+ def extract_to_path(self, handle: int, index: int, dest: str):
301
+ self.check(self.lib.openrar_archive_handle_extract_to_path(
302
+ handle, index, os.fspath(dest).encode("utf-8"), None, None, None))
303
+
304
+ def test(self, handle: int, index: int) -> bool:
305
+ return self.test_code(handle, index) == RAR_OK
306
+
307
+ def test_code(self, handle: int, index: int) -> int:
308
+ """Raw return code. Use this when the distinction between
309
+ CRC_MISMATCH / BAD_PASSWORD / TRUNCATED / UNSUPPORTED_FEATURE matters;
310
+ test() collapses them all to False."""
311
+ return self.lib.openrar_archive_handle_test(handle, index, None, None, None)
312
+
313
+ def set_limits(self, handle: int, member: int, total: int, hdr_count: int, hdr_bytes: int):
314
+ # Reject negatives before ctypes masks them into c_uint64. A caller
315
+ # passing -1 for "unlimited" silently produced 2**64-1, which happens
316
+ # to mean unlimited here - but -1 for a real budget silently became
317
+ # the LARGEST possible budget instead of an error.
318
+ limits = (member, total, hdr_count, hdr_bytes)
319
+ if any(v < 0 for v in limits):
320
+ raise OpenRARError(RAR_ERR_INVALID_ARG,
321
+ f"limits must be non-negative (got {limits})")
322
+ self.check(self.lib.openrar_archive_handle_set_limits(
323
+ handle, member, total, hdr_count, hdr_bytes))
324
+
325
+ def close(self, handle: int):
326
+ self.lib.openrar_archive_close(handle)
327
+
328
+ def create(self, arc_path: str, src_paths, arc_names, method: int = 3, dict_size: int = 0):
329
+ src_paths = list(src_paths)
330
+ arc_names = list(arc_names)
331
+ n = len(src_paths)
332
+ if n == 0:
333
+ raise OpenRARError(RAR_ERR_INVALID_ARG, "at least one file is required")
334
+ if len(arc_names) != n:
335
+ raise OpenRARError(
336
+ RAR_ERR_INVALID_ARG,
337
+ f"src_paths ({n}) and arc_names ({len(arc_names)}) must be the same length")
338
+ src_arr = (ctypes.c_char_p * n)(*(os.fspath(p).encode("utf-8") for p in src_paths))
339
+ name_arr = (ctypes.c_char_p * n)(*(nm.encode("utf-8") for nm in arc_names))
340
+ self.check(self.lib.openrar_archive_create_file(
341
+ os.fspath(arc_path).encode("utf-8"), src_arr, name_arr, n, method, dict_size))
342
+
343
+ def delete_entries(self, arc_path: str, indices):
344
+ n = len(indices)
345
+ arr = (ctypes.c_uint32 * n)(*indices)
346
+ self.check(self.lib.openrar_archive_delete_entries_file(
347
+ os.fspath(arc_path).encode("utf-8"), arr, n))
348
+
349
+
350
+ _cached = None
351
+ _cached_lock = threading.Lock()
352
+
353
+
354
+ def load_native(path: str = "") -> Native:
355
+ global _cached
356
+ # Guard first-use: two threads racing here previously constructed two
357
+ # Native objects (two CDLL handles). An explicit path still replaces the
358
+ # process-wide default, which is the documented behaviour.
359
+ with _cached_lock:
360
+ if _cached is None or path:
361
+ _cached = Native(path)
362
+ return _cached
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: openrar
3
+ Version: 2.0.1
4
+ Summary: OpenRAR Python SDK — ctypes binding over the frozen OpenRAR C ABI (RAR 5.0 read/create/mutate)
5
+ Author: OpenRAR Project
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/geometric-dev/openrar
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: Microsoft :: Windows
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Operating System :: MacOS
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+
15
+ # OpenRAR Python SDK
16
+
17
+ ctypes binding over the frozen OpenRAR C ABI (`docs/abi-freeze.md`) — no
18
+ compiled extension, the C ABI is the whole contract.
19
+
20
+ ```python
21
+ from openrar import Archive, create, package_version
22
+
23
+ create("backup.rar", ["data.bin"], ["data.bin"], method=3)
24
+ with Archive("backup.rar") as a:
25
+ for entry in a.list():
26
+ print(entry["path"], entry["size"])
27
+ a.extract_to_path(0, "data-out.bin")
28
+ assert a.test(0)
29
+ ```
30
+
31
+ ## Conformance
32
+
33
+ The normative cross-binding case list lives in `tests/conformance_test.py`
34
+ (freeze prereq 3). It runs as the `python_conformance` ctest gate against
35
+ the freshly built library on every CI leg, and can run standalone:
36
+
37
+ OPENRAR_LIB=/path/to/libopenrar.so pytest tests/ -q
38
+
39
+ The wheel bundles the platform shared library beside the package; the
40
+ loader performs the documented startup probes (ABI version strict equality
41
+ + feature-mask negotiation).
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ openrar/__init__.py
4
+ openrar/_native.py
5
+ openrar.egg-info/PKG-INFO
6
+ openrar.egg-info/SOURCES.txt
7
+ openrar.egg-info/dependency_links.txt
8
+ openrar.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ openrar
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "openrar"
7
+ version = "2.0.1"
8
+ description = "OpenRAR Python SDK — ctypes binding over the frozen OpenRAR C ABI (RAR 5.0 read/create/mutate)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "OpenRAR Project" }]
13
+ urls = { Homepage = "https://github.com/geometric-dev/openrar" }
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: Microsoft :: Windows",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Operating System :: MacOS",
19
+ ]
20
+
21
+ [tool.setuptools]
22
+ packages = ["openrar"]
23
+
24
+ # Platform wheels bundle the built shared library beside the package (the
25
+ # release workflow copies it into openrar/ before `python -m build`). Without
26
+ # this declaration the wheel never contains the library, and load_native()
27
+ # fails for every installed user - which is what the previous state did (the
28
+ # note below described the mechanism but nothing implemented it).
29
+ [tool.setuptools.package-data]
30
+ openrar = ["*.so", "*.so.*", "*.dylib", "*.dll"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+