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 +41 -0
- openrar-2.0.1/README.md +27 -0
- openrar-2.0.1/openrar/__init__.py +85 -0
- openrar-2.0.1/openrar/_native.py +362 -0
- openrar-2.0.1/openrar.egg-info/PKG-INFO +41 -0
- openrar-2.0.1/openrar.egg-info/SOURCES.txt +8 -0
- openrar-2.0.1/openrar.egg-info/dependency_links.txt +1 -0
- openrar-2.0.1/openrar.egg-info/top_level.txt +1 -0
- openrar-2.0.1/pyproject.toml +30 -0
- openrar-2.0.1/setup.cfg +4 -0
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).
|
openrar-2.0.1/README.md
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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"]
|
openrar-2.0.1/setup.cfg
ADDED