tretool 0.2.1__py3-none-any.whl → 1.0.0__py3-none-any.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.
tretool/ziplib.py ADDED
@@ -0,0 +1,664 @@
1
+ """
2
+ secure_zip.py - 支持加密的ZIP文件操作库
3
+
4
+ 功能:
5
+ 1. 支持AES加密(128/192/256位)和传统PKWARE加密
6
+ 2. 创建/解压加密ZIP文件
7
+ 3. 查看加密ZIP内容
8
+ 4. 完整性检查
9
+ """
10
+
11
+ import os
12
+ import struct
13
+ import zlib
14
+ import shutil
15
+ import hashlib
16
+ import random
17
+ from typing import List, Union, Optional, Dict, BinaryIO, Tuple
18
+ from pathlib import Path
19
+ from dataclasses import dataclass
20
+ from datetime import datetime
21
+ from Crypto.Cipher import AES
22
+ from Crypto.Util.Padding import pad, unpad
23
+ from Crypto.Random import get_random_bytes
24
+
25
+ # ZIP文件格式常量
26
+ ZIP_SIGNATURE = b'PK\x03\x04'
27
+ ZIP_CENTRAL_DIR_SIGNATURE = b'PK\x01\x02'
28
+ ZIP_END_OF_CENTRAL_DIR_SIGNATURE = b'PK\x05\x06'
29
+
30
+ # 加密相关常量
31
+ COMPRESSION_STORED = 0
32
+ COMPRESSION_DEFLATED = 8
33
+ ENCRYPTION_TRADITIONAL = 0x01
34
+ ENCRYPTION_AES128 = 0x02
35
+ ENCRYPTION_AES192 = 0x03
36
+ ENCRYPTION_AES256 = 0x04
37
+ AES_BLOCK_SIZE = 16
38
+ AES_SALT_SIZE = 16
39
+ AES_PASSWORD_VERIFIER_SIZE = 2
40
+ AES_MAC_SIZE = 10
41
+ AES_KEY_LENGTHS = {
42
+ ENCRYPTION_AES128: 16,
43
+ ENCRYPTION_AES192: 24,
44
+ ENCRYPTION_AES256: 32
45
+ }
46
+
47
+ @dataclass
48
+ class ZipFileHeader:
49
+ version: int
50
+ flags: int
51
+ compression: int
52
+ mod_time: int
53
+ mod_date: int
54
+ crc32: int
55
+ compressed_size: int
56
+ uncompressed_size: int
57
+ filename: str
58
+ extra: bytes
59
+ file_offset: int
60
+ is_encrypted: bool = False
61
+ encryption_method: int = 0
62
+ aes_strength: int = 0
63
+ salt: bytes = b''
64
+ password_verifier: bytes = b''
65
+
66
+ class SecureZipFile:
67
+ def __init__(self, filename: Union[str, Path], mode: str = 'r', password: Optional[str] = None):
68
+ """
69
+ 初始化加密ZIP文件对象
70
+
71
+ 参数:
72
+ filename: ZIP文件路径
73
+ mode: 打开模式 ('r'读取, 'w'写入, 'a'追加)
74
+ password: 加密密码(可选)
75
+ """
76
+ self.filename = Path(filename)
77
+ self.mode = mode
78
+ self.password = password
79
+ self.file_headers: Dict[str, ZipFileHeader] = {}
80
+ self.fp: Optional[BinaryIO] = None
81
+
82
+ if mode == 'r':
83
+ self._read_zip_file()
84
+ elif mode == 'w':
85
+ self.fp = open(self.filename, 'wb')
86
+ elif mode == 'a':
87
+ if self.filename.exists():
88
+ self._read_zip_file()
89
+ self.fp = open(self.filename, 'r+b')
90
+ # 定位到中央目录前
91
+ self.fp.seek(self.end_of_central_dir_offset)
92
+ else:
93
+ self.fp = open(self.filename, 'wb')
94
+ else:
95
+ raise ValueError("Invalid mode, must be 'r', 'w' or 'a'")
96
+
97
+ def _read_zip_file(self):
98
+ """读取ZIP文件并解析文件头信息"""
99
+ if not self.filename.exists():
100
+ raise FileNotFoundError(f"ZIP file not found: {self.filename}")
101
+
102
+ self.fp = open(self.filename, 'rb')
103
+ self._find_end_of_central_dir()
104
+ self._read_central_directory()
105
+
106
+ def _find_end_of_central_dir(self):
107
+ """定位并读取ZIP文件尾部的中央目录结束记录"""
108
+ file_size = self.filename.stat().st_size
109
+ max_comment_len = 65535
110
+ search_size = min(file_size, max_comment_len + 22)
111
+
112
+ self.fp.seek(file_size - search_size)
113
+ data = self.fp.read()
114
+
115
+ pos = data.rfind(ZIP_END_OF_CENTRAL_DIR_SIGNATURE)
116
+ if pos < 0:
117
+ raise ValueError("Not a valid ZIP file (end of central directory signature not found)")
118
+
119
+ end_record = data[pos:pos+22]
120
+ (
121
+ self.disk_number,
122
+ self.central_dir_disk,
123
+ self.disk_entries,
124
+ self.total_entries,
125
+ self.central_dir_size,
126
+ self.central_dir_offset,
127
+ self.comment_length
128
+ ) = struct.unpack('<HHHHIIH', end_record[4:22])
129
+
130
+ self.end_of_central_dir_offset = file_size - (search_size - pos)
131
+
132
+ def _read_central_directory(self):
133
+ """读取中央目录并解析文件头信息"""
134
+ self.fp.seek(self.central_dir_offset)
135
+
136
+ while True:
137
+ signature = self.fp.read(4)
138
+ if signature != ZIP_CENTRAL_DIR_SIGNATURE:
139
+ break
140
+
141
+ header_data = self.fp.read(42)
142
+ (
143
+ version_made_by, version_needed, flags, compression,
144
+ mod_time, mod_date, crc32, compressed_size, uncompressed_size,
145
+ filename_len, extra_len, comment_len, disk_num_start,
146
+ internal_attrs, external_attrs, local_header_offset
147
+ ) = struct.unpack('<HHHHHHIIIHHHHHII', header_data)
148
+
149
+ filename = self.fp.read(filename_len).decode('utf-8')
150
+ extra = self.fp.read(extra_len)
151
+ comment = self.fp.read(comment_len)
152
+
153
+ is_encrypted = (flags & 0x1) != 0
154
+ encryption_method = 0
155
+ aes_strength = 0
156
+ salt = b''
157
+ password_verifier = b''
158
+
159
+ if is_encrypted:
160
+ # 检查AES加密标志
161
+ if (flags & 0x40) != 0:
162
+ # AES加密
163
+ encryption_method = (flags >> 8) & 0xFF
164
+ aes_strength = AES_KEY_LENGTHS.get(encryption_method, 0)
165
+
166
+ # 从额外字段中读取salt和验证器
167
+ extra_pos = 0
168
+ while extra_pos < len(extra):
169
+ header_id, data_size = struct.unpack_from('<HH', extra, extra_pos)
170
+ extra_pos += 4
171
+ if header_id == 0x9901: # AE-x ID
172
+ aes_data = extra[extra_pos:extra_pos+data_size]
173
+ if len(aes_data) >= 7:
174
+ aes_version, vendor_id, strength = struct.unpack_from('<HBB', aes_data, 0)
175
+ salt = aes_data[7:7+AES_SALT_SIZE]
176
+ password_verifier = aes_data[7+AES_SALT_SIZE:7+AES_SALT_SIZE+AES_PASSWORD_VERIFIER_SIZE]
177
+ break
178
+ extra_pos += data_size
179
+ else:
180
+ # 传统PKWARE加密
181
+ encryption_method = ENCRYPTION_TRADITIONAL
182
+
183
+ # 保存文件头信息
184
+ self.file_headers[filename] = ZipFileHeader(
185
+ version=version_needed,
186
+ flags=flags,
187
+ compression=compression,
188
+ mod_time=mod_time,
189
+ mod_date=mod_date,
190
+ crc32=crc32,
191
+ compressed_size=compressed_size,
192
+ uncompressed_size=uncompressed_size,
193
+ filename=filename,
194
+ extra=extra,
195
+ file_offset=local_header_offset,
196
+ is_encrypted=is_encrypted,
197
+ encryption_method=encryption_method,
198
+ aes_strength=aes_strength,
199
+ salt=salt,
200
+ password_verifier=password_verifier
201
+ )
202
+
203
+ def close(self):
204
+ """关闭ZIP文件"""
205
+ if self.fp is not None:
206
+ if self.mode in ('w', 'a'):
207
+ self._write_central_directory()
208
+ self.fp.close()
209
+ self.fp = None
210
+
211
+ def __enter__(self):
212
+ return self
213
+
214
+ def __exit__(self, exc_type, exc_val, exc_tb):
215
+ self.close()
216
+
217
+ def _generate_aes_key(self, salt: bytes, key_length: int) -> Tuple[bytes, bytes]:
218
+ """生成AES密钥和验证器"""
219
+ if not self.password:
220
+ raise ValueError("Password is required for AES encryption")
221
+
222
+ # 使用PBKDF2生成密钥
223
+ key = hashlib.pbkdf2_hmac(
224
+ 'sha1',
225
+ self.password.encode('utf-8'),
226
+ salt,
227
+ 1000, # 迭代次数
228
+ key_length * 2 + 2 # 密钥+MAC密钥+验证器
229
+ )
230
+
231
+ encryption_key = key[:key_length]
232
+ mac_key = key[key_length:key_length*2]
233
+ password_verifier = key[key_length*2:key_length*2+2]
234
+
235
+ return encryption_key, mac_key, password_verifier
236
+
237
+ def _traditional_encrypt(self, data: bytes) -> bytes:
238
+ """传统PKWARE加密"""
239
+ if not self.password:
240
+ raise ValueError("Password is required for traditional encryption")
241
+
242
+ # 初始化加密密钥
243
+ keys = [0x12345678, 0x23456789, 0x34567890]
244
+ for c in self.password.encode('utf-8'):
245
+ keys = self._update_keys(keys, c)
246
+
247
+ # 加密数据
248
+ encrypted_data = bytearray()
249
+ for i, b in enumerate(data):
250
+ c = b ^ self._crc32_crypt_byte(keys[2])
251
+ encrypted_data.append(c)
252
+ keys = self._update_keys(keys, c)
253
+
254
+ return bytes(encrypted_data)
255
+
256
+ def _traditional_decrypt(self, data: bytes) -> bytes:
257
+ """传统PKWARE解密"""
258
+ if not self.password:
259
+ raise ValueError("Password is required for traditional decryption")
260
+
261
+ # 初始化加密密钥
262
+ keys = [0x12345678, 0x23456789, 0x34567890]
263
+ for c in self.password.encode('utf-8'):
264
+ keys = self._update_keys(keys, c)
265
+
266
+ # 解密数据
267
+ decrypted_data = bytearray()
268
+ for i, b in enumerate(data):
269
+ c = b ^ self._crc32_crypt_byte(keys[2])
270
+ decrypted_data.append(c)
271
+ keys = self._update_keys(keys, c)
272
+
273
+ return bytes(decrypted_data)
274
+
275
+ def _update_keys(self, keys: List[int], c: int) -> List[int]:
276
+ """更新传统加密密钥"""
277
+ keys[0] = zlib.crc32(bytes([c]), keys[0]) & 0xFFFFFFFF
278
+ keys[1] = (keys[1] + (keys[0] & 0xFF)) & 0xFFFFFFFF
279
+ keys[1] = (keys[1] * 134775813 + 1) & 0xFFFFFFFF
280
+ keys[2] = zlib.crc32(bytes([keys[1] >> 24]), keys[2]) & 0xFFFFFFFF
281
+ return keys
282
+
283
+ def _crc32_crypt_byte(self, key: int) -> int:
284
+ """传统加密的字节加密函数"""
285
+ temp = (key | 2) & 0xFFFF
286
+ return ((temp * (temp ^ 1)) >> 8) & 0xFF
287
+
288
+ def _write_central_directory(self):
289
+ """写入中央目录和结束记录"""
290
+ if self.fp is None:
291
+ raise ValueError("ZIP file not open")
292
+
293
+ central_dir_start = self.fp.tell()
294
+
295
+ for header in self.file_headers.values():
296
+ self.fp.write(ZIP_CENTRAL_DIR_SIGNATURE)
297
+ self.fp.write(struct.pack(
298
+ '<HHHHHHIIIHHHHHII',
299
+ 20, # version made by
300
+ 20, # version needed to extract
301
+ header.flags,
302
+ header.compression,
303
+ header.mod_time,
304
+ header.mod_date,
305
+ header.crc32,
306
+ header.compressed_size,
307
+ header.uncompressed_size,
308
+ len(header.filename.encode('utf-8')),
309
+ len(header.extra),
310
+ 0, # file comment length
311
+ 0, # disk number start
312
+ 0, # internal file attributes
313
+ 0o644 << 16, # external file attributes
314
+ header.file_offset
315
+ ))
316
+ self.fp.write(header.filename.encode('utf-8'))
317
+ self.fp.write(header.extra)
318
+
319
+ central_dir_end = self.fp.tell()
320
+ central_dir_size = central_dir_end - central_dir_start
321
+
322
+ self.fp.write(ZIP_END_OF_CENTRAL_DIR_SIGNATURE)
323
+ self.fp.write(struct.pack(
324
+ '<HHHHIIH',
325
+ 0, # number of this disk
326
+ 0, # disk where central directory starts
327
+ len(self.file_headers),
328
+ len(self.file_headers),
329
+ central_dir_size,
330
+ central_dir_start,
331
+ 0 # ZIP file comment length
332
+ ))
333
+
334
+ def write(self, filename: str, data: bytes, compress: bool = True,
335
+ encryption_method: int = 0) -> None:
336
+ """
337
+ 向ZIP文件中写入一个文件
338
+
339
+ 参数:
340
+ filename: ZIP内的文件名
341
+ data: 文件数据
342
+ compress: 是否压缩数据
343
+ encryption_method: 加密方法 (0=不加密, 1=传统加密, 2=AES128, 3=AES192, 4=AES256)
344
+ """
345
+ if self.fp is None or self.mode not in ('w', 'a'):
346
+ raise ValueError("ZIP file not open for writing")
347
+
348
+ # 计算CRC32校验和
349
+ crc32 = zlib.crc32(data) & 0xFFFFFFFF
350
+
351
+ if compress:
352
+ compressed_data = zlib.compress(data)
353
+ compression = COMPRESSION_DEFLATED
354
+ else:
355
+ compressed_data = data
356
+ compression = COMPRESSION_STORED
357
+
358
+ # 加密数据
359
+ is_encrypted = encryption_method != 0
360
+ salt = b''
361
+ password_verifier = b''
362
+ extra = b''
363
+
364
+ if is_encrypted:
365
+ if not self.password:
366
+ raise ValueError("Password is required for encryption")
367
+
368
+ if encryption_method == ENCRYPTION_TRADITIONAL:
369
+ # 传统PKWARE加密
370
+ encrypted_data = self._traditional_encrypt(compressed_data)
371
+ flags = 0x1 # 加密标志
372
+ elif encryption_method in (ENCRYPTION_AES128, ENCRYPTION_AES192, ENCRYPTION_AES256):
373
+ # AES加密
374
+ key_length = AES_KEY_LENGTHS[encryption_method]
375
+ salt = get_random_bytes(AES_SALT_SIZE)
376
+ encryption_key, mac_key, password_verifier = self._generate_aes_key(salt, key_length)
377
+
378
+ # 创建AES加密器
379
+ cipher = AES.new(encryption_key, AES.MODE_CBC, iv=salt)
380
+ padded_data = pad(compressed_data, AES_BLOCK_SIZE)
381
+ encrypted_data = cipher.encrypt(padded_data)
382
+
383
+ # 添加HMAC-SHA1验证码(简化实现)
384
+ mac = hashlib.sha1(encrypted_data).digest()[:AES_MAC_SIZE]
385
+ encrypted_data += mac
386
+
387
+ # 设置AES额外字段
388
+ aes_extra = struct.pack('<HBB', 0x9901, 7, encryption_method - 1)
389
+ extra = aes_extra + salt + password_verifier
390
+ flags = 0x41 # 加密标志 + AES标志
391
+ else:
392
+ raise ValueError("Unsupported encryption method")
393
+ else:
394
+ encrypted_data = compressed_data
395
+ flags = 0
396
+
397
+ # 记录本地文件头位置
398
+ file_offset = self.fp.tell()
399
+
400
+ # 写入本地文件头
401
+ self.fp.write(ZIP_SIGNATURE)
402
+ self.fp.write(struct.pack(
403
+ '<HHHHHIII',
404
+ 20, # version needed to extract
405
+ flags, # general purpose bit flag
406
+ compression,
407
+ 0, # last mod time (simplified)
408
+ 0, # last mod date (simplified)
409
+ crc32,
410
+ len(encrypted_data),
411
+ len(data)
412
+ ))
413
+
414
+ # 写入文件名和额外字段
415
+ self.fp.write(filename.encode('utf-8'))
416
+ if extra:
417
+ self.fp.write(extra)
418
+
419
+ # 写入加密数据
420
+ self.fp.write(encrypted_data)
421
+
422
+ # 保存文件头信息
423
+ self.file_headers[filename] = ZipFileHeader(
424
+ version=20,
425
+ flags=flags,
426
+ compression=compression,
427
+ mod_time=0,
428
+ mod_date=0,
429
+ crc32=crc32,
430
+ compressed_size=len(encrypted_data),
431
+ uncompressed_size=len(data),
432
+ filename=filename,
433
+ extra=extra,
434
+ file_offset=file_offset,
435
+ is_encrypted=is_encrypted,
436
+ encryption_method=encryption_method,
437
+ aes_strength=AES_KEY_LENGTHS.get(encryption_method, 0),
438
+ salt=salt,
439
+ password_verifier=password_verifier
440
+ )
441
+
442
+ def extract(self, member: str, path: Optional[Union[str, Path]] = None) -> None:
443
+ """
444
+ 从ZIP文件中提取一个文件
445
+
446
+ 参数:
447
+ member: 要提取的文件名
448
+ path: 提取目标路径(默认为当前目录)
449
+ """
450
+ if self.fp is None or self.mode != 'r':
451
+ raise ValueError("ZIP file not open for reading")
452
+
453
+ if member not in self.file_headers:
454
+ raise KeyError(f"File not found in ZIP: {member}")
455
+
456
+ header = self.file_headers[member]
457
+ target_path = Path(path or '.') / member
458
+
459
+ # 确保目标目录存在
460
+ target_path.parent.mkdir(parents=True, exist_ok=True)
461
+
462
+ # 定位到文件数据
463
+ self.fp.seek(header.file_offset)
464
+ signature = self.fp.read(4)
465
+ if signature != ZIP_SIGNATURE:
466
+ raise ValueError("Invalid local file header signature")
467
+
468
+ # 读取本地文件头
469
+ local_header = self.fp.read(26)
470
+ (
471
+ version, flags, compression, mod_time, mod_date,
472
+ crc32, compressed_size, uncompressed_size,
473
+ filename_len, extra_len
474
+ ) = struct.unpack('<HHHHHIIIHH', local_header)
475
+
476
+ # 跳过文件名和额外字段
477
+ filename = self.fp.read(filename_len).decode('utf-8')
478
+ extra = self.fp.read(extra_len)
479
+
480
+ # 读取加密数据
481
+ encrypted_data = self.fp.read(compressed_size)
482
+
483
+ # 解密数据
484
+ if header.is_encrypted:
485
+ if not self.password:
486
+ raise ValueError("Password is required for encrypted file")
487
+
488
+ if header.encryption_method == ENCRYPTION_TRADITIONAL:
489
+ # 传统PKWARE解密
490
+ decrypted_data = self._traditional_decrypt(encrypted_data)
491
+ elif header.encryption_method in (ENCRYPTION_AES128, ENCRYPTION_AES192, ENCRYPTION_AES256):
492
+ # AES解密
493
+ key_length = header.aes_strength
494
+ encryption_key, mac_key, password_verifier = self._generate_aes_key(header.salt, key_length)
495
+
496
+ # 验证密码
497
+ if header.password_verifier != password_verifier:
498
+ raise ValueError("Incorrect password")
499
+
500
+ # 分离数据和MAC
501
+ if len(encrypted_data) < AES_MAC_SIZE:
502
+ raise ValueError("Invalid encrypted data length")
503
+
504
+ data_part = encrypted_data[:-AES_MAC_SIZE]
505
+ mac = encrypted_data[-AES_MAC_SIZE:]
506
+
507
+ # 验证MAC(简化实现)
508
+ computed_mac = hashlib.sha1(data_part).digest()[:AES_MAC_SIZE]
509
+ if mac != computed_mac:
510
+ raise ValueError("MAC verification failed")
511
+
512
+ # 解密数据
513
+ cipher = AES.new(encryption_key, AES.MODE_CBC, iv=header.salt)
514
+ decrypted_padded_data = cipher.decrypt(data_part)
515
+ decrypted_data = unpad(decrypted_padded_data, AES_BLOCK_SIZE)
516
+ else:
517
+ raise ValueError("Unsupported encryption method")
518
+ else:
519
+ decrypted_data = encrypted_data
520
+
521
+ # 解压数据
522
+ if compression == COMPRESSION_STORED:
523
+ data = decrypted_data
524
+ elif compression == COMPRESSION_DEFLATED:
525
+ data = zlib.decompress(decrypted_data)
526
+ else:
527
+ raise ValueError(f"Unsupported compression method: {compression}")
528
+
529
+ # 验证CRC32
530
+ if zlib.crc32(data) & 0xFFFFFFFF != header.crc32:
531
+ raise ValueError("CRC32 checksum failed")
532
+
533
+ # 写入目标文件
534
+ with open(target_path, 'wb') as f:
535
+ f.write(data)
536
+
537
+ def namelist(self) -> List[str]:
538
+ """返回ZIP文件中所有文件的名称列表"""
539
+ return list(self.file_headers.keys())
540
+
541
+ def testzip(self) -> Optional[str]:
542
+ """
543
+ 测试ZIP文件中所有文件的完整性
544
+
545
+ 返回:
546
+ 第一个损坏的文件名,如果所有文件都完好则返回None
547
+ """
548
+ if self.fp is None or self.mode != 'r':
549
+ raise ValueError("ZIP file not open for reading")
550
+
551
+ for filename, header in self.file_headers.items():
552
+ try:
553
+ self.extract(filename, '/dev/null') # 尝试提取到虚拟位置
554
+ except:
555
+ return filename
556
+
557
+ return None
558
+
559
+ # 高级API函数
560
+ def create_secure_zip(
561
+ zip_path: Union[str, Path],
562
+ files_to_zip: List[Union[str, Path]],
563
+ password: Optional[str] = None,
564
+ encryption_method: int = 0,
565
+ compression: bool = True,
566
+ overwrite: bool = False
567
+ ) -> None:
568
+ """
569
+ 创建加密的ZIP文件
570
+
571
+ 参数:
572
+ zip_path: 要创建的ZIP文件路径
573
+ files_to_zip: 要压缩的文件/文件夹列表
574
+ password: 加密密码
575
+ encryption_method: 加密方法 (0=不加密, 1=传统加密, 2=AES128, 3=AES192, 4=AES256)
576
+ compression: 是否压缩数据
577
+ overwrite: 是否覆盖已存在的ZIP文件
578
+
579
+ 异常:
580
+ FileExistsError: 当ZIP文件已存在且不允许覆盖时
581
+ FileNotFoundError: 当要压缩的文件不存在时
582
+ ValueError: 当需要密码但未提供时
583
+ """
584
+ zip_path = Path(zip_path)
585
+ if zip_path.exists() and not overwrite:
586
+ raise FileExistsError(f"ZIP file already exists: {zip_path}")
587
+
588
+ with SecureZipFile(zip_path, 'w', password) as zipf:
589
+ for item in files_to_zip:
590
+ item = Path(item)
591
+ if not item.exists():
592
+ raise FileNotFoundError(f"File not found: {item}")
593
+
594
+ if item.is_file():
595
+ with open(item, 'rb') as f:
596
+ data = f.read()
597
+ zipf.write(str(item.name), data, compress=compression, encryption_method=encryption_method)
598
+ elif item.is_dir():
599
+ for root, _, files in os.walk(item):
600
+ for file in files:
601
+ file_path = Path(root) / file
602
+ rel_path = str(file_path.relative_to(item.parent))
603
+ with open(file_path, 'rb') as f:
604
+ data = f.read()
605
+ zipf.write(rel_path, data, compress=compression, encryption_method=encryption_method)
606
+
607
+ def extract_secure_zip(
608
+ zip_path: Union[str, Path],
609
+ extract_to: Union[str, Path],
610
+ password: Optional[str] = None,
611
+ members: Optional[List[str]] = None,
612
+ overwrite: bool = False
613
+ ) -> None:
614
+ """
615
+ 解压加密的ZIP文件
616
+
617
+ 参数:
618
+ zip_path: ZIP文件路径
619
+ extract_to: 解压目标目录
620
+ password: 解密密码
621
+ members: 可选,只解压指定的文件
622
+ overwrite: 是否覆盖已存在的文件
623
+
624
+ 异常:
625
+ FileNotFoundError: 当ZIP文件不存在时
626
+ ValueError: 当ZIP文件损坏或密码错误时
627
+ """
628
+ zip_path = Path(zip_path)
629
+ extract_to = Path(extract_to)
630
+
631
+ if not zip_path.exists():
632
+ raise FileNotFoundError(f"ZIP file not found: {zip_path}")
633
+
634
+ if not overwrite:
635
+ # 检查是否会覆盖已有文件
636
+ with SecureZipFile(zip_path, 'r', password) as zipf:
637
+ for member in members or zipf.namelist():
638
+ dest = extract_to / member
639
+ if dest.exists():
640
+ raise FileExistsError(f"File exists and overwrite=False: {dest}")
641
+
642
+ with SecureZipFile(zip_path, 'r', password) as zipf:
643
+ for member in members or zipf.namelist():
644
+ zipf.extract(member, extract_to)
645
+
646
+ def is_encrypted_zip(zip_path: Union[str, Path]) -> bool:
647
+ """
648
+ 检查ZIP文件是否加密
649
+
650
+ 参数:
651
+ zip_path: ZIP文件路径
652
+
653
+ 返回:
654
+ 如果ZIP文件包含加密文件则返回True,否则返回False
655
+
656
+ 异常:
657
+ FileNotFoundError: 当ZIP文件不存在时
658
+ """
659
+ zip_path = Path(zip_path)
660
+ if not zip_path.exists():
661
+ raise FileNotFoundError(f"ZIP file not found: {zip_path}")
662
+
663
+ with SecureZipFile(zip_path, 'r') as zipf:
664
+ return any(header.is_encrypted for header in zipf.file_headers.values())
@@ -1,23 +1,27 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tretool
3
- Version: 0.2.1
3
+ Version: 1.0.0
4
4
  Summary: 一个有着许多功能的Python工具库
5
5
  Author-email: Jemy <sh_ljr_2013@163.com>
6
6
  License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/pypa/sampleproject
8
- Project-URL: Issues, https://github.com/pypa/sampleproject/issues
7
+ Project-URL: Homepage, https://github.com/jemy/sampleproject
8
+ Project-URL: Issues, https://github.com/jemy/sampleproject/issues
9
+ Keywords: tool,tools,utility,utilities,productivity
9
10
  Classifier: Programming Language :: Python :: 3
10
11
  Classifier: Operating System :: OS Independent
11
12
  Requires-Python: >=3.10
12
13
  Description-Content-Type: text/markdown
13
14
  License-File: LICENSE
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == "dev"
17
+ Requires-Dist: pipreqs; extra == "dev"
14
18
  Dynamic: license-file
15
19
 
16
20
  # tretool
17
21
 
18
22
  ## tretool - Python多功能工具库
19
23
 
20
- [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/)
24
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
21
25
 
22
26
  **tretool** 是一个集成常用功能的Python工具库。
23
27
 
@@ -25,4 +29,6 @@ Dynamic: license-file
25
29
 
26
30
  ### 使用pip
27
31
  bash
28
- ```pip install tretool
32
+ ```
33
+ pip install tretool
34
+ ```