pylibmspack 0.1.0__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.
Files changed (31) hide show
  1. pylibmspack-0.1.0/LICENSE +21 -0
  2. pylibmspack-0.1.0/MANIFEST.in +10 -0
  3. pylibmspack-0.1.0/NOTICE +3 -0
  4. pylibmspack-0.1.0/PKG-INFO +211 -0
  5. pylibmspack-0.1.0/README.md +182 -0
  6. pylibmspack-0.1.0/THIRD_PARTY_LICENSES/LGPL-2.1.txt +460 -0
  7. pylibmspack-0.1.0/pyproject.toml +53 -0
  8. pylibmspack-0.1.0/scripts/build_libmspack.py +227 -0
  9. pylibmspack-0.1.0/scripts/generate_fixtures.py +37 -0
  10. pylibmspack-0.1.0/setup.cfg +22 -0
  11. pylibmspack-0.1.0/setup.py +89 -0
  12. pylibmspack-0.1.0/src/pylibmspack/__init__.py +24 -0
  13. pylibmspack-0.1.0/src/pylibmspack/_cab.c +1145 -0
  14. pylibmspack-0.1.0/src/pylibmspack/_libmspack.py +11 -0
  15. pylibmspack-0.1.0/src/pylibmspack/cab.py +286 -0
  16. pylibmspack-0.1.0/src/pylibmspack/errors.py +17 -0
  17. pylibmspack-0.1.0/src/pylibmspack/vendor/__init__.py +1 -0
  18. pylibmspack-0.1.0/src/pylibmspack/vendor/libmspack_0.10.1.orig.tar.xz +0 -0
  19. pylibmspack-0.1.0/src/pylibmspack.egg-info/PKG-INFO +211 -0
  20. pylibmspack-0.1.0/src/pylibmspack.egg-info/SOURCES.txt +30 -0
  21. pylibmspack-0.1.0/src/pylibmspack.egg-info/dependency_links.txt +1 -0
  22. pylibmspack-0.1.0/src/pylibmspack.egg-info/not-zip-safe +1 -0
  23. pylibmspack-0.1.0/src/pylibmspack.egg-info/requires.txt +3 -0
  24. pylibmspack-0.1.0/src/pylibmspack.egg-info/top_level.txt +1 -0
  25. pylibmspack-0.1.0/tests/fixtures/expected/lzx.txt +3 -0
  26. pylibmspack-0.1.0/tests/fixtures/expected/mszip.txt +1 -0
  27. pylibmspack-0.1.0/tests/fixtures/expected/qtm.txt +1 -0
  28. pylibmspack-0.1.0/tests/fixtures/normal_2files_1folder.cab +0 -0
  29. pylibmspack-0.1.0/tests/fixtures/small_mszip.cab +0 -0
  30. pylibmspack-0.1.0/tests/fixtures/traversal.cab +0 -0
  31. pylibmspack-0.1.0/tests/test_cab.py +83 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ include README.md LICENSE NOTICE pyproject.toml setup.cfg setup.py
2
+ include THIRD_PARTY_LICENSES/LGPL-2.1.txt
3
+ recursive-include src/pylibmspack *.c *.py
4
+ recursive-include src/pylibmspack/.dylibs *
5
+ include src/pylibmspack/libmspack.dylib
6
+ recursive-include src/pylibmspack/vendor *.tar.xz
7
+ recursive-include scripts *.py
8
+ recursive-include tests *.py
9
+ recursive-include tests/fixtures *.cab
10
+ recursive-include tests/fixtures/expected *.txt
@@ -0,0 +1,3 @@
1
+ pylibmspack bundles libmspack under the LGPL-2.1 license.
2
+ The corresponding libmspack source tarball is included under pylibmspack/vendor/.
3
+ Users may replace the bundled shared library inside pylibmspack/.libs with a compatible build.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: pylibmspack
3
+ Version: 0.1.0
4
+ Summary: In-process libmspack bindings for Microsoft CAB files
5
+ Author-email: Example Author <dev@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/bwhitn/pylibmspack
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: C
20
+ Classifier: Topic :: System :: Archiving
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ License-File: THIRD_PARTY_LICENSES/LGPL-2.1.txt
25
+ License-File: NOTICE
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # pylibmspack
31
+
32
+ `pylibmspack` provides in-process Python bindings to **libmspack** for reading and extracting Microsoft CAB files, including Quantum and LZX cabinets. It is a CPython extension (no subprocess calls).
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install pylibmspack
38
+ ```
39
+
40
+ Supports Python 3.9 through 3.13.
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ from pylibmspack import CabArchive
46
+
47
+ cab = CabArchive("example.cab")
48
+ print(cab.files())
49
+
50
+ print(cab.read("hello.txt"))
51
+
52
+ cab.extract("hello.txt", "./out")
53
+
54
+ cab.extract_all("./out")
55
+ ```
56
+
57
+ ### In-memory usage
58
+
59
+ ```python
60
+ from pylibmspack import CabArchive
61
+
62
+ data = open("example.cab", "rb").read()
63
+ cab = CabArchive.from_bytes(data)
64
+
65
+ info = cab.info()
66
+ print(info["files_count"], info["flags"])
67
+
68
+ payload = cab.read("hello.txt")
69
+ ```
70
+
71
+ ### Safe vs raw extraction
72
+
73
+ ```python
74
+ from pylibmspack import CabArchive, CabPathTraversalError
75
+
76
+ cab = CabArchive("example.cab")
77
+ try:
78
+ cab.extract_all("./out", safe=True)
79
+ except CabPathTraversalError as exc:
80
+ print("Blocked unsafe path:", exc)
81
+
82
+ # Raw extraction (no safety checks)
83
+ cab.extract_all_raw("./out-raw")
84
+ ```
85
+
86
+ ### Multi-cabinet sets
87
+
88
+ ```python
89
+ from pylibmspack import CabArchive
90
+
91
+ cab = CabArchive("part1.cab")
92
+ info = cab.info()
93
+
94
+ if info["has_next"]:
95
+ print("Next cabinet:", info["next_cabinet"])
96
+ print("Disk label:", info["next_disk"])
97
+ ```
98
+
99
+ ### FAQ / troubleshooting
100
+
101
+ **Why do I get `CabPathTraversalError`?**
102
+ The archive contains absolute paths or `..` segments. Use `safe=False` only if you trust the archive contents.
103
+
104
+ **Can I read from bytes instead of a file path?**
105
+ Yes. Use `CabArchive.from_bytes(data)` and then call `files()`, `read()`, or `info()`.
106
+
107
+ **Why does extraction fail with `CabDecompressionError`?**
108
+ The CAB may be corrupt, truncated, or uses an unsupported compression method.
109
+
110
+ ## API reference
111
+
112
+ ### CabArchive(path: str)
113
+
114
+ Open a CAB archive on disk.
115
+
116
+ ### CabArchive.files() -> list[CabFileInfo]
117
+
118
+ Return metadata for each member as a `CabFileInfo` TypedDict. Each entry includes:
119
+
120
+ - `name` (str)
121
+ - `size` (int)
122
+ - `dos_date` (int)
123
+ - `dos_time` (int)
124
+ - `date_y` / `date_m` / `date_d` (int)
125
+ - `time_h` / `time_m` / `time_s` (int)
126
+ - `datetime_utc` (str, ISO 8601)
127
+ - `attrs` (int)
128
+ - `is_readonly` / `is_hidden` / `is_system` / `is_archive` (bool)
129
+ - `folder_index` (int)
130
+ - `offset` (int)
131
+ - `compression` (str: `none`, `mszip`, `quantum`, `lzx`)
132
+ - `has_prev` / `has_next` (bool)
133
+ - `prev_cabinet` / `next_cabinet` (str | None)
134
+ - `cabinet_set_id` / `cabinet_set_index` (int | None)
135
+
136
+ ### CabArchive.read(name: str, *, max_size: int = 256*1024*1024) -> bytes
137
+
138
+ Extract a member and return its bytes. Enforces a `max_size` limit and uses safe path validation.
139
+
140
+ ### CabArchive.extract(name: str, dest_dir: str, *, safe: bool = True) -> str
141
+
142
+ Extract a member to disk and return the output path. When `safe=True`, absolute paths and traversal are rejected.
143
+
144
+ ### CabArchive.extract_all(dest_dir: str, *, safe: bool = True) -> list[str]
145
+
146
+ Extract all members to disk and return output paths.
147
+
148
+ ### CabArchive.extract_raw(name: str, dest_dir: str) -> str
149
+
150
+ Extract a member using the raw path (no safety checks).
151
+
152
+ ### CabArchive.extract_all_raw(dest_dir: str) -> list[str]
153
+
154
+ Extract all members using raw paths (no safety checks).
155
+
156
+ ### CabArchive.from_bytes(data: bytes) -> CabArchive
157
+
158
+ Create an archive backed by in-memory bytes instead of a file path.
159
+
160
+ ### CabArchive.info() -> CabInfo
161
+
162
+ Return parsed CAB header metadata. The `CabInfo` dict includes:
163
+
164
+ - `filename` (str | None)
165
+ - `base_offset` (int)
166
+ - `length` (int)
167
+ - `set_id` (int)
168
+ - `set_index` (int)
169
+ - `header_resv` (int)
170
+ - `flags` (int)
171
+ - `has_prev` / `has_next` (bool)
172
+ - `prev_cabinet` / `next_cabinet` (str | None)
173
+ - `prev_disk` / `next_disk` (str | None)
174
+ - `files_count` (int)
175
+ - `folders_count` (int)
176
+
177
+ ### Exceptions
178
+
179
+ All errors derive from `CabError`:
180
+
181
+ - `CabError`
182
+ - `CabFormatError`
183
+ - `CabDecompressionError`
184
+ - `CabPathTraversalError`
185
+
186
+ ## Safe extraction
187
+
188
+ By default, `extract()` and `extract_all()` reject:
189
+ - absolute paths (`/`, `\`, drive letters, UNC paths)
190
+ - path traversal (`..` after normalization)
191
+ - mixed or odd separators (`/` and `\` are normalized)
192
+
193
+ Use `safe=False` to allow the original paths.
194
+
195
+ ## Build from source
196
+
197
+ This project uses setuptools and builds a shared `libmspack` that is bundled into wheels. A pinned libmspack source tarball is included under `pylibmspack/vendor/` and used for offline builds (SHA-256 verified).
198
+
199
+ ```bash
200
+ python -m pip install -U pip setuptools wheel
201
+ python -m pip install -e .
202
+ ```
203
+
204
+ If you want to supply a local tarball, pass `--tarball` to `scripts/build_libmspack.py`. To allow a network download during builds, set `PYLIBMSPACK_ALLOW_DOWNLOAD=1` (disabled by default).
205
+
206
+ ## Licensing
207
+
208
+ - **pylibmspack** code is MIT licensed.
209
+ - Wheels bundle **libmspack** under LGPL-2.1. The corresponding libmspack source tarball is included under `pylibmspack/vendor/`. You may replace the shared library inside `pylibmspack/.libs` with a compatible build.
210
+
211
+ See `THIRD_PARTY_LICENSES/LGPL-2.1.txt` and `NOTICE` for details.
@@ -0,0 +1,182 @@
1
+ # pylibmspack
2
+
3
+ `pylibmspack` provides in-process Python bindings to **libmspack** for reading and extracting Microsoft CAB files, including Quantum and LZX cabinets. It is a CPython extension (no subprocess calls).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install pylibmspack
9
+ ```
10
+
11
+ Supports Python 3.9 through 3.13.
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from pylibmspack import CabArchive
17
+
18
+ cab = CabArchive("example.cab")
19
+ print(cab.files())
20
+
21
+ print(cab.read("hello.txt"))
22
+
23
+ cab.extract("hello.txt", "./out")
24
+
25
+ cab.extract_all("./out")
26
+ ```
27
+
28
+ ### In-memory usage
29
+
30
+ ```python
31
+ from pylibmspack import CabArchive
32
+
33
+ data = open("example.cab", "rb").read()
34
+ cab = CabArchive.from_bytes(data)
35
+
36
+ info = cab.info()
37
+ print(info["files_count"], info["flags"])
38
+
39
+ payload = cab.read("hello.txt")
40
+ ```
41
+
42
+ ### Safe vs raw extraction
43
+
44
+ ```python
45
+ from pylibmspack import CabArchive, CabPathTraversalError
46
+
47
+ cab = CabArchive("example.cab")
48
+ try:
49
+ cab.extract_all("./out", safe=True)
50
+ except CabPathTraversalError as exc:
51
+ print("Blocked unsafe path:", exc)
52
+
53
+ # Raw extraction (no safety checks)
54
+ cab.extract_all_raw("./out-raw")
55
+ ```
56
+
57
+ ### Multi-cabinet sets
58
+
59
+ ```python
60
+ from pylibmspack import CabArchive
61
+
62
+ cab = CabArchive("part1.cab")
63
+ info = cab.info()
64
+
65
+ if info["has_next"]:
66
+ print("Next cabinet:", info["next_cabinet"])
67
+ print("Disk label:", info["next_disk"])
68
+ ```
69
+
70
+ ### FAQ / troubleshooting
71
+
72
+ **Why do I get `CabPathTraversalError`?**
73
+ The archive contains absolute paths or `..` segments. Use `safe=False` only if you trust the archive contents.
74
+
75
+ **Can I read from bytes instead of a file path?**
76
+ Yes. Use `CabArchive.from_bytes(data)` and then call `files()`, `read()`, or `info()`.
77
+
78
+ **Why does extraction fail with `CabDecompressionError`?**
79
+ The CAB may be corrupt, truncated, or uses an unsupported compression method.
80
+
81
+ ## API reference
82
+
83
+ ### CabArchive(path: str)
84
+
85
+ Open a CAB archive on disk.
86
+
87
+ ### CabArchive.files() -> list[CabFileInfo]
88
+
89
+ Return metadata for each member as a `CabFileInfo` TypedDict. Each entry includes:
90
+
91
+ - `name` (str)
92
+ - `size` (int)
93
+ - `dos_date` (int)
94
+ - `dos_time` (int)
95
+ - `date_y` / `date_m` / `date_d` (int)
96
+ - `time_h` / `time_m` / `time_s` (int)
97
+ - `datetime_utc` (str, ISO 8601)
98
+ - `attrs` (int)
99
+ - `is_readonly` / `is_hidden` / `is_system` / `is_archive` (bool)
100
+ - `folder_index` (int)
101
+ - `offset` (int)
102
+ - `compression` (str: `none`, `mszip`, `quantum`, `lzx`)
103
+ - `has_prev` / `has_next` (bool)
104
+ - `prev_cabinet` / `next_cabinet` (str | None)
105
+ - `cabinet_set_id` / `cabinet_set_index` (int | None)
106
+
107
+ ### CabArchive.read(name: str, *, max_size: int = 256*1024*1024) -> bytes
108
+
109
+ Extract a member and return its bytes. Enforces a `max_size` limit and uses safe path validation.
110
+
111
+ ### CabArchive.extract(name: str, dest_dir: str, *, safe: bool = True) -> str
112
+
113
+ Extract a member to disk and return the output path. When `safe=True`, absolute paths and traversal are rejected.
114
+
115
+ ### CabArchive.extract_all(dest_dir: str, *, safe: bool = True) -> list[str]
116
+
117
+ Extract all members to disk and return output paths.
118
+
119
+ ### CabArchive.extract_raw(name: str, dest_dir: str) -> str
120
+
121
+ Extract a member using the raw path (no safety checks).
122
+
123
+ ### CabArchive.extract_all_raw(dest_dir: str) -> list[str]
124
+
125
+ Extract all members using raw paths (no safety checks).
126
+
127
+ ### CabArchive.from_bytes(data: bytes) -> CabArchive
128
+
129
+ Create an archive backed by in-memory bytes instead of a file path.
130
+
131
+ ### CabArchive.info() -> CabInfo
132
+
133
+ Return parsed CAB header metadata. The `CabInfo` dict includes:
134
+
135
+ - `filename` (str | None)
136
+ - `base_offset` (int)
137
+ - `length` (int)
138
+ - `set_id` (int)
139
+ - `set_index` (int)
140
+ - `header_resv` (int)
141
+ - `flags` (int)
142
+ - `has_prev` / `has_next` (bool)
143
+ - `prev_cabinet` / `next_cabinet` (str | None)
144
+ - `prev_disk` / `next_disk` (str | None)
145
+ - `files_count` (int)
146
+ - `folders_count` (int)
147
+
148
+ ### Exceptions
149
+
150
+ All errors derive from `CabError`:
151
+
152
+ - `CabError`
153
+ - `CabFormatError`
154
+ - `CabDecompressionError`
155
+ - `CabPathTraversalError`
156
+
157
+ ## Safe extraction
158
+
159
+ By default, `extract()` and `extract_all()` reject:
160
+ - absolute paths (`/`, `\`, drive letters, UNC paths)
161
+ - path traversal (`..` after normalization)
162
+ - mixed or odd separators (`/` and `\` are normalized)
163
+
164
+ Use `safe=False` to allow the original paths.
165
+
166
+ ## Build from source
167
+
168
+ This project uses setuptools and builds a shared `libmspack` that is bundled into wheels. A pinned libmspack source tarball is included under `pylibmspack/vendor/` and used for offline builds (SHA-256 verified).
169
+
170
+ ```bash
171
+ python -m pip install -U pip setuptools wheel
172
+ python -m pip install -e .
173
+ ```
174
+
175
+ If you want to supply a local tarball, pass `--tarball` to `scripts/build_libmspack.py`. To allow a network download during builds, set `PYLIBMSPACK_ALLOW_DOWNLOAD=1` (disabled by default).
176
+
177
+ ## Licensing
178
+
179
+ - **pylibmspack** code is MIT licensed.
180
+ - Wheels bundle **libmspack** under LGPL-2.1. The corresponding libmspack source tarball is included under `pylibmspack/vendor/`. You may replace the shared library inside `pylibmspack/.libs` with a compatible build.
181
+
182
+ See `THIRD_PARTY_LICENSES/LGPL-2.1.txt` and `NOTICE` for details.