multiarchive 0.2.0__tar.gz → 0.2.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.
- {multiarchive-0.2.0 → multiarchive-0.2.1}/PKG-INFO +48 -30
- {multiarchive-0.2.0 → multiarchive-0.2.1}/README.md +47 -29
- {multiarchive-0.2.0 → multiarchive-0.2.1}/pyproject.toml +1 -1
- {multiarchive-0.2.0 → multiarchive-0.2.1}/pyproject.toml.orig +8 -8
- {multiarchive-0.2.0 → multiarchive-0.2.1}/src/multiarchive/_archive.py +46 -10
- {multiarchive-0.2.0 → multiarchive-0.2.1}/src/multiarchive/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: multiarchive
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A high level archive handler for Python.
|
|
5
5
|
Author: NSPC911
|
|
6
6
|
Author-email: NSPC911 <87571998+NSPC911@users.noreply.github.com>
|
|
@@ -31,7 +31,7 @@ uv add "backports-zstd; python_version < '3.14'"
|
|
|
31
31
|
|
|
32
32
|
## Quick Start
|
|
33
33
|
|
|
34
|
-
```
|
|
34
|
+
```py
|
|
35
35
|
from multiarchive import Archive
|
|
36
36
|
|
|
37
37
|
# Open and inspect
|
|
@@ -82,14 +82,14 @@ Plain compressed files expose one member named after the file without its compre
|
|
|
82
82
|
|
|
83
83
|
Use the context manager (recommended):
|
|
84
84
|
|
|
85
|
-
```
|
|
85
|
+
```py
|
|
86
86
|
with Archive("file.zip") as arc:
|
|
87
87
|
...
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
Or the classmethod:
|
|
91
91
|
|
|
92
|
-
```
|
|
92
|
+
```py
|
|
93
93
|
arc = Archive.open_archive("file.zip")
|
|
94
94
|
try:
|
|
95
95
|
arc.namelist()
|
|
@@ -97,12 +97,30 @@ finally:
|
|
|
97
97
|
arc.close()
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Reading is supported on all formats
|
|
100
|
+
Reading is supported on all formats
|
|
101
101
|
If `algo` isnt explicitly provided, it will attempt to open it as ZIP first, then TAR, then RAR (if `rarfile` is installed). If all fails, it raises a ValueError.
|
|
102
102
|
|
|
103
|
+
#### Passwords
|
|
104
|
+
|
|
105
|
+
```py
|
|
106
|
+
with Archive("file.zip", pwd=b"secret") as arc:
|
|
107
|
+
...
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
classmethod
|
|
111
|
+
|
|
112
|
+
```py
|
|
113
|
+
arc = Archive.open_archive("file.zip", pwd=b"secret")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Exceptions for passwords:
|
|
117
|
+
- `ValueError`: if archive needs password but none was provided
|
|
118
|
+
- `KeyError`: if provided password is incorrect
|
|
119
|
+
- `TypeError`: if password is provided for a tarball (not supported)
|
|
120
|
+
|
|
103
121
|
### Listing Members
|
|
104
122
|
|
|
105
|
-
```
|
|
123
|
+
```py
|
|
106
124
|
with Archive("file.zip") as arc:
|
|
107
125
|
names = arc.namelist() # Just names
|
|
108
126
|
names = arc.members # alias to namelist
|
|
@@ -114,7 +132,7 @@ with Archive("file.zip") as arc:
|
|
|
114
132
|
|
|
115
133
|
### Checking Membership
|
|
116
134
|
|
|
117
|
-
```
|
|
135
|
+
```py
|
|
118
136
|
with Archive("file.zip") as arc:
|
|
119
137
|
if "config.json" in arc:
|
|
120
138
|
print("Found!")
|
|
@@ -128,7 +146,7 @@ with Archive("file.zip") as arc:
|
|
|
128
146
|
|
|
129
147
|
### Extracting
|
|
130
148
|
|
|
131
|
-
```
|
|
149
|
+
```py
|
|
132
150
|
with Archive("file.tar.gz") as arc:
|
|
133
151
|
# Single file
|
|
134
152
|
arc.extract("src/main.py", path="./extracted")
|
|
@@ -136,7 +154,7 @@ with Archive("file.tar.gz") as arc:
|
|
|
136
154
|
|
|
137
155
|
### Reading Member Contents
|
|
138
156
|
|
|
139
|
-
```
|
|
157
|
+
```py
|
|
140
158
|
with Archive("file.zip") as arc:
|
|
141
159
|
f = arc.open("data.json")
|
|
142
160
|
if f is not None:
|
|
@@ -148,7 +166,7 @@ with Archive("file.zip") as arc:
|
|
|
148
166
|
|
|
149
167
|
### Creating a New Archive
|
|
150
168
|
|
|
151
|
-
```
|
|
169
|
+
```py
|
|
152
170
|
from multiarchive import Archive
|
|
153
171
|
|
|
154
172
|
# ZIP
|
|
@@ -162,7 +180,7 @@ with Archive("output.tar.gz", mode="w", compression_level=9) as arc:
|
|
|
162
180
|
|
|
163
181
|
Add members with an optional name to use inside the archive:
|
|
164
182
|
|
|
165
|
-
```
|
|
183
|
+
```py
|
|
166
184
|
with Archive("output.zip", mode="w") as arc:
|
|
167
185
|
arc.add("src/main.py", arcname="main.py")
|
|
168
186
|
```
|
|
@@ -170,14 +188,14 @@ with Archive("output.zip", mode="w") as arc:
|
|
|
170
188
|
ZIP timestamps are limited to the years 1980 through 2107. To clamp timestamps
|
|
171
189
|
outside that range instead of raising an error:
|
|
172
190
|
|
|
173
|
-
```
|
|
191
|
+
```py
|
|
174
192
|
with Archive("output.zip", mode="w", strict_timestamps=False) as arc:
|
|
175
193
|
arc.add("old-file.txt")
|
|
176
194
|
```
|
|
177
195
|
|
|
178
196
|
The format is determined by file extension. Use the `algo` parameter to override:
|
|
179
197
|
|
|
180
|
-
```
|
|
198
|
+
```py
|
|
181
199
|
with Archive("my_archive", mode="w", algo="tar.xz") as arc:
|
|
182
200
|
...
|
|
183
201
|
```
|
|
@@ -196,19 +214,19 @@ with Archive("my_archive", mode="w", algo="tar.xz") as arc:
|
|
|
196
214
|
|
|
197
215
|
The `infolist()` method returns `ArchiveMemberInfo` objects with a unified interface across all formats:
|
|
198
216
|
|
|
199
|
-
| Field | Type
|
|
200
|
-
| ------------------- |
|
|
201
|
-
| `name` | `str`
|
|
202
|
-
| `uncompressed_size` | `int`
|
|
203
|
-
| `compressed_size` | `int \| None`
|
|
204
|
-
| `mtime` | `float`
|
|
205
|
-
| `mode` | `int \| None`
|
|
206
|
-
| `is_dir` | `bool`
|
|
217
|
+
| Field | Type | Description |
|
|
218
|
+
| ------------------- | --------------------------------------- | ----------------------------------------------------------- |
|
|
219
|
+
| `name` | `str` | Member filename |
|
|
220
|
+
| `uncompressed_size` | `int` | Original file size in bytes |
|
|
221
|
+
| `compressed_size` | `int \| None` | Compressed size (`None` for TAR) |
|
|
222
|
+
| `mtime` | `float` | Modification time as epoch seconds |
|
|
223
|
+
| `mode` | `int \| None` | Unix permission bits |
|
|
224
|
+
| `is_dir` | `bool` | Whether the member is a directory |
|
|
207
225
|
| `raw` | `ZipInfo \| TarInfo \| RarInfo \| None` | Original backend object (`None` for plain compressed files) |
|
|
208
226
|
|
|
209
227
|
Factory methods for direct conversion (you shouldn't need these in normal usage):
|
|
210
228
|
|
|
211
|
-
```
|
|
229
|
+
```py
|
|
212
230
|
info = ArchiveMemberInfo.from_zipinfo(zip_info)
|
|
213
231
|
info = ArchiveMemberInfo.from_tarinfo(tar_info)
|
|
214
232
|
info = ArchiveMemberInfo.from_rarinfo(rar_info)
|
|
@@ -216,15 +234,15 @@ info = ArchiveMemberInfo.from_rarinfo(rar_info)
|
|
|
216
234
|
|
|
217
235
|
## Properties
|
|
218
236
|
|
|
219
|
-
| Method/Property
|
|
220
|
-
|
|
|
221
|
-
| `members`
|
|
222
|
-
| `size`
|
|
223
|
-
| `comment` / `comment=`
|
|
237
|
+
| Method/Property | Description |
|
|
238
|
+
| ---------------------- | --------------------------------------------------------------- |
|
|
239
|
+
| `members` | Alias for `namelist()` |
|
|
240
|
+
| `size` | Total uncompressed size of all members |
|
|
241
|
+
| `comment` / `comment=` | Get/set archive comment (ZIP only, raises ValueError otherwise) |
|
|
224
242
|
|
|
225
243
|
## Error Handling
|
|
226
244
|
|
|
227
|
-
```
|
|
245
|
+
```py
|
|
228
246
|
from multiarchive import Archive, BadArchiveError
|
|
229
247
|
|
|
230
248
|
try:
|
|
@@ -246,7 +264,7 @@ except FileNotFoundError:
|
|
|
246
264
|
|
|
247
265
|
### `Archive`
|
|
248
266
|
|
|
249
|
-
```
|
|
267
|
+
```py
|
|
250
268
|
Archive(
|
|
251
269
|
filename: str | Path,
|
|
252
270
|
mode: str = "r",
|
|
@@ -281,7 +299,7 @@ Raised when an archive file is corrupt or in an unsupported format.
|
|
|
281
299
|
|
|
282
300
|
Frozen dataclass mapping format names to their common file extensions:
|
|
283
301
|
|
|
284
|
-
```
|
|
302
|
+
```py
|
|
285
303
|
ArchiveExtensions.zip # (".zip",)
|
|
286
304
|
ArchiveExtensions.gz # (".tgz", ".tar.gz")
|
|
287
305
|
ArchiveExtensions.xz # (".tar.xz", ".tar.lzma")
|
|
@@ -22,7 +22,7 @@ uv add "backports-zstd; python_version < '3.14'"
|
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
25
|
-
```
|
|
25
|
+
```py
|
|
26
26
|
from multiarchive import Archive
|
|
27
27
|
|
|
28
28
|
# Open and inspect
|
|
@@ -73,14 +73,14 @@ Plain compressed files expose one member named after the file without its compre
|
|
|
73
73
|
|
|
74
74
|
Use the context manager (recommended):
|
|
75
75
|
|
|
76
|
-
```
|
|
76
|
+
```py
|
|
77
77
|
with Archive("file.zip") as arc:
|
|
78
78
|
...
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Or the classmethod:
|
|
82
82
|
|
|
83
|
-
```
|
|
83
|
+
```py
|
|
84
84
|
arc = Archive.open_archive("file.zip")
|
|
85
85
|
try:
|
|
86
86
|
arc.namelist()
|
|
@@ -88,12 +88,30 @@ finally:
|
|
|
88
88
|
arc.close()
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Reading is supported on all formats
|
|
91
|
+
Reading is supported on all formats
|
|
92
92
|
If `algo` isnt explicitly provided, it will attempt to open it as ZIP first, then TAR, then RAR (if `rarfile` is installed). If all fails, it raises a ValueError.
|
|
93
93
|
|
|
94
|
+
#### Passwords
|
|
95
|
+
|
|
96
|
+
```py
|
|
97
|
+
with Archive("file.zip", pwd=b"secret") as arc:
|
|
98
|
+
...
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
classmethod
|
|
102
|
+
|
|
103
|
+
```py
|
|
104
|
+
arc = Archive.open_archive("file.zip", pwd=b"secret")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Exceptions for passwords:
|
|
108
|
+
- `ValueError`: if archive needs password but none was provided
|
|
109
|
+
- `KeyError`: if provided password is incorrect
|
|
110
|
+
- `TypeError`: if password is provided for a tarball (not supported)
|
|
111
|
+
|
|
94
112
|
### Listing Members
|
|
95
113
|
|
|
96
|
-
```
|
|
114
|
+
```py
|
|
97
115
|
with Archive("file.zip") as arc:
|
|
98
116
|
names = arc.namelist() # Just names
|
|
99
117
|
names = arc.members # alias to namelist
|
|
@@ -105,7 +123,7 @@ with Archive("file.zip") as arc:
|
|
|
105
123
|
|
|
106
124
|
### Checking Membership
|
|
107
125
|
|
|
108
|
-
```
|
|
126
|
+
```py
|
|
109
127
|
with Archive("file.zip") as arc:
|
|
110
128
|
if "config.json" in arc:
|
|
111
129
|
print("Found!")
|
|
@@ -119,7 +137,7 @@ with Archive("file.zip") as arc:
|
|
|
119
137
|
|
|
120
138
|
### Extracting
|
|
121
139
|
|
|
122
|
-
```
|
|
140
|
+
```py
|
|
123
141
|
with Archive("file.tar.gz") as arc:
|
|
124
142
|
# Single file
|
|
125
143
|
arc.extract("src/main.py", path="./extracted")
|
|
@@ -127,7 +145,7 @@ with Archive("file.tar.gz") as arc:
|
|
|
127
145
|
|
|
128
146
|
### Reading Member Contents
|
|
129
147
|
|
|
130
|
-
```
|
|
148
|
+
```py
|
|
131
149
|
with Archive("file.zip") as arc:
|
|
132
150
|
f = arc.open("data.json")
|
|
133
151
|
if f is not None:
|
|
@@ -139,7 +157,7 @@ with Archive("file.zip") as arc:
|
|
|
139
157
|
|
|
140
158
|
### Creating a New Archive
|
|
141
159
|
|
|
142
|
-
```
|
|
160
|
+
```py
|
|
143
161
|
from multiarchive import Archive
|
|
144
162
|
|
|
145
163
|
# ZIP
|
|
@@ -153,7 +171,7 @@ with Archive("output.tar.gz", mode="w", compression_level=9) as arc:
|
|
|
153
171
|
|
|
154
172
|
Add members with an optional name to use inside the archive:
|
|
155
173
|
|
|
156
|
-
```
|
|
174
|
+
```py
|
|
157
175
|
with Archive("output.zip", mode="w") as arc:
|
|
158
176
|
arc.add("src/main.py", arcname="main.py")
|
|
159
177
|
```
|
|
@@ -161,14 +179,14 @@ with Archive("output.zip", mode="w") as arc:
|
|
|
161
179
|
ZIP timestamps are limited to the years 1980 through 2107. To clamp timestamps
|
|
162
180
|
outside that range instead of raising an error:
|
|
163
181
|
|
|
164
|
-
```
|
|
182
|
+
```py
|
|
165
183
|
with Archive("output.zip", mode="w", strict_timestamps=False) as arc:
|
|
166
184
|
arc.add("old-file.txt")
|
|
167
185
|
```
|
|
168
186
|
|
|
169
187
|
The format is determined by file extension. Use the `algo` parameter to override:
|
|
170
188
|
|
|
171
|
-
```
|
|
189
|
+
```py
|
|
172
190
|
with Archive("my_archive", mode="w", algo="tar.xz") as arc:
|
|
173
191
|
...
|
|
174
192
|
```
|
|
@@ -187,19 +205,19 @@ with Archive("my_archive", mode="w", algo="tar.xz") as arc:
|
|
|
187
205
|
|
|
188
206
|
The `infolist()` method returns `ArchiveMemberInfo` objects with a unified interface across all formats:
|
|
189
207
|
|
|
190
|
-
| Field | Type
|
|
191
|
-
| ------------------- |
|
|
192
|
-
| `name` | `str`
|
|
193
|
-
| `uncompressed_size` | `int`
|
|
194
|
-
| `compressed_size` | `int \| None`
|
|
195
|
-
| `mtime` | `float`
|
|
196
|
-
| `mode` | `int \| None`
|
|
197
|
-
| `is_dir` | `bool`
|
|
208
|
+
| Field | Type | Description |
|
|
209
|
+
| ------------------- | --------------------------------------- | ----------------------------------------------------------- |
|
|
210
|
+
| `name` | `str` | Member filename |
|
|
211
|
+
| `uncompressed_size` | `int` | Original file size in bytes |
|
|
212
|
+
| `compressed_size` | `int \| None` | Compressed size (`None` for TAR) |
|
|
213
|
+
| `mtime` | `float` | Modification time as epoch seconds |
|
|
214
|
+
| `mode` | `int \| None` | Unix permission bits |
|
|
215
|
+
| `is_dir` | `bool` | Whether the member is a directory |
|
|
198
216
|
| `raw` | `ZipInfo \| TarInfo \| RarInfo \| None` | Original backend object (`None` for plain compressed files) |
|
|
199
217
|
|
|
200
218
|
Factory methods for direct conversion (you shouldn't need these in normal usage):
|
|
201
219
|
|
|
202
|
-
```
|
|
220
|
+
```py
|
|
203
221
|
info = ArchiveMemberInfo.from_zipinfo(zip_info)
|
|
204
222
|
info = ArchiveMemberInfo.from_tarinfo(tar_info)
|
|
205
223
|
info = ArchiveMemberInfo.from_rarinfo(rar_info)
|
|
@@ -207,15 +225,15 @@ info = ArchiveMemberInfo.from_rarinfo(rar_info)
|
|
|
207
225
|
|
|
208
226
|
## Properties
|
|
209
227
|
|
|
210
|
-
| Method/Property
|
|
211
|
-
|
|
|
212
|
-
| `members`
|
|
213
|
-
| `size`
|
|
214
|
-
| `comment` / `comment=`
|
|
228
|
+
| Method/Property | Description |
|
|
229
|
+
| ---------------------- | --------------------------------------------------------------- |
|
|
230
|
+
| `members` | Alias for `namelist()` |
|
|
231
|
+
| `size` | Total uncompressed size of all members |
|
|
232
|
+
| `comment` / `comment=` | Get/set archive comment (ZIP only, raises ValueError otherwise) |
|
|
215
233
|
|
|
216
234
|
## Error Handling
|
|
217
235
|
|
|
218
|
-
```
|
|
236
|
+
```py
|
|
219
237
|
from multiarchive import Archive, BadArchiveError
|
|
220
238
|
|
|
221
239
|
try:
|
|
@@ -237,7 +255,7 @@ except FileNotFoundError:
|
|
|
237
255
|
|
|
238
256
|
### `Archive`
|
|
239
257
|
|
|
240
|
-
```
|
|
258
|
+
```py
|
|
241
259
|
Archive(
|
|
242
260
|
filename: str | Path,
|
|
243
261
|
mode: str = "r",
|
|
@@ -272,7 +290,7 @@ Raised when an archive file is corrupt or in an unsupported format.
|
|
|
272
290
|
|
|
273
291
|
Frozen dataclass mapping format names to their common file extensions:
|
|
274
292
|
|
|
275
|
-
```
|
|
293
|
+
```py
|
|
276
294
|
ArchiveExtensions.zip # (".zip",)
|
|
277
295
|
ArchiveExtensions.gz # (".tgz", ".tar.gz")
|
|
278
296
|
ArchiveExtensions.xz # (".tar.xz", ".tar.lzma")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "multiarchive"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "A high level archive handler for Python."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -15,13 +15,13 @@ build-backend = "uv_build"
|
|
|
15
15
|
|
|
16
16
|
[dependency-groups]
|
|
17
17
|
dev = [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
"backports-zstd>=1.3.0 ; python_full_version < '3.14'",
|
|
19
|
+
"poethepoet>=0.45.0",
|
|
20
|
+
"pytest>=9.0.3",
|
|
21
|
+
"pytest-xdist>=3.8.0",
|
|
22
|
+
"rarfile>=4.2",
|
|
23
|
+
"ruff>=0.15.12",
|
|
24
|
+
"ty==0.0.32",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[tool.poe.tasks]
|
|
@@ -166,6 +166,7 @@ class Archive:
|
|
|
166
166
|
mode: str = "r",
|
|
167
167
|
compression_level: int | None = None,
|
|
168
168
|
*,
|
|
169
|
+
pwd: bytes | str | None = None,
|
|
169
170
|
strict_timestamps: bool = True,
|
|
170
171
|
) -> None:
|
|
171
172
|
"""Initialize the archive handler.
|
|
@@ -176,6 +177,7 @@ class Archive:
|
|
|
176
177
|
mode: File access mode ('r' for read, 'w' for write, 'a' for append)
|
|
177
178
|
compression_level: Compression level (ZIP: 0-9, TAR gzip: 0-9, TAR bzip2: 1-9)
|
|
178
179
|
If None, uses default compression
|
|
180
|
+
pwd: Password for reading an encrypted ZIP file
|
|
179
181
|
strict_timestamps: If True, reject timestamps outside the range supported
|
|
180
182
|
by ZIP; if False, clamp them to the minimum or maximum supported value
|
|
181
183
|
|
|
@@ -185,6 +187,7 @@ class Archive:
|
|
|
185
187
|
self.filename = str(filename)
|
|
186
188
|
self.mode = mode
|
|
187
189
|
self.compression_level = compression_level
|
|
190
|
+
self.pwd = pwd
|
|
188
191
|
self.algo = algo
|
|
189
192
|
self._archive: ArchiveType | IO[bytes] | None = None
|
|
190
193
|
self._archive_type: (
|
|
@@ -201,6 +204,7 @@ class Archive:
|
|
|
201
204
|
compression_level: int | None = None,
|
|
202
205
|
*,
|
|
203
206
|
algo: Algos | None = None,
|
|
207
|
+
pwd: bytes | None = None,
|
|
204
208
|
strict_timestamps: bool = True,
|
|
205
209
|
) -> "Archive":
|
|
206
210
|
"""Create and open an archive without using a context manager.
|
|
@@ -212,6 +216,7 @@ class Archive:
|
|
|
212
216
|
algo: Optional override for archive type based on file extension
|
|
213
217
|
mode: File access mode ('r' for read, 'w' for write, 'a' for append)
|
|
214
218
|
compression_level: Compression level (ZIP: 0-9, TAR gzip: 0-9, TAR bzip2: 1-9)
|
|
219
|
+
pwd: Password for reading an encrypted ZIP file
|
|
215
220
|
strict_timestamps: If True, reject timestamps outside the range supported
|
|
216
221
|
by ZIP; if False, clamp them to the minimum or maximum supported value
|
|
217
222
|
|
|
@@ -234,6 +239,7 @@ class Archive:
|
|
|
234
239
|
algo=algo,
|
|
235
240
|
mode=mode,
|
|
236
241
|
compression_level=compression_level,
|
|
242
|
+
pwd=pwd,
|
|
237
243
|
strict_timestamps=strict_timestamps,
|
|
238
244
|
)
|
|
239
245
|
archive._detect_and_open()
|
|
@@ -283,7 +289,7 @@ class Archive:
|
|
|
283
289
|
self._detect_and_open_read()
|
|
284
290
|
else:
|
|
285
291
|
self._detect_and_open_write()
|
|
286
|
-
except
|
|
292
|
+
except BadArchive + (EOFError,) as exc:
|
|
287
293
|
raise BadArchiveError(f"Failed to open archive. {exc}") from exc
|
|
288
294
|
except (FileNotFoundError, ValueError):
|
|
289
295
|
raise
|
|
@@ -296,8 +302,9 @@ class Archive:
|
|
|
296
302
|
|
|
297
303
|
Raises:
|
|
298
304
|
FileNotFoundError: If the archive file doesn't exist
|
|
299
|
-
NotImplementedError: If the archive is password-protected (for ZIP and RAR)
|
|
300
305
|
ValueError: If the file is not a valid ZIP, RAR, or TAR archive
|
|
306
|
+
KeyError: If the provided password is incorrect for an encrypted ZIP file
|
|
307
|
+
TypeError: If a password is provided for a TAR file (not supported)
|
|
301
308
|
BadArchiveError: If a compressed file has invalid contents
|
|
302
309
|
RuntimeError: If the file is not readable or is not a file
|
|
303
310
|
""" # noqa: DOC502
|
|
@@ -310,12 +317,22 @@ class Archive:
|
|
|
310
317
|
# Try ZIP first
|
|
311
318
|
try:
|
|
312
319
|
archive = zipfile.ZipFile(self.filename, "r")
|
|
313
|
-
|
|
314
|
-
if
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"Password
|
|
320
|
+
encrypted = [zinfo for zinfo in archive.infolist() if zinfo.flag_bits & 0x1]
|
|
321
|
+
if encrypted:
|
|
322
|
+
if self.pwd is None:
|
|
323
|
+
archive.close()
|
|
324
|
+
raise ValueError("Password required for encrypted ZIP file")
|
|
325
|
+
archive.setpassword(
|
|
326
|
+
self.pwd if type(self.pwd) is bytes else self.pwd.encode()
|
|
318
327
|
)
|
|
328
|
+
try:
|
|
329
|
+
for zinfo in encrypted:
|
|
330
|
+
with archive.open(zinfo) as member:
|
|
331
|
+
member.read(1)
|
|
332
|
+
break
|
|
333
|
+
except RuntimeError as exc:
|
|
334
|
+
archive.close()
|
|
335
|
+
raise KeyError("Invalid password for encrypted ZIP file") from exc
|
|
319
336
|
self._archive = archive
|
|
320
337
|
self._archive_type = "zip"
|
|
321
338
|
return
|
|
@@ -327,10 +344,22 @@ class Archive:
|
|
|
327
344
|
archive = rarfile.RarFile(self.filename, "r")
|
|
328
345
|
# Check for password protection
|
|
329
346
|
if archive.needs_password():
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"Password
|
|
347
|
+
if self.pwd is None:
|
|
348
|
+
archive.close()
|
|
349
|
+
raise ValueError("Password required for encrypted RAR file")
|
|
350
|
+
archive.setpassword(
|
|
351
|
+
self.pwd.decode() if type(self.pwd) is bytes else self.pwd
|
|
333
352
|
)
|
|
353
|
+
try:
|
|
354
|
+
for rarinfo in archive.infolist():
|
|
355
|
+
if rarinfo.needs_password():
|
|
356
|
+
with archive.open(rarinfo) as member:
|
|
357
|
+
member.read(1)
|
|
358
|
+
break
|
|
359
|
+
# might be too broad tbh
|
|
360
|
+
except rarfile.Error as exc:
|
|
361
|
+
archive.close()
|
|
362
|
+
raise KeyError("Invalid password for encrypted RAR file") from exc
|
|
334
363
|
self._archive = archive
|
|
335
364
|
self._archive_type = "rar"
|
|
336
365
|
return
|
|
@@ -340,6 +369,10 @@ class Archive:
|
|
|
340
369
|
# Try TAR (with auto-detection for compression)
|
|
341
370
|
try:
|
|
342
371
|
self._archive = tarfile.open(self.filename, "r:*") # noqa: SIM115
|
|
372
|
+
if self.pwd is not None:
|
|
373
|
+
self._archive.close()
|
|
374
|
+
self._archive = None
|
|
375
|
+
raise TypeError("Passwords are not supported for TAR files")
|
|
343
376
|
self._archive_type = "tar"
|
|
344
377
|
return
|
|
345
378
|
except tarfile.TarError:
|
|
@@ -374,6 +407,9 @@ class Archive:
|
|
|
374
407
|
Raises:
|
|
375
408
|
ValueError: If file extension is not recognized or compression_level is invalid
|
|
376
409
|
"""
|
|
410
|
+
if self.pwd is not None:
|
|
411
|
+
raise ValueError("Passwords are only supported when reading ZIP files")
|
|
412
|
+
|
|
377
413
|
filename_lower = self.filename.lower()
|
|
378
414
|
|
|
379
415
|
if (
|
|
File without changes
|