Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rarfile.py: 34%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

2169 statements  

1# rarfile.py 

2# 

3# Copyright (c) 2005-2026 Marko Kreen <markokr@gmail.com> 

4# 

5# Permission to use, copy, modify, and/or distribute this software for any 

6# purpose with or without fee is hereby granted, provided that the above 

7# copyright notice and this permission notice appear in all copies. 

8# 

9# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 

10# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 

11# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 

12# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 

13# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 

14# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 

15# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 

16 

17"""RAR archive reader. 

18 

19This is Python module for Rar archive reading. The interface 

20is made as :mod:`zipfile`-like as possible. 

21 

22Basic logic: 

23 - Parse archive structure with Python. 

24 - Extract non-compressed files with Python 

25 - Extract compressed files with unrar. 

26 - Optionally write compressed data to temp file to speed up unrar, 

27 otherwise it needs to scan whole archive on each execution. 

28 

29Example:: 

30 

31 import rarfile 

32 

33 rf = rarfile.RarFile("myarchive.rar") 

34 for f in rf.infolist(): 

35 print(f.filename, f.file_size) 

36 if f.filename == "README": 

37 print(rf.read(f)) 

38 

39Archive files can also be accessed via file-like object returned 

40by :meth:`RarFile.open`:: 

41 

42 import rarfile 

43 

44 with rarfile.RarFile("archive.rar") as rf: 

45 with rf.open("README") as f: 

46 for ln in f: 

47 print(ln.strip()) 

48 

49For decompression to work, either ``unrar`` or ``unar`` tool must be in PATH. 

50""" 

51 

52import errno 

53import io 

54import os 

55import re 

56import shutil 

57import struct 

58import sys 

59import warnings 

60from binascii import crc32, hexlify 

61from datetime import datetime, timezone 

62from hashlib import blake2s, pbkdf2_hmac, sha1, sha256 

63from pathlib import Path 

64from struct import Struct, pack, unpack 

65from subprocess import DEVNULL, PIPE, STDOUT, Popen 

66from tempfile import mkstemp 

67 

68AES = None 

69 

70# only needed for encrypted headers 

71try: 

72 try: 

73 from cryptography.hazmat.backends import default_backend 

74 from cryptography.hazmat.primitives.ciphers import ( 

75 Cipher, algorithms, modes, 

76 ) 

77 _have_crypto = 1 

78 except ImportError: 

79 from Crypto.Cipher import AES 

80 _have_crypto = 2 

81except ImportError: 

82 _have_crypto = 0 

83 

84 

85class AES_CBC_Decrypt: 

86 """Decrypt API""" 

87 def __init__(self, key, iv): 

88 if _have_crypto == 2: 

89 self.decrypt = AES.new(key, AES.MODE_CBC, iv).decrypt 

90 else: 

91 ciph = Cipher(algorithms.AES(key), modes.CBC(iv), default_backend()) 

92 self.decrypt = ciph.decryptor().update 

93 

94 

95__version__ = "4.5" 

96 

97# export only interesting items 

98__all__ = ["get_rar_version", "is_rarfile", "is_rarfile_sfx", "RarInfo", "RarFile", "RarExtFile"] 

99 

100## 

101## Module configuration. Can be tuned after importing. 

102## 

103 

104#: executable for unrar tool 

105UNRAR_TOOL = "unrar" 

106 

107#: executable for unar tool 

108UNAR_TOOL = "unar" 

109 

110#: executable for bsdtar tool 

111BSDTAR_TOOL = "bsdtar" 

112 

113#: executable for p7zip/7z tool 

114SEVENZIP_TOOL = "7z" 

115 

116#: executable for alternative 7z tool 

117SEVENZIP2_TOOL = "7zz" 

118 

119#: default fallback charset 

120DEFAULT_CHARSET = "windows-1252" 

121 

122#: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed 

123TRY_ENCODINGS = ("utf8", "utf-16le") 

124 

125#: whether to speed up decompression by using tmp archive 

126USE_EXTRACT_HACK = 1 

127 

128#: limit the filesize for tmp archive usage 

129HACK_SIZE_LIMIT = 20 * 1024 * 1024 

130 

131#: set specific directory for mkstemp() used by hack dir usage 

132HACK_TMP_DIR = None 

133 

134#: Separator for path name components. Always "/". 

135PATH_SEP = "/" 

136 

137## 

138## rar constants 

139## 

140 

141# block types 

142RAR_BLOCK_MARK = 0x72 # r 

143RAR_BLOCK_MAIN = 0x73 # s 

144RAR_BLOCK_FILE = 0x74 # t 

145RAR_BLOCK_OLD_COMMENT = 0x75 # u 

146RAR_BLOCK_OLD_EXTRA = 0x76 # v 

147RAR_BLOCK_OLD_SUB = 0x77 # w 

148RAR_BLOCK_OLD_RECOVERY = 0x78 # x 

149RAR_BLOCK_OLD_AUTH = 0x79 # y 

150RAR_BLOCK_SUB = 0x7a # z 

151RAR_BLOCK_ENDARC = 0x7b # { 

152 

153# flags for RAR_BLOCK_MAIN 

154RAR_MAIN_VOLUME = 0x0001 

155RAR_MAIN_COMMENT = 0x0002 

156RAR_MAIN_LOCK = 0x0004 

157RAR_MAIN_SOLID = 0x0008 

158RAR_MAIN_NEWNUMBERING = 0x0010 

159RAR_MAIN_AUTH = 0x0020 

160RAR_MAIN_RECOVERY = 0x0040 

161RAR_MAIN_PASSWORD = 0x0080 

162RAR_MAIN_FIRSTVOLUME = 0x0100 

163RAR_MAIN_ENCRYPTVER = 0x0200 

164 

165# flags for RAR_BLOCK_FILE 

166RAR_FILE_SPLIT_BEFORE = 0x0001 

167RAR_FILE_SPLIT_AFTER = 0x0002 

168RAR_FILE_PASSWORD = 0x0004 

169RAR_FILE_COMMENT = 0x0008 

170RAR_FILE_SOLID = 0x0010 

171RAR_FILE_DICTMASK = 0x00e0 

172RAR_FILE_DICT64 = 0x0000 

173RAR_FILE_DICT128 = 0x0020 

174RAR_FILE_DICT256 = 0x0040 

175RAR_FILE_DICT512 = 0x0060 

176RAR_FILE_DICT1024 = 0x0080 

177RAR_FILE_DICT2048 = 0x00a0 

178RAR_FILE_DICT4096 = 0x00c0 

179RAR_FILE_DIRECTORY = 0x00e0 

180RAR_FILE_LARGE = 0x0100 

181RAR_FILE_UNICODE = 0x0200 

182RAR_FILE_SALT = 0x0400 

183RAR_FILE_VERSION = 0x0800 

184RAR_FILE_EXTTIME = 0x1000 

185RAR_FILE_EXTFLAGS = 0x2000 

186 

187# flags for RAR_BLOCK_ENDARC 

188RAR_ENDARC_NEXT_VOLUME = 0x0001 

189RAR_ENDARC_DATACRC = 0x0002 

190RAR_ENDARC_REVSPACE = 0x0004 

191RAR_ENDARC_VOLNR = 0x0008 

192 

193# flags common to all blocks 

194RAR_SKIP_IF_UNKNOWN = 0x4000 

195RAR_LONG_BLOCK = 0x8000 

196 

197# Subtypes for RAR_BLOCK_OLD_SUB 

198RAR_OLD_SUB_OS2 = 0x100 

199RAR_OLD_SUB_UNIX = 0x101 

200RAR_OLD_SUB_MAC = 0x102 

201RAR_OLD_SUB_BEOS = 0x103 

202RAR_OLD_SUB_NT = 0x104 

203RAR_OLD_SUB_STREAM = 0x105 

204 

205# Host OS types 

206RAR_OS_MSDOS = 0 #: MSDOS (only in RAR3) 

207RAR_OS_OS2 = 1 #: OS2 (only in RAR3) 

208RAR_OS_WIN32 = 2 #: Windows 

209RAR_OS_UNIX = 3 #: UNIX 

210RAR_OS_MACOS = 4 #: MacOS (only in RAR3) 

211RAR_OS_BEOS = 5 #: BeOS (only in RAR3) 

212 

213# Compression methods - "0".."5" 

214RAR_M0 = 0x30 #: No compression. 

215RAR_M1 = 0x31 #: Compression level `-m1` - Fastest compression. 

216RAR_M2 = 0x32 #: Compression level `-m2`. 

217RAR_M3 = 0x33 #: Compression level `-m3`. 

218RAR_M4 = 0x34 #: Compression level `-m4`. 

219RAR_M5 = 0x35 #: Compression level `-m5` - Maximum compression. 

220 

221RAR_MAX_PASSWORD = 127 #: Max number of utf-16 chars in passwords. 

222RAR_MAX_KDF_SHIFT = 24 #: Max power-of-2 for KDF count 

223RAR_MAX_COMMENT = 256 * 1024 #: Max supported comment size 

224 

225# 

226# RAR5 constants 

227# 

228 

229RAR5_BLOCK_MAIN = 1 

230RAR5_BLOCK_FILE = 2 

231RAR5_BLOCK_SERVICE = 3 

232RAR5_BLOCK_ENCRYPTION = 4 

233RAR5_BLOCK_ENDARC = 5 

234 

235RAR5_BLOCK_FLAG_EXTRA_DATA = 0x01 

236RAR5_BLOCK_FLAG_DATA_AREA = 0x02 

237RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN = 0x04 

238RAR5_BLOCK_FLAG_SPLIT_BEFORE = 0x08 

239RAR5_BLOCK_FLAG_SPLIT_AFTER = 0x10 

240RAR5_BLOCK_FLAG_DEPENDS_PREV = 0x20 

241RAR5_BLOCK_FLAG_KEEP_WITH_PARENT = 0x40 

242 

243RAR5_MAIN_FLAG_ISVOL = 0x01 

244RAR5_MAIN_FLAG_HAS_VOLNR = 0x02 

245RAR5_MAIN_FLAG_SOLID = 0x04 

246RAR5_MAIN_FLAG_RECOVERY = 0x08 

247RAR5_MAIN_FLAG_LOCKED = 0x10 

248 

249RAR5_FILE_FLAG_ISDIR = 0x01 

250RAR5_FILE_FLAG_HAS_MTIME = 0x02 

251RAR5_FILE_FLAG_HAS_CRC32 = 0x04 

252RAR5_FILE_FLAG_UNKNOWN_SIZE = 0x08 

253 

254RAR5_COMPR_SOLID = 0x40 

255 

256RAR5_ENC_FLAG_HAS_CHECKVAL = 0x01 

257 

258RAR5_ENDARC_FLAG_NEXT_VOL = 0x01 

259 

260RAR5_XFILE_ENCRYPTION = 1 

261RAR5_XFILE_HASH = 2 

262RAR5_XFILE_TIME = 3 

263RAR5_XFILE_VERSION = 4 

264RAR5_XFILE_REDIR = 5 

265RAR5_XFILE_OWNER = 6 

266RAR5_XFILE_SERVICE = 7 

267 

268RAR5_XTIME_UNIXTIME = 0x01 

269RAR5_XTIME_HAS_MTIME = 0x02 

270RAR5_XTIME_HAS_CTIME = 0x04 

271RAR5_XTIME_HAS_ATIME = 0x08 

272RAR5_XTIME_UNIXTIME_NS = 0x10 

273 

274RAR5_XENC_CIPHER_AES256 = 0 

275 

276RAR5_XENC_CHECKVAL = 0x01 

277RAR5_XENC_TWEAKED = 0x02 

278 

279RAR5_XHASH_BLAKE2SP = 0 

280 

281RAR5_XREDIR_UNIX_SYMLINK = 1 

282RAR5_XREDIR_WINDOWS_SYMLINK = 2 

283RAR5_XREDIR_WINDOWS_JUNCTION = 3 

284RAR5_XREDIR_HARD_LINK = 4 

285RAR5_XREDIR_FILE_COPY = 5 

286 

287RAR5_XREDIR_ISDIR = 0x01 

288 

289RAR5_XOWNER_UNAME = 0x01 

290RAR5_XOWNER_GNAME = 0x02 

291RAR5_XOWNER_UID = 0x04 

292RAR5_XOWNER_GID = 0x08 

293 

294RAR5_OS_WINDOWS = 0 

295RAR5_OS_UNIX = 1 

296 

297DOS_MODE_ARCHIVE = 0x20 

298DOS_MODE_DIR = 0x10 

299DOS_MODE_SYSTEM = 0x04 

300DOS_MODE_HIDDEN = 0x02 

301DOS_MODE_READONLY = 0x01 

302 

303RAR5_PW_CHECK_SIZE = 8 

304RAR5_PW_SUM_SIZE = 4 

305 

306## 

307## internal constants 

308## 

309 

310RAR_ID = b"Rar!\x1a\x07\x00" 

311RAR5_ID = b"Rar!\x1a\x07\x01\x00" 

312 

313WIN32 = sys.platform == "win32" 

314BSIZE = 512 * 1024 if WIN32 else 64 * 1024 

315 

316SFX_MAX_SIZE = 2 * 1024 * 1024 

317RAR_V3 = 3 

318RAR_V5 = 5 

319 

320_BAD_CHARS = r"""\x00-\x1F<>|"?*""" 

321RC_BAD_CHARS_UNIX = re.compile(r"[%s]" % _BAD_CHARS) 

322RC_BAD_CHARS_WIN32 = re.compile(r"[%s:^\\]" % _BAD_CHARS) 

323 

324FORCE_TOOL = False 

325 

326 

327def _find_sfx_header(xfile): 

328 sig = RAR_ID[:-1] 

329 buf = io.BytesIO() 

330 steps = (64, SFX_MAX_SIZE) 

331 

332 with XFile(xfile) as fd: 

333 for step in steps: 

334 data = fd.read(step) 

335 if not data: 

336 break 

337 buf.write(data) 

338 curdata = buf.getvalue() 

339 findpos = 0 

340 while True: 

341 pos = curdata.find(sig, findpos) 

342 if pos < 0: 

343 break 

344 if curdata[pos:pos + len(RAR_ID)] == RAR_ID: 

345 return RAR_V3, pos 

346 if curdata[pos:pos + len(RAR5_ID)] == RAR5_ID: 

347 return RAR_V5, pos 

348 findpos = pos + len(sig) 

349 fd.restore_pos() 

350 return 0, 0 

351 

352 

353## 

354## Public interface 

355## 

356 

357 

358def get_rar_version(xfile): 

359 """Check quickly whether file is rar archive. 

360 """ 

361 with XFile(xfile) as fd: 

362 buf = fd.read(len(RAR5_ID)) 

363 fd.restore_pos() 

364 if buf.startswith(RAR_ID): 

365 return RAR_V3 

366 elif buf.startswith(RAR5_ID): 

367 return RAR_V5 

368 return 0 

369 

370 

371def is_rarfile(xfile): 

372 """Check quickly whether file is rar archive. 

373 """ 

374 try: 

375 return get_rar_version(xfile) > 0 

376 except OSError: 

377 # File not found or not accessible, ignore 

378 return False 

379 

380 

381def is_rarfile_sfx(xfile): 

382 """Check whether file is rar archive with support for SFX. 

383 

384 It will read 2M from file. 

385 """ 

386 return _find_sfx_header(xfile)[0] > 0 

387 

388 

389class Error(Exception): 

390 """Base class for rarfile errors.""" 

391 

392 

393class BadRarFile(Error): 

394 """Incorrect data in archive.""" 

395 

396 

397class NotRarFile(Error): 

398 """The file is not RAR archive.""" 

399 

400 

401class BadRarName(Error): 

402 """Cannot guess multipart name components.""" 

403 

404 

405class NoRarEntry(Error): 

406 """File not found in RAR""" 

407 

408 

409class PasswordRequired(Error): 

410 """File requires password""" 

411 

412 

413class BadSymLinkError(Error): 

414 """Invalid symbolic link""" 

415 

416 

417class NeedFirstVolume(Error): 

418 """Need to start from first volume. 

419 

420 Attributes: 

421 

422 current_volume 

423 Volume number of current file or None if not known 

424 """ 

425 def __init__(self, msg, volume): 

426 super().__init__(msg) 

427 self.current_volume = volume 

428 

429 

430class NoCrypto(Error): 

431 """Cannot parse encrypted headers - no crypto available.""" 

432 

433 

434class RarExecError(Error): 

435 """Problem reported by unrar/rar.""" 

436 

437 

438class RarWarning(RarExecError): 

439 """Non-fatal error""" 

440 

441 

442class RarFatalError(RarExecError): 

443 """Fatal error""" 

444 

445 

446class RarCRCError(RarExecError): 

447 """CRC error during unpacking""" 

448 

449 

450class RarLockedArchiveError(RarExecError): 

451 """Must not modify locked archive""" 

452 

453 

454class RarWriteError(RarExecError): 

455 """Write error""" 

456 

457 

458class RarOpenError(RarExecError): 

459 """Open error""" 

460 

461 

462class RarUserError(RarExecError): 

463 """User error""" 

464 

465 

466class RarMemoryError(RarExecError): 

467 """Memory error""" 

468 

469 

470class RarCreateError(RarExecError): 

471 """Create error""" 

472 

473 

474class RarNoFilesError(RarExecError): 

475 """No files that match pattern were found""" 

476 

477 

478class RarUserBreak(RarExecError): 

479 """User stop""" 

480 

481 

482class RarWrongPassword(RarExecError): 

483 """Incorrect password""" 

484 

485 

486class RarUnknownError(RarExecError): 

487 """Unknown exit code""" 

488 

489 

490class RarSignalExit(RarExecError): 

491 """Unrar exited with signal""" 

492 

493 

494class RarCannotExec(RarExecError): 

495 """Executable not found.""" 

496 

497 

498class UnsupportedWarning(UserWarning): 

499 """Archive uses feature that are unsupported by rarfile. 

500 

501 .. versionadded:: 4.0 

502 """ 

503 

504 

505class RarInfo: 

506 r"""An entry in rar archive. 

507 

508 Timestamps as :class:`~datetime.datetime` are without timezone in RAR3, 

509 with UTC timezone in RAR5 archives. 

510 

511 Attributes: 

512 

513 filename 

514 File name with relative path. 

515 Path separator is "/". Always unicode string. 

516 

517 date_time 

518 File modification timestamp. As tuple of (year, month, day, hour, minute, second). 

519 RAR5 allows archives where it is missing, it's None then. 

520 

521 comment 

522 Optional file comment field. Unicode string. (RAR3-only) 

523 

524 file_size 

525 Uncompressed size. 

526 

527 compress_size 

528 Compressed size. 

529 

530 compress_type 

531 Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants. 

532 

533 extract_version 

534 Minimal Rar version needed for decompressing. As (major*10 + minor), 

535 so 2.9 is 29. 

536 

537 RAR3: 10, 20, 29 

538 

539 RAR5 does not have such field in archive, it's simply set to 50. 

540 

541 host_os 

542 Host OS type, one of RAR_OS_* constants. 

543 

544 RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`, 

545 :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`. 

546 

547 RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`. 

548 

549 mode 

550 File attributes. May be either dos-style or unix-style, depending on host_os. 

551 

552 mtime 

553 File modification time. Same value as :attr:`date_time` 

554 but as :class:`~datetime.datetime` object with extended precision. 

555 

556 ctime 

557 Optional time field: creation time. As :class:`~datetime.datetime` object. 

558 

559 atime 

560 Optional time field: last access time. As :class:`~datetime.datetime` object. 

561 

562 arctime 

563 Optional time field: archival time. As :class:`~datetime.datetime` object. 

564 (RAR3-only) 

565 

566 CRC 

567 CRC-32 of uncompressed file, unsigned int. 

568 

569 RAR5: may be None. 

570 

571 blake2sp_hash 

572 Blake2SP hash over decompressed data. (RAR5-only) 

573 

574 volume 

575 Volume nr, starting from 0. 

576 

577 volume_file 

578 Volume file name, where file starts. 

579 

580 file_redir 

581 If not None, file is link of some sort. Contains tuple of (type, flags, target). 

582 (RAR5-only) 

583 

584 Type is one of constants: 

585 

586 :data:`RAR5_XREDIR_UNIX_SYMLINK` 

587 Unix symlink. 

588 :data:`RAR5_XREDIR_WINDOWS_SYMLINK` 

589 Windows symlink. 

590 :data:`RAR5_XREDIR_WINDOWS_JUNCTION` 

591 Windows junction. 

592 :data:`RAR5_XREDIR_HARD_LINK` 

593 Hard link to target. 

594 :data:`RAR5_XREDIR_FILE_COPY` 

595 Current file is copy of another archive entry. 

596 

597 Flags may contain bits: 

598 

599 :data:`RAR5_XREDIR_ISDIR` 

600 Symlink points to directory. 

601 """ 

602 

603 # zipfile-compatible fields 

604 filename = None 

605 file_size = None 

606 compress_size = None 

607 date_time = None 

608 CRC = None 

609 volume = None 

610 orig_filename = None 

611 

612 # optional extended time fields, datetime() objects. 

613 mtime = None 

614 ctime = None 

615 atime = None 

616 

617 extract_version = None 

618 mode = None 

619 host_os = None 

620 compress_type = None 

621 

622 # rar3-only fields 

623 comment = None 

624 arctime = None 

625 

626 # rar5-only fields 

627 blake2sp_hash = None 

628 file_redir = None 

629 

630 # internal fields 

631 flags = 0 

632 type = None 

633 

634 # zipfile compat 

635 def is_dir(self): 

636 """Returns True if entry is a directory. 

637 

638 .. versionadded:: 4.0 

639 """ 

640 return False 

641 

642 def is_symlink(self): 

643 """Returns True if entry is a symlink. 

644 

645 .. versionadded:: 4.0 

646 """ 

647 return False 

648 

649 def is_file(self): 

650 """Returns True if entry is a normal file. 

651 

652 .. versionadded:: 4.0 

653 """ 

654 return False 

655 

656 def needs_password(self): 

657 """Returns True if data is stored password-protected. 

658 """ 

659 if self.type == RAR_BLOCK_FILE: 

660 return (self.flags & RAR_FILE_PASSWORD) > 0 

661 return False 

662 

663 def isdir(self): 

664 """Returns True if entry is a directory. 

665 

666 .. deprecated:: 4.0 

667 """ 

668 return self.is_dir() 

669 

670 

671class RarFile: 

672 """Parse RAR structure, provide access to files in archive. 

673 

674 Parameters: 

675 

676 file 

677 archive file name or file-like object. 

678 mode 

679 only "r" is supported. 

680 charset 

681 fallback charset to use, if filenames are not already Unicode-enabled. 

682 info_callback 

683 debug callback, gets to see all archive entries. 

684 crc_check 

685 set to False to disable CRC checks 

686 errors 

687 Either "stop" to quietly stop parsing on errors, 

688 or "strict" to raise errors. Default is "stop". 

689 part_only 

690 If True, read only single file and allow it to be middle-part 

691 of multi-volume archive. 

692 

693 .. versionadded:: 4.0 

694 """ 

695 

696 #: File name, if available. Unicode string or None. 

697 filename = None 

698 

699 #: Archive comment. Unicode string or None. 

700 comment = None 

701 

702 def __init__(self, file, mode="r", charset=None, info_callback=None, 

703 crc_check=True, errors="stop", part_only=False): 

704 if is_filelike(file): 

705 self.filename = getattr(file, "name", None) 

706 else: 

707 if isinstance(file, Path): 

708 file = str(file) 

709 self.filename = file 

710 self._rarfile = file 

711 

712 self._charset = charset or DEFAULT_CHARSET 

713 self._info_callback = info_callback 

714 self._crc_check = crc_check 

715 self._part_only = part_only 

716 self._password = None 

717 self._file_parser = None 

718 

719 if errors == "stop": 

720 self._strict = False 

721 elif errors == "strict": 

722 self._strict = True 

723 else: 

724 raise ValueError("Invalid value for errors= parameter.") 

725 

726 if mode != "r": 

727 raise NotImplementedError("RarFile supports only mode=r") 

728 

729 self._parse() 

730 

731 def __enter__(self): 

732 """Open context.""" 

733 return self 

734 

735 def __exit__(self, typ, value, traceback): 

736 """Exit context.""" 

737 self.close() 

738 

739 def __iter__(self): 

740 """Iterate over members.""" 

741 return iter(self.infolist()) 

742 

743 def setpassword(self, pwd): 

744 """Sets the password to use when extracting. 

745 """ 

746 self._password = pwd 

747 if self._file_parser: 

748 if self._file_parser.has_header_encryption(): 

749 self._file_parser = None 

750 if not self._file_parser: 

751 self._parse() 

752 else: 

753 self._file_parser.setpassword(self._password) 

754 

755 def needs_password(self): 

756 """Returns True if any archive entries require password for extraction. 

757 """ 

758 return self._file_parser.needs_password() 

759 

760 def is_solid(self): 

761 """Returns True if archive uses solid compression. 

762 

763 .. versionadded:: 4.2 

764 """ 

765 return self._file_parser.is_solid() 

766 

767 def namelist(self): 

768 """Return list of filenames in archive. 

769 """ 

770 return [f.filename for f in self.infolist()] 

771 

772 def infolist(self): 

773 """Return RarInfo objects for all files/directories in archive. 

774 """ 

775 return self._file_parser.infolist() 

776 

777 def volumelist(self): 

778 """Returns filenames of archive volumes. 

779 

780 In case of single-volume archive, the list contains 

781 just the name of main archive file. 

782 """ 

783 return self._file_parser.volumelist() 

784 

785 def getinfo(self, name): 

786 """Return RarInfo for file. 

787 """ 

788 return self._file_parser.getinfo(name) 

789 

790 def getinfo_orig(self, name): 

791 """Return RarInfo for file source. 

792 

793 RAR5: if name is hard-linked or copied file, 

794 returns original entry with original filename. 

795 

796 .. versionadded:: 4.1 

797 """ 

798 return self._file_parser.getinfo_orig(name) 

799 

800 def open(self, name, mode="r", pwd=None): 

801 """Returns file-like object (:class:`RarExtFile`) from where the data can be read. 

802 

803 The object implements :class:`io.RawIOBase` interface, so it can 

804 be further wrapped with :class:`io.BufferedReader` 

805 and :class:`io.TextIOWrapper`. 

806 

807 On older Python where io module is not available, it implements 

808 only .read(), .seek(), .tell() and .close() methods. 

809 

810 The object is seekable, although the seeking is fast only on 

811 uncompressed files, on compressed files the seeking is implemented 

812 by reading ahead and/or restarting the decompression. 

813 

814 Parameters: 

815 

816 name 

817 file name or RarInfo instance. 

818 mode 

819 must be "r" 

820 pwd 

821 password to use for extracting. 

822 """ 

823 

824 if mode != "r": 

825 raise NotImplementedError("RarFile.open() supports only mode=r") 

826 

827 # entry lookup 

828 inf = self.getinfo(name) 

829 if inf.is_dir(): 

830 raise io.UnsupportedOperation("Directory does not have any data: " + inf.filename) 

831 

832 # check password 

833 if inf.needs_password(): 

834 pwd = pwd or self._password 

835 if pwd is None: 

836 raise PasswordRequired("File %s requires password" % inf.filename) 

837 else: 

838 pwd = None 

839 

840 return self._file_parser.open(inf, pwd) 

841 

842 def read(self, name, pwd=None): 

843 """Return uncompressed data for archive entry. 

844 

845 For longer files using :meth:`~RarFile.open` may be better idea. 

846 

847 Parameters: 

848 

849 name 

850 filename or RarInfo instance 

851 pwd 

852 password to use for extracting. 

853 """ 

854 

855 with self.open(name, "r", pwd) as f: 

856 return f.read() 

857 

858 def close(self): 

859 """Release open resources.""" 

860 pass 

861 

862 def printdir(self, file=None): 

863 """Print archive file list to stdout or given file. 

864 """ 

865 if file is None: 

866 file = sys.stdout 

867 for f in self.infolist(): 

868 print(f.filename, file=file) 

869 

870 def extract(self, member, path=None, pwd=None): 

871 """Extract single file into current directory. 

872 

873 Parameters: 

874 

875 member 

876 filename or :class:`RarInfo` instance 

877 path 

878 optional destination path 

879 pwd 

880 optional password to use 

881 """ 

882 inf = self.getinfo(member) 

883 return self._extract_one(inf, path, pwd, True) 

884 

885 def extractall(self, path=None, members=None, pwd=None): 

886 """Extract all files into current directory. 

887 

888 Parameters: 

889 

890 path 

891 optional destination path 

892 members 

893 optional filename or :class:`RarInfo` instance list to extract 

894 pwd 

895 optional password to use 

896 """ 

897 if members is None: 

898 members = self.namelist() 

899 

900 done = set() 

901 dirs = [] 

902 for m in members: 

903 inf = self.getinfo(m) 

904 dst = self._extract_one(inf, path, pwd, not inf.is_dir()) 

905 if inf.is_dir(): 

906 if dst not in done: 

907 dirs.append((dst, inf)) 

908 done.add(dst) 

909 if dirs: 

910 dirs.sort(reverse=True) 

911 for dst, inf in dirs: 

912 self._set_attrs(inf, dst) 

913 

914 def testrar(self, pwd=None): 

915 """Read all files and test CRC. 

916 """ 

917 for member in self.infolist(): 

918 if member.is_file(): 

919 with self.open(member, 'r', pwd) as f: 

920 empty_read(f, member.file_size, BSIZE) 

921 

922 def strerror(self): 

923 """Return error string if parsing failed or None if no problems. 

924 """ 

925 if not self._file_parser: 

926 return "Not a RAR file" 

927 return self._file_parser.strerror() 

928 

929 ## 

930 ## private methods 

931 ## 

932 

933 def _parse(self): 

934 """Run parser for file type 

935 """ 

936 ver, sfx_ofs = _find_sfx_header(self._rarfile) 

937 if ver == RAR_V3: 

938 p3 = RAR3Parser(self._rarfile, self._password, self._crc_check, 

939 self._charset, self._strict, self._info_callback, 

940 sfx_ofs, self._part_only) 

941 self._file_parser = p3 # noqa 

942 elif ver == RAR_V5: 

943 p5 = RAR5Parser(self._rarfile, self._password, self._crc_check, 

944 self._charset, self._strict, self._info_callback, 

945 sfx_ofs, self._part_only) 

946 self._file_parser = p5 # noqa 

947 else: 

948 raise NotRarFile("Not a RAR file") 

949 

950 self._file_parser.parse() 

951 self.comment = self._file_parser.comment 

952 

953 def _extract_one(self, info, path, pwd, set_attrs): 

954 fname = sanitize_filename( 

955 info.filename, os.path.sep, WIN32 

956 ) 

957 

958 if path is None: 

959 path = os.getcwd() 

960 else: 

961 path = os.fspath(path) 

962 dstfn = os.path.join(path, fname) 

963 

964 # Reject members whose destination escapes `path` once symlinks 

965 # already created on disk are resolved. Without this, a symlink 

966 # member can point outside `path` and a later file/dir member 

967 # named through it will be written outside the extraction root. 

968 real_path = os.path.realpath(path) 

969 real_dst = os.path.realpath(dstfn) 

970 if real_dst != real_path and not real_dst.startswith(real_path + os.sep): 

971 raise BadRarFile( 

972 "Refusing to extract entry that escapes destination: %r" % info.filename 

973 ) 

974 

975 dirname = os.path.dirname(dstfn) 

976 if dirname and dirname != ".": 

977 os.makedirs(dirname, exist_ok=True) 

978 

979 if info.is_file(): 

980 return self._make_file(info, dstfn, pwd, set_attrs) 

981 if info.is_dir(): 

982 return self._make_dir(info, dstfn, pwd, set_attrs) 

983 if info.is_symlink(): 

984 return self._make_symlink(info, dstfn, pwd, set_attrs, path) 

985 return None 

986 

987 def _create_helper(self, name, flags, info): 

988 return os.open(name, flags) 

989 

990 def _make_file(self, info, dstfn, pwd, set_attrs): 

991 def helper(name, flags): 

992 return self._create_helper(name, flags, info) 

993 with self.open(info, "r", pwd) as src: 

994 with open(dstfn, "wb", opener=helper) as dst: 

995 shutil.copyfileobj(src, dst) 

996 if set_attrs: 

997 self._set_attrs(info, dstfn) 

998 return dstfn 

999 

1000 def _make_dir(self, info, dstfn, pwd, set_attrs): 

1001 os.makedirs(dstfn, exist_ok=True) 

1002 if set_attrs: 

1003 self._set_attrs(info, dstfn) 

1004 return dstfn 

1005 

1006 def _make_symlink(self, info, dstfn, pwd, set_attrs, top): 

1007 target_is_directory = False 

1008 if info.host_os == RAR_OS_UNIX: 

1009 link_name = self.read(info, pwd).decode("utf8", "replace") 

1010 target_is_directory = (info.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY 

1011 elif info.file_redir: 

1012 redir_type, redir_flags, link_name = info.file_redir 

1013 if redir_type == RAR5_XREDIR_WINDOWS_JUNCTION: 

1014 warnings.warn(f"Windows junction not supported - {info.filename}", UnsupportedWarning) 

1015 return None 

1016 target_is_directory = (redir_type & RAR5_XREDIR_ISDIR) > 0 

1017 else: 

1018 warnings.warn(f"Unsupported link type - {info.filename}", UnsupportedWarning) 

1019 return None 

1020 

1021 # disallow abs paths 

1022 target = os.path.normpath(link_name) 

1023 if os.path.isabs(target) or os.path.splitdrive(target)[0]: 

1024 raise BadSymLinkError('Absolute links not allowed') 

1025 

1026 # disallow ../ traversal 

1027 dest_abs = os.path.realpath(top) 

1028 target_base = os.path.dirname(dstfn) 

1029 target_abs = os.path.realpath(os.path.join(target_base, target)) 

1030 if os.path.commonpath([target_abs, dest_abs]) != dest_abs: 

1031 raise BadSymLinkError('Link to outside not allowed') 

1032 

1033 os.symlink(link_name, dstfn, target_is_directory=target_is_directory) 

1034 return dstfn 

1035 

1036 def _set_attrs(self, info, dstfn): 

1037 if info.host_os == RAR_OS_UNIX: 

1038 os.chmod(dstfn, info.mode & 0o777) 

1039 elif info.host_os in (RAR_OS_WIN32, RAR_OS_MSDOS): 

1040 # only keep R/O attr, except for dirs on win32 

1041 if info.mode & DOS_MODE_READONLY and (info.is_file() or not WIN32): 

1042 st = os.stat(dstfn) 

1043 new_mode = st.st_mode & ~0o222 

1044 os.chmod(dstfn, new_mode) 

1045 

1046 if info.mtime: 

1047 mtime_ns = to_nsecs(info.mtime) 

1048 atime_ns = to_nsecs(info.atime) if info.atime else mtime_ns 

1049 os.utime(dstfn, ns=(atime_ns, mtime_ns)) 

1050 

1051 

1052# 

1053# File format parsing 

1054# 

1055 

1056class CommonParser: 

1057 """Shared parser parts.""" 

1058 _main = None 

1059 _hdrenc_main = None 

1060 _needs_password = False 

1061 _fd = None 

1062 _expect_sig = None 

1063 _parse_error = None 

1064 _password = None 

1065 comment = None 

1066 

1067 def __init__(self, rarfile, password, crc_check, charset, strict, 

1068 info_cb, sfx_offset, part_only): 

1069 self._rarfile = rarfile 

1070 self._password = password 

1071 self._crc_check = crc_check 

1072 self._charset = charset 

1073 self._strict = strict 

1074 self._info_callback = info_cb 

1075 self._info_list = [] 

1076 self._info_map = {} 

1077 self._vol_list = [] 

1078 self._sfx_offset = sfx_offset 

1079 self._part_only = part_only 

1080 

1081 def is_solid(self): 

1082 """Returns True if archive uses solid compression. 

1083 """ 

1084 if self._main: 

1085 if self._main.flags & RAR_MAIN_SOLID: 

1086 return True 

1087 return False 

1088 

1089 def has_header_encryption(self): 

1090 """Returns True if headers are encrypted 

1091 """ 

1092 if self._hdrenc_main: 

1093 return True 

1094 if self._main: 

1095 if self._main.flags & RAR_MAIN_PASSWORD: 

1096 return True 

1097 return False 

1098 

1099 def setpassword(self, pwd): 

1100 """Set cached password.""" 

1101 self._password = pwd 

1102 

1103 def volumelist(self): 

1104 """Volume files""" 

1105 return self._vol_list 

1106 

1107 def needs_password(self): 

1108 """Is password required""" 

1109 return self._needs_password 

1110 

1111 def strerror(self): 

1112 """Last error""" 

1113 return self._parse_error 

1114 

1115 def infolist(self): 

1116 """List of RarInfo records. 

1117 """ 

1118 return self._info_list 

1119 

1120 def getinfo(self, member): 

1121 """Return RarInfo for filename 

1122 """ 

1123 if isinstance(member, RarInfo): 

1124 fname = member.filename 

1125 elif isinstance(member, Path): 

1126 fname = str(member) 

1127 else: 

1128 fname = member 

1129 

1130 if fname.endswith("/"): 

1131 fname = fname.rstrip("/") 

1132 

1133 try: 

1134 return self._info_map[fname] 

1135 except KeyError: 

1136 raise NoRarEntry("No such file: %s" % fname) from None 

1137 

1138 def getinfo_orig(self, member): 

1139 inf = self.getinfo(member) 

1140 if inf.file_redir: 

1141 redir_type, redir_flags, redir_name = inf.file_redir 

1142 # cannot leave to unrar as it expects copied file to exist 

1143 if redir_type in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK): 

1144 inf = self.getinfo(redir_name) 

1145 return inf 

1146 

1147 def parse(self): 

1148 """Process file.""" 

1149 self._fd = None 

1150 try: 

1151 self._parse_real() 

1152 finally: 

1153 if self._fd: 

1154 self._fd.close() 

1155 self._fd = None 

1156 

1157 def _parse_real(self): 

1158 """Actually read file. 

1159 """ 

1160 fd = XFile(self._rarfile) 

1161 self._fd = fd 

1162 fd.seek(self._sfx_offset, 0) 

1163 sig = fd.read(len(self._expect_sig)) 

1164 if sig != self._expect_sig: 

1165 raise NotRarFile("Not a Rar archive") 

1166 

1167 volume = 0 # first vol (.rar) is 0 

1168 more_vols = False 

1169 endarc = False 

1170 volfile = self._rarfile 

1171 self._vol_list = [self._rarfile] 

1172 raise_need_first_vol = False 

1173 while True: 

1174 if endarc: 

1175 h = None # don"t read past ENDARC 

1176 else: 

1177 h = self._parse_header(fd) 

1178 if not h: 

1179 if raise_need_first_vol: 

1180 # did not find ENDARC with VOLNR 

1181 raise NeedFirstVolume("Need to start from first volume", None) 

1182 if more_vols and not self._part_only: 

1183 volume += 1 

1184 fd.close() 

1185 try: 

1186 volfile = self._next_volname(volfile) 

1187 fd = XFile(volfile) 

1188 except IOError: 

1189 self._set_error("Cannot open next volume: %s", volfile) 

1190 break 

1191 self._fd = fd 

1192 sig = fd.read(len(self._expect_sig)) 

1193 if sig != self._expect_sig: 

1194 self._set_error("Invalid volume sig: %s", volfile) 

1195 break 

1196 more_vols = False 

1197 endarc = False 

1198 self._vol_list.append(volfile) 

1199 self._main = None 

1200 self._hdrenc_main = None 

1201 continue 

1202 break 

1203 h.volume = volume 

1204 h.volume_file = volfile 

1205 

1206 if h.type == RAR_BLOCK_MAIN and not self._main: 

1207 self._main = h 

1208 if volume == 0 and (h.flags & RAR_MAIN_NEWNUMBERING) and not self._part_only: 

1209 # RAR 2.x does not set FIRSTVOLUME, 

1210 # so check it only if NEWNUMBERING is used 

1211 if (h.flags & RAR_MAIN_FIRSTVOLUME) == 0: 

1212 if getattr(h, "main_volume_number", None) is not None: 

1213 # rar5 may have more info 

1214 raise NeedFirstVolume( 

1215 "Need to start from first volume (current: %r)" 

1216 % (h.main_volume_number,), 

1217 h.main_volume_number 

1218 ) 

1219 # delay raise until we have volnr from ENDARC 

1220 raise_need_first_vol = True 

1221 if h.flags & RAR_MAIN_PASSWORD: 

1222 self._needs_password = True 

1223 if not self._password: 

1224 break 

1225 elif h.type == RAR_BLOCK_ENDARC: 

1226 # use flag, but also allow RAR 2.x logic below to trigger 

1227 if h.flags & RAR_ENDARC_NEXT_VOLUME: 

1228 more_vols = True 

1229 endarc = True 

1230 if raise_need_first_vol and (h.flags & RAR_ENDARC_VOLNR) > 0: 

1231 raise NeedFirstVolume( 

1232 "Need to start from first volume (current: %r)" 

1233 % (h.endarc_volnr,), 

1234 h.endarc_volnr 

1235 ) 

1236 elif h.type == RAR_BLOCK_FILE: 

1237 # RAR 2.x does not write RAR_BLOCK_ENDARC 

1238 if h.flags & RAR_FILE_SPLIT_AFTER: 

1239 more_vols = True 

1240 # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME 

1241 if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE: 

1242 if not self._part_only: 

1243 raise_need_first_vol = True 

1244 

1245 if h.needs_password(): 

1246 self._needs_password = True 

1247 

1248 # store it 

1249 self.process_entry(fd, h) 

1250 

1251 if self._info_callback: 

1252 self._info_callback(h) 

1253 

1254 # go to next header 

1255 if h.add_size > 0: 

1256 fd.seek(h.data_offset + h.add_size, 0) 

1257 

1258 def process_entry(self, fd, item): 

1259 """Examine item, add into lookup cache.""" 

1260 raise NotImplementedError() 

1261 

1262 def _decrypt_header(self, fd): 

1263 raise NotImplementedError("_decrypt_header") 

1264 

1265 def _parse_block_header(self, fd): 

1266 raise NotImplementedError("_parse_block_header") 

1267 

1268 def _open_hack(self, inf, pwd): 

1269 raise NotImplementedError("_open_hack") 

1270 

1271 def _parse_header(self, fd): 

1272 """Read single header 

1273 """ 

1274 try: 

1275 # handle encrypted headers 

1276 if (self._main and self._main.flags & RAR_MAIN_PASSWORD) or self._hdrenc_main: 

1277 if not self._password: 

1278 return None 

1279 fd = self._decrypt_header(fd) 

1280 

1281 # now read actual header 

1282 return self._parse_block_header(fd) 

1283 except struct.error: 

1284 self._set_error("Broken header in RAR file") 

1285 return None 

1286 

1287 def _next_volname(self, volfile): 

1288 """Given current vol name, construct next one 

1289 """ 

1290 if is_filelike(volfile): 

1291 raise IOError("Working on single FD") 

1292 if self._main.flags & RAR_MAIN_NEWNUMBERING: 

1293 return _next_newvol(volfile) 

1294 return _next_oldvol(volfile) 

1295 

1296 def _set_error(self, msg, *args): 

1297 if args: 

1298 msg = msg % args 

1299 self._parse_error = msg 

1300 if self._strict: 

1301 raise BadRarFile(msg) 

1302 

1303 def open(self, inf, pwd): 

1304 """Return stream object for file data.""" 

1305 

1306 if inf.file_redir: 

1307 redir_type, redir_flags, redir_name = inf.file_redir 

1308 # cannot leave to unrar as it expects copied file to exist 

1309 if redir_type in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK): 

1310 inf = self.getinfo(redir_name) 

1311 if not inf: 

1312 raise BadRarFile("cannot find copied file") 

1313 elif redir_type in ( 

1314 RAR5_XREDIR_UNIX_SYMLINK, RAR5_XREDIR_WINDOWS_SYMLINK, 

1315 RAR5_XREDIR_WINDOWS_JUNCTION, 

1316 ): 

1317 return io.BytesIO(redir_name.encode("utf8")) 

1318 if inf.flags & RAR_FILE_SPLIT_BEFORE: 

1319 raise NeedFirstVolume("Partial file, please start from first volume: " + inf.filename, None) 

1320 

1321 # is temp write usable? 

1322 use_hack = 1 

1323 if not self._main: 

1324 use_hack = 0 

1325 elif self._main._must_disable_hack(): 

1326 use_hack = 0 

1327 elif inf._must_disable_hack(): 

1328 use_hack = 0 

1329 elif is_filelike(self._rarfile): 

1330 pass 

1331 elif inf.file_size > HACK_SIZE_LIMIT: 

1332 use_hack = 0 

1333 elif not USE_EXTRACT_HACK: 

1334 use_hack = 0 

1335 

1336 # now extract 

1337 if inf.compress_type == RAR_M0 and (inf.flags & RAR_FILE_PASSWORD) == 0 and inf.file_redir is None: 

1338 return self._open_clear(inf) 

1339 elif use_hack: 

1340 return self._open_hack(inf, pwd) 

1341 elif is_filelike(self._rarfile): 

1342 return self._open_unrar_membuf(self._rarfile, inf, pwd) 

1343 else: 

1344 return self._open_unrar(self._rarfile, inf, pwd) 

1345 

1346 def _open_clear(self, inf): 

1347 if FORCE_TOOL: 

1348 return self._open_unrar(self._rarfile, inf) 

1349 return DirectReader(self, inf) 

1350 

1351 def _open_hack_core(self, inf, pwd, prefix, suffix): 

1352 

1353 size = inf.compress_size + inf.header_size 

1354 rf = XFile(inf.volume_file, 0) 

1355 rf.seek(inf.header_offset) 

1356 

1357 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR) 

1358 tmpf = os.fdopen(tmpfd, "wb") 

1359 

1360 try: 

1361 tmpf.write(prefix) 

1362 while size > 0: 

1363 if size > BSIZE: 

1364 buf = rf.read(BSIZE) 

1365 else: 

1366 buf = rf.read(size) 

1367 if not buf: 

1368 raise BadRarFile("read failed: " + inf.filename) 

1369 tmpf.write(buf) 

1370 size -= len(buf) 

1371 tmpf.write(suffix) 

1372 tmpf.close() 

1373 rf.close() 

1374 except BaseException: 

1375 rf.close() 

1376 tmpf.close() 

1377 os.unlink(tmpname) 

1378 raise 

1379 

1380 return self._open_unrar(tmpname, inf, pwd, tmpname) 

1381 

1382 def _open_unrar_membuf(self, memfile, inf, pwd): 

1383 """Write in-memory archive to temp file, needed for solid archives. 

1384 """ 

1385 tmpname = membuf_tempfile(memfile) 

1386 return self._open_unrar(tmpname, inf, pwd, tmpname, force_file=True) 

1387 

1388 def _open_unrar(self, rarfile, inf, pwd=None, tmpfile=None, force_file=False): 

1389 """Extract using unrar 

1390 """ 

1391 setup = tool_setup() 

1392 

1393 # not giving filename avoids encoding related problems 

1394 fn = None 

1395 if not tmpfile or force_file: 

1396 fn = inf.filename.replace("/", os.path.sep) 

1397 

1398 # read from unrar pipe 

1399 cmd = setup.open_cmdline(pwd, rarfile, fn) 

1400 return PipeReader(self, inf, cmd, tmpfile) 

1401 

1402 

1403# 

1404# RAR3 format 

1405# 

1406 

1407class Rar3Info(RarInfo): 

1408 """RAR3 specific fields.""" 

1409 extract_version = 15 

1410 salt = None 

1411 add_size = 0 

1412 header_crc = None 

1413 header_size = None 

1414 header_offset = None 

1415 data_offset = None 

1416 _md_class = None 

1417 _md_expect = None 

1418 _name_size = None 

1419 

1420 # make sure some rar5 fields are always present 

1421 file_redir = None 

1422 blake2sp_hash = None 

1423 

1424 endarc_datacrc = None 

1425 endarc_volnr = None 

1426 

1427 old_sub_type = None 

1428 

1429 def _must_disable_hack(self): 

1430 if self.type == RAR_BLOCK_FILE: 

1431 if self.flags & RAR_FILE_PASSWORD: 

1432 return True 

1433 elif self.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): 

1434 return True 

1435 elif self.type == RAR_BLOCK_MAIN: 

1436 if self.flags & (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD): 

1437 return True 

1438 return False 

1439 

1440 def is_dir(self): 

1441 """Returns True if entry is a directory.""" 

1442 if self.type == RAR_BLOCK_FILE and not self.is_symlink(): 

1443 return (self.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY 

1444 return False 

1445 

1446 def is_symlink(self): 

1447 """Returns True if entry is a symlink.""" 

1448 return ( 

1449 self.type == RAR_BLOCK_FILE and 

1450 self.host_os == RAR_OS_UNIX and 

1451 self.mode & 0xF000 == 0xA000 

1452 ) 

1453 

1454 def is_file(self): 

1455 """Returns True if entry is a normal file.""" 

1456 return ( 

1457 self.type == RAR_BLOCK_FILE and 

1458 not (self.is_dir() or self.is_symlink()) 

1459 ) 

1460 

1461 

1462class RAR3Parser(CommonParser): 

1463 """Parse RAR3 file format. 

1464 """ 

1465 _expect_sig = RAR_ID 

1466 _last_aes_key = (None, None, None) # (salt, key, iv) 

1467 

1468 def _decrypt_header(self, fd): 

1469 if not _have_crypto: 

1470 raise NoCrypto("Cannot parse encrypted headers - no crypto") 

1471 salt = fd.read(8) 

1472 if self._last_aes_key[0] == salt: 

1473 key, iv = self._last_aes_key[1:] 

1474 else: 

1475 key, iv = rar3_s2k(self._password, salt) 

1476 self._last_aes_key = (salt, key, iv) 

1477 return HeaderDecrypt(fd, key, iv) 

1478 

1479 def _parse_block_header(self, fd): 

1480 """Parse common block header 

1481 """ 

1482 h = Rar3Info() 

1483 h.header_offset = fd.tell() 

1484 

1485 # read and parse base header 

1486 buf = fd.read(S_BLK_HDR.size) 

1487 if not buf: 

1488 return None 

1489 if len(buf) < S_BLK_HDR.size: 

1490 self._set_error("Unexpected EOF when reading header") 

1491 return None 

1492 t = S_BLK_HDR.unpack_from(buf) 

1493 h.header_crc, h.type, h.flags, h.header_size = t 

1494 

1495 # read full header 

1496 if h.header_size > S_BLK_HDR.size: 

1497 hdata = buf + fd.read(h.header_size - S_BLK_HDR.size) 

1498 else: 

1499 hdata = buf 

1500 h.data_offset = fd.tell() 

1501 

1502 # unexpected EOF? 

1503 if len(hdata) != h.header_size: 

1504 self._set_error("Unexpected EOF when reading header") 

1505 return None 

1506 

1507 pos = S_BLK_HDR.size 

1508 

1509 # block has data assiciated with it? 

1510 if h.flags & RAR_LONG_BLOCK: 

1511 h.add_size, pos = load_le32(hdata, pos) 

1512 else: 

1513 h.add_size = 0 

1514 

1515 # parse interesting ones, decide header boundaries for crc 

1516 if h.type == RAR_BLOCK_MARK: 

1517 return h 

1518 elif h.type == RAR_BLOCK_MAIN: 

1519 pos += 6 

1520 if h.flags & RAR_MAIN_ENCRYPTVER: 

1521 pos += 1 

1522 crc_pos = pos 

1523 if h.flags & RAR_MAIN_COMMENT: 

1524 self._parse_subblocks(h, hdata, pos) 

1525 elif h.type == RAR_BLOCK_FILE: 

1526 pos = self._parse_file_header(h, hdata, pos - 4) 

1527 crc_pos = pos 

1528 if h.flags & RAR_FILE_COMMENT: 

1529 pos = self._parse_subblocks(h, hdata, pos) 

1530 elif h.type == RAR_BLOCK_SUB: 

1531 pos = self._parse_file_header(h, hdata, pos - 4) 

1532 crc_pos = h.header_size 

1533 elif h.type == RAR_BLOCK_OLD_AUTH: 

1534 pos += 8 

1535 crc_pos = pos 

1536 elif h.type == RAR_BLOCK_OLD_EXTRA: 

1537 pos += 7 

1538 crc_pos = pos 

1539 elif h.type == RAR_BLOCK_OLD_SUB: 

1540 pos = self._parse_old_subblock(h, hdata, pos) 

1541 

1542 # these types do not have their own data CRC, 

1543 # so data was included in header CRC. 

1544 if h.old_sub_type in (RAR_OLD_SUB_UNIX, RAR_OLD_SUB_MAC): 

1545 # skip CRC check, it requires to read data part 

1546 return h 

1547 

1548 crc_pos = h.header_size 

1549 elif h.type == RAR_BLOCK_ENDARC: 

1550 if h.flags & RAR_ENDARC_DATACRC: 

1551 h.endarc_datacrc, pos = load_le32(hdata, pos) 

1552 if h.flags & RAR_ENDARC_VOLNR: 

1553 h.endarc_volnr = S_SHORT.unpack_from(hdata, pos)[0] 

1554 pos += 2 

1555 crc_pos = h.header_size 

1556 else: 

1557 crc_pos = h.header_size 

1558 

1559 # calculate crc 

1560 crcdat = hdata[2:crc_pos] 

1561 calc_crc = crc32(crcdat) & 0xFFFF 

1562 

1563 # return good header 

1564 if h.header_crc == calc_crc: 

1565 return h 

1566 

1567 # header parsing failed. 

1568 self._set_error("Header CRC error (%02x): exp=%x got=%x (xlen = %d)", 

1569 h.type, h.header_crc, calc_crc, len(crcdat)) 

1570 

1571 # instead panicing, send eof 

1572 return None 

1573 

1574 def _parse_file_header(self, h, hdata, pos): 

1575 """Read file-specific header 

1576 """ 

1577 fld = S_FILE_HDR.unpack_from(hdata, pos) 

1578 pos += S_FILE_HDR.size 

1579 

1580 h.compress_size = fld[0] 

1581 h.file_size = fld[1] 

1582 h.host_os = fld[2] 

1583 h.CRC = fld[3] 

1584 h.date_time = parse_dos_time(fld[4]) 

1585 h.mtime = to_datetime(h.date_time) 

1586 h.extract_version = fld[5] 

1587 h.compress_type = fld[6] 

1588 h._name_size = name_size = fld[7] 

1589 h.mode = fld[8] 

1590 

1591 h._md_class = CRC32Context 

1592 h._md_expect = h.CRC 

1593 

1594 if h.flags & RAR_FILE_LARGE: 

1595 h1, pos = load_le32(hdata, pos) 

1596 h2, pos = load_le32(hdata, pos) 

1597 h.compress_size |= h1 << 32 

1598 h.file_size |= h2 << 32 

1599 h.add_size = h.compress_size 

1600 

1601 name, pos = load_bytes(hdata, name_size, pos) 

1602 if h.flags & RAR_FILE_UNICODE and b"\0" in name: 

1603 # stored in custom encoding 

1604 nul = name.find(b"\0") 

1605 h.orig_filename = name[:nul] 

1606 u = UnicodeFilename(h.orig_filename, name[nul + 1:]) 

1607 h.filename = u.decode() 

1608 

1609 # if parsing failed fall back to simple name 

1610 if u.failed: 

1611 h.filename = self._decode(h.orig_filename) 

1612 elif h.flags & RAR_FILE_UNICODE: 

1613 # stored in UTF8 

1614 h.orig_filename = name 

1615 h.filename = name.decode("utf8", "replace") 

1616 else: 

1617 nul = name.find(b"\0") 

1618 if nul >= 0: 

1619 name = name[:nul] 

1620 # stored in random encoding 

1621 h.orig_filename = name 

1622 h.filename = self._decode(name) 

1623 

1624 # change separator, set dir suffix 

1625 h.filename = h.filename.replace("\\", "/").rstrip("/") 

1626 if h.is_dir(): 

1627 h.filename = h.filename + "/" 

1628 

1629 if h.flags & RAR_FILE_SALT: 

1630 h.salt, pos = load_bytes(hdata, 8, pos) 

1631 else: 

1632 h.salt = None 

1633 

1634 # optional extended time stamps 

1635 if h.flags & RAR_FILE_EXTTIME: 

1636 pos = _parse_ext_time(h, hdata, pos) 

1637 else: 

1638 h.mtime = h.atime = h.ctime = h.arctime = None 

1639 

1640 return pos 

1641 

1642 def _parse_old_subblock(self, h, hdata, pos): 

1643 """Parse RAR2 subblock 

1644 """ 

1645 h.old_sub_type, _reserved = S_OLD_SUBBLOCK_HDR.unpack_from(hdata, pos) 

1646 return pos 

1647 

1648 def _parse_subblocks(self, h, hdata, pos): 

1649 """Find old-style comment subblock 

1650 """ 

1651 while pos < len(hdata): 

1652 # ordinary block header 

1653 t = S_BLK_HDR.unpack_from(hdata, pos) 

1654 ___scrc, stype, sflags, slen = t 

1655 pos_next = pos + slen 

1656 pos += S_BLK_HDR.size 

1657 

1658 # corrupt header 

1659 if pos_next < pos: 

1660 break 

1661 

1662 # followed by block-specific header 

1663 if stype == RAR_BLOCK_OLD_COMMENT and pos + S_COMMENT_HDR.size <= pos_next: 

1664 declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos) 

1665 if declen > RAR_MAX_COMMENT: 

1666 pos = pos_next 

1667 continue 

1668 pos += S_COMMENT_HDR.size 

1669 data = hdata[pos: pos_next] 

1670 cmt = rar3_decompress(ver, meth, data, declen, sflags, 

1671 crc, self._password) 

1672 if not self._crc_check or (crc32(cmt) & 0xFFFF == crc): 

1673 h.comment = self._decode_comment(cmt) 

1674 

1675 pos = pos_next 

1676 return pos 

1677 

1678 def _read_comment_v3(self, inf, pwd=None): 

1679 

1680 if inf.compress_size > RAR_MAX_COMMENT: 

1681 return None 

1682 if inf.file_size > RAR_MAX_COMMENT: 

1683 return None 

1684 

1685 # read data 

1686 with XFile(inf.volume_file) as rf: 

1687 rf.seek(inf.data_offset) 

1688 data = rf.read(inf.compress_size) 

1689 

1690 # decompress 

1691 cmt = rar3_decompress(inf.extract_version, inf.compress_type, data, 

1692 inf.file_size, inf.flags, inf.CRC, pwd, inf.salt) 

1693 

1694 # check crc 

1695 if self._crc_check: 

1696 crc = crc32(cmt) 

1697 if crc != inf.CRC: 

1698 return None 

1699 

1700 return self._decode_comment(cmt) 

1701 

1702 def _decode(self, val): 

1703 for c in TRY_ENCODINGS: 

1704 try: 

1705 return val.decode(c) 

1706 except UnicodeError: 

1707 pass 

1708 return val.decode(self._charset, "replace") 

1709 

1710 def _decode_comment(self, val): 

1711 return self._decode(val) 

1712 

1713 def process_entry(self, fd, item): 

1714 if item.type == RAR_BLOCK_FILE: 

1715 # use only first part 

1716 if item.flags & RAR_FILE_VERSION: 

1717 pass # skip old versions 

1718 elif (item.flags & RAR_FILE_SPLIT_BEFORE) == 0: 

1719 self._info_map[item.filename.rstrip("/")] = item 

1720 self._info_list.append(item) 

1721 elif len(self._info_list) > 0: 

1722 # final crc is in last block 

1723 old = self._info_list[-1] 

1724 old.CRC = item.CRC 

1725 old._md_expect = item._md_expect 

1726 old.compress_size += item.compress_size 

1727 

1728 # parse new-style comment 

1729 if item.type == RAR_BLOCK_SUB and item.filename == "CMT": 

1730 if item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): 

1731 pass 

1732 elif item.flags & RAR_FILE_SOLID: 

1733 # file comment 

1734 cmt = self._read_comment_v3(item, self._password) 

1735 if len(self._info_list) > 0: 

1736 old = self._info_list[-1] 

1737 old.comment = cmt 

1738 else: 

1739 # archive comment 

1740 cmt = self._read_comment_v3(item, self._password) 

1741 self.comment = cmt 

1742 

1743 if item.type == RAR_BLOCK_MAIN: 

1744 if item.flags & RAR_MAIN_COMMENT: 

1745 self.comment = item.comment 

1746 if item.flags & RAR_MAIN_PASSWORD: 

1747 self._needs_password = True 

1748 

1749 # put file compressed data into temporary .rar archive, and run 

1750 # unrar on that, thus avoiding unrar going over whole archive 

1751 def _open_hack(self, inf, pwd): 

1752 # create main header: crc, type, flags, size, res1, res2 

1753 prefix = RAR_ID + S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + b"\0" * (2 + 4) 

1754 return self._open_hack_core(inf, pwd, prefix, b"") 

1755 

1756 

1757# 

1758# RAR5 format 

1759# 

1760 

1761class Rar5Info(RarInfo): 

1762 """Shared fields for RAR5 records. 

1763 """ 

1764 extract_version = 50 

1765 header_crc = None 

1766 header_size = None 

1767 header_offset = None 

1768 data_offset = None 

1769 

1770 # type=all 

1771 block_type = None 

1772 block_flags = None 

1773 add_size = 0 

1774 block_extra_size = 0 

1775 

1776 # type=MAIN 

1777 volume_number = None 

1778 _md_class = None 

1779 _md_expect = None 

1780 

1781 def _must_disable_hack(self): 

1782 return False 

1783 

1784 

1785class Rar5BaseFile(Rar5Info): 

1786 """Shared sturct for file & service record. 

1787 """ 

1788 type = -1 

1789 file_flags = None 

1790 file_encryption = (0, 0, 0, b"", b"", b"") 

1791 file_compress_flags = None 

1792 file_redir = None 

1793 file_owner = None 

1794 file_version = None 

1795 blake2sp_hash = None 

1796 

1797 def _must_disable_hack(self): 

1798 if self.flags & RAR_FILE_PASSWORD: 

1799 return True 

1800 if self.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER): 

1801 return True 

1802 if self.file_compress_flags & RAR5_COMPR_SOLID: 

1803 return True 

1804 if self.file_redir: 

1805 return True 

1806 return False 

1807 

1808 

1809class Rar5FileInfo(Rar5BaseFile): 

1810 """RAR5 file record. 

1811 """ 

1812 type = RAR_BLOCK_FILE 

1813 

1814 def is_symlink(self): 

1815 """Returns True if entry is a symlink.""" 

1816 # pylint: disable=unsubscriptable-object 

1817 return ( 

1818 self.file_redir is not None and 

1819 self.file_redir[0] in ( 

1820 RAR5_XREDIR_UNIX_SYMLINK, 

1821 RAR5_XREDIR_WINDOWS_SYMLINK, 

1822 RAR5_XREDIR_WINDOWS_JUNCTION, 

1823 ) 

1824 ) 

1825 

1826 def is_file(self): 

1827 """Returns True if entry is a normal file.""" 

1828 return not (self.is_dir() or self.is_symlink()) 

1829 

1830 def is_dir(self): 

1831 """Returns True if entry is a directory.""" 

1832 if not self.file_redir: 

1833 if self.file_flags & RAR5_FILE_FLAG_ISDIR: 

1834 return True 

1835 return False 

1836 

1837 

1838class Rar5ServiceInfo(Rar5BaseFile): 

1839 """RAR5 service record. 

1840 """ 

1841 type = RAR_BLOCK_SUB 

1842 

1843 

1844class Rar5MainInfo(Rar5Info): 

1845 """RAR5 archive main record. 

1846 """ 

1847 type = RAR_BLOCK_MAIN 

1848 main_flags = None 

1849 main_volume_number = None 

1850 

1851 def _must_disable_hack(self): 

1852 if self.main_flags & RAR5_MAIN_FLAG_SOLID: 

1853 return True 

1854 return False 

1855 

1856 

1857class Rar5EncryptionInfo(Rar5Info): 

1858 """RAR5 archive header encryption record. 

1859 """ 

1860 type = RAR5_BLOCK_ENCRYPTION 

1861 encryption_algo = None 

1862 encryption_flags = None 

1863 encryption_kdf_count = None 

1864 encryption_salt = None 

1865 encryption_check_value = None 

1866 

1867 def needs_password(self): 

1868 return True 

1869 

1870 

1871class Rar5EndArcInfo(Rar5Info): 

1872 """RAR5 end of archive record. 

1873 """ 

1874 type = RAR_BLOCK_ENDARC 

1875 endarc_flags = None 

1876 

1877 

1878class RAR5Parser(CommonParser): 

1879 """Parse RAR5 format. 

1880 """ 

1881 _expect_sig = RAR5_ID 

1882 _hdrenc_main = None 

1883 

1884 # AES encrypted headers 

1885 _last_aes256_key = (-1, None, None) # (kdf_count, salt, key) 

1886 

1887 def _get_utf8_password(self): 

1888 pwd = self._password 

1889 if isinstance(pwd, str): 

1890 return pwd.encode("utf8") 

1891 return pwd 

1892 

1893 def _gen_key(self, kdf_count, salt): 

1894 if self._last_aes256_key[:2] == (kdf_count, salt): 

1895 return self._last_aes256_key[2] 

1896 if kdf_count > RAR_MAX_KDF_SHIFT: 

1897 raise BadRarFile("Too large kdf_count") 

1898 pwd = self._get_utf8_password() 

1899 key = rar5_s2k(pwd, salt, 1 << kdf_count) 

1900 self._last_aes256_key = (kdf_count, salt, key) 

1901 return key 

1902 

1903 def _decrypt_header(self, fd): 

1904 if not _have_crypto: 

1905 raise NoCrypto("Cannot parse encrypted headers - no crypto") 

1906 h = self._hdrenc_main 

1907 key = self._gen_key(h.encryption_kdf_count, h.encryption_salt) 

1908 iv = fd.read(16) 

1909 return HeaderDecrypt(fd, key, iv) 

1910 

1911 def _parse_block_header(self, fd): 

1912 """Parse common block header 

1913 """ 

1914 header_offset = fd.tell() 

1915 

1916 preload = 4 + 1 

1917 start_bytes = fd.read(preload) 

1918 if len(start_bytes) < preload: 

1919 self._set_error("Unexpected EOF when reading header") 

1920 return None 

1921 while start_bytes[-1] & 0x80: 

1922 b = fd.read(1) 

1923 if not b: 

1924 self._set_error("Unexpected EOF when reading header") 

1925 return None 

1926 start_bytes += b 

1927 header_crc, pos = load_le32(start_bytes, 0) 

1928 hdrlen, pos = load_vint(start_bytes, pos) 

1929 if hdrlen > 2 * 1024 * 1024: 

1930 return None 

1931 header_size = pos + hdrlen 

1932 

1933 # read full header, check for EOF 

1934 hdata = start_bytes + fd.read(header_size - len(start_bytes)) 

1935 if len(hdata) != header_size: 

1936 self._set_error("Unexpected EOF when reading header") 

1937 return None 

1938 data_offset = fd.tell() 

1939 

1940 calc_crc = crc32(memoryview(hdata)[4:]) 

1941 if header_crc != calc_crc: 

1942 # header parsing failed. 

1943 self._set_error("Header CRC error: exp=%x got=%x (xlen = %d)", 

1944 header_crc, calc_crc, len(hdata)) 

1945 return None 

1946 

1947 block_type, pos = load_vint(hdata, pos) 

1948 

1949 if block_type == RAR5_BLOCK_MAIN: 

1950 h, pos = self._parse_block_common(Rar5MainInfo(), hdata) 

1951 h = self._parse_main_block(h, hdata, pos) 

1952 elif block_type == RAR5_BLOCK_FILE: 

1953 h, pos = self._parse_block_common(Rar5FileInfo(), hdata) 

1954 h = self._parse_file_block(h, hdata, pos) 

1955 elif block_type == RAR5_BLOCK_SERVICE: 

1956 h, pos = self._parse_block_common(Rar5ServiceInfo(), hdata) 

1957 h = self._parse_file_block(h, hdata, pos) 

1958 elif block_type == RAR5_BLOCK_ENCRYPTION: 

1959 h, pos = self._parse_block_common(Rar5EncryptionInfo(), hdata) 

1960 h = self._parse_encryption_block(h, hdata, pos) 

1961 elif block_type == RAR5_BLOCK_ENDARC: 

1962 h, pos = self._parse_block_common(Rar5EndArcInfo(), hdata) 

1963 h = self._parse_endarc_block(h, hdata, pos) 

1964 else: 

1965 h = None 

1966 if h: 

1967 h.header_offset = header_offset 

1968 h.data_offset = data_offset 

1969 return h 

1970 

1971 def _parse_block_common(self, h, hdata): 

1972 h.header_crc, pos = load_le32(hdata, 0) 

1973 hdrlen, pos = load_vint(hdata, pos) 

1974 h.header_size = hdrlen + pos 

1975 h.block_type, pos = load_vint(hdata, pos) 

1976 h.block_flags, pos = load_vint(hdata, pos) 

1977 

1978 if h.block_flags & RAR5_BLOCK_FLAG_EXTRA_DATA: 

1979 h.block_extra_size, pos = load_vint(hdata, pos) 

1980 if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA: 

1981 h.add_size, pos = load_vint(hdata, pos) 

1982 

1983 h.compress_size = h.add_size 

1984 

1985 if h.block_flags & RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN: 

1986 h.flags |= RAR_SKIP_IF_UNKNOWN 

1987 if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA: 

1988 h.flags |= RAR_LONG_BLOCK 

1989 return h, pos 

1990 

1991 def _parse_main_block(self, h, hdata, pos): 

1992 h.main_flags, pos = load_vint(hdata, pos) 

1993 if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR: 

1994 h.main_volume_number, pos = load_vint(hdata, pos) 

1995 

1996 h.flags |= RAR_MAIN_NEWNUMBERING 

1997 if h.main_flags & RAR5_MAIN_FLAG_SOLID: 

1998 h.flags |= RAR_MAIN_SOLID 

1999 if h.main_flags & RAR5_MAIN_FLAG_ISVOL: 

2000 h.flags |= RAR_MAIN_VOLUME 

2001 if h.main_flags & RAR5_MAIN_FLAG_RECOVERY: 

2002 h.flags |= RAR_MAIN_RECOVERY 

2003 if self._hdrenc_main: 

2004 h.flags |= RAR_MAIN_PASSWORD 

2005 if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR == 0: 

2006 h.flags |= RAR_MAIN_FIRSTVOLUME 

2007 

2008 return h 

2009 

2010 def _parse_file_block(self, h, hdata, pos): 

2011 h.file_flags, pos = load_vint(hdata, pos) 

2012 h.file_size, pos = load_vint(hdata, pos) 

2013 h.mode, pos = load_vint(hdata, pos) 

2014 

2015 if h.file_flags & RAR5_FILE_FLAG_HAS_MTIME: 

2016 h.mtime, pos = load_unixtime(hdata, pos) 

2017 h.date_time = h.mtime.timetuple()[:6] 

2018 if h.file_flags & RAR5_FILE_FLAG_HAS_CRC32: 

2019 h.CRC, pos = load_le32(hdata, pos) 

2020 h._md_class = CRC32Context 

2021 h._md_expect = h.CRC 

2022 

2023 h.file_compress_flags, pos = load_vint(hdata, pos) 

2024 h.file_host_os, pos = load_vint(hdata, pos) 

2025 

2026 name, pos = load_vstr(hdata, pos) 

2027 nul = name.find(b"\0") 

2028 if nul >= 0: 

2029 name = name[:nul] 

2030 h.orig_filename = name 

2031 h.filename = h.orig_filename.decode("utf8", "replace").rstrip("/") 

2032 

2033 # use compatible values 

2034 if h.file_host_os == RAR5_OS_WINDOWS: 

2035 h.host_os = RAR_OS_WIN32 

2036 else: 

2037 h.host_os = RAR_OS_UNIX 

2038 h.compress_type = RAR_M0 + ((h.file_compress_flags >> 7) & 7) 

2039 

2040 if h.block_extra_size: 

2041 # allow 1 byte of garbage 

2042 while pos < len(hdata) - 1: 

2043 xsize, pos = load_vint(hdata, pos) 

2044 xdata, pos = load_bytes(hdata, xsize, pos) 

2045 self._process_file_extra(h, xdata) 

2046 

2047 if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE: 

2048 h.flags |= RAR_FILE_SPLIT_BEFORE 

2049 if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_AFTER: 

2050 h.flags |= RAR_FILE_SPLIT_AFTER 

2051 if h.file_flags & RAR5_FILE_FLAG_ISDIR: 

2052 h.flags |= RAR_FILE_DIRECTORY 

2053 if h.file_compress_flags & RAR5_COMPR_SOLID: 

2054 h.flags |= RAR_FILE_SOLID 

2055 

2056 if h.is_dir(): 

2057 h.filename = h.filename + "/" 

2058 return h 

2059 

2060 def _parse_endarc_block(self, h, hdata, pos): 

2061 h.endarc_flags, pos = load_vint(hdata, pos) 

2062 if h.endarc_flags & RAR5_ENDARC_FLAG_NEXT_VOL: 

2063 h.flags |= RAR_ENDARC_NEXT_VOLUME 

2064 return h 

2065 

2066 def _check_password(self, check_value, kdf_count_shift, salt): 

2067 if len(check_value) != RAR5_PW_CHECK_SIZE + RAR5_PW_SUM_SIZE: 

2068 return 

2069 if kdf_count_shift > RAR_MAX_KDF_SHIFT: 

2070 raise BadRarFile("Too large kdf_count") 

2071 

2072 hdr_check = check_value[:RAR5_PW_CHECK_SIZE] 

2073 hdr_sum = check_value[RAR5_PW_CHECK_SIZE:] 

2074 sum_hash = sha256(hdr_check).digest() 

2075 if sum_hash[:RAR5_PW_SUM_SIZE] != hdr_sum: 

2076 return 

2077 

2078 kdf_count = (1 << kdf_count_shift) + 32 

2079 pwd = self._get_utf8_password() 

2080 pwd_hash = rar5_s2k(pwd, salt, kdf_count) 

2081 

2082 pwd_check = bytearray(RAR5_PW_CHECK_SIZE) 

2083 len_mask = RAR5_PW_CHECK_SIZE - 1 

2084 for i, v in enumerate(pwd_hash): 

2085 pwd_check[i & len_mask] ^= v 

2086 

2087 if pwd_check != hdr_check: 

2088 raise RarWrongPassword() 

2089 

2090 def _parse_encryption_block(self, h, hdata, pos): 

2091 self._hdrenc_main = h 

2092 self._needs_password = True 

2093 h.encryption_algo, pos = load_vint(hdata, pos) 

2094 h.encryption_flags, pos = load_vint(hdata, pos) 

2095 h.encryption_kdf_count, pos = load_byte(hdata, pos) 

2096 h.encryption_salt, pos = load_bytes(hdata, 16, pos) 

2097 if h.encryption_flags & RAR5_ENC_FLAG_HAS_CHECKVAL: 

2098 h.encryption_check_value, pos = load_bytes(hdata, 12, pos) 

2099 if h.encryption_algo != RAR5_XENC_CIPHER_AES256: 

2100 raise BadRarFile("Unsupported header encryption cipher") 

2101 if h.encryption_check_value and self._password: 

2102 self._check_password(h.encryption_check_value, h.encryption_kdf_count, h.encryption_salt) 

2103 return h 

2104 

2105 def _process_file_extra(self, h, xdata): 

2106 xtype, pos = load_vint(xdata, 0) 

2107 if xtype == RAR5_XFILE_TIME: 

2108 self._parse_file_xtime(h, xdata, pos) 

2109 elif xtype == RAR5_XFILE_ENCRYPTION: 

2110 self._parse_file_encryption(h, xdata, pos) 

2111 elif xtype == RAR5_XFILE_HASH: 

2112 self._parse_file_hash(h, xdata, pos) 

2113 elif xtype == RAR5_XFILE_VERSION: 

2114 self._parse_file_version(h, xdata, pos) 

2115 elif xtype == RAR5_XFILE_REDIR: 

2116 self._parse_file_redir(h, xdata, pos) 

2117 elif xtype == RAR5_XFILE_OWNER: 

2118 self._parse_file_owner(h, xdata, pos) 

2119 elif xtype == RAR5_XFILE_SERVICE: 

2120 pass 

2121 else: 

2122 pass 

2123 

2124 # extra block for file time record 

2125 def _parse_file_xtime(self, h, xdata, pos): 

2126 tflags, pos = load_vint(xdata, pos) 

2127 

2128 ldr = load_windowstime 

2129 if tflags & RAR5_XTIME_UNIXTIME: 

2130 ldr = load_unixtime 

2131 

2132 if tflags & RAR5_XTIME_HAS_MTIME: 

2133 h.mtime, pos = ldr(xdata, pos) 

2134 h.date_time = h.mtime.timetuple()[:6] 

2135 if tflags & RAR5_XTIME_HAS_CTIME: 

2136 h.ctime, pos = ldr(xdata, pos) 

2137 if tflags & RAR5_XTIME_HAS_ATIME: 

2138 h.atime, pos = ldr(xdata, pos) 

2139 

2140 if tflags & RAR5_XTIME_UNIXTIME_NS: 

2141 if tflags & RAR5_XTIME_HAS_MTIME: 

2142 nsec, pos = load_le32(xdata, pos) 

2143 h.mtime = to_nsdatetime(h.mtime, nsec) 

2144 if tflags & RAR5_XTIME_HAS_CTIME: 

2145 nsec, pos = load_le32(xdata, pos) 

2146 h.ctime = to_nsdatetime(h.ctime, nsec) 

2147 if tflags & RAR5_XTIME_HAS_ATIME: 

2148 nsec, pos = load_le32(xdata, pos) 

2149 h.atime = to_nsdatetime(h.atime, nsec) 

2150 

2151 # just remember encryption info 

2152 def _parse_file_encryption(self, h, xdata, pos): 

2153 algo, pos = load_vint(xdata, pos) 

2154 flags, pos = load_vint(xdata, pos) 

2155 kdf_count, pos = load_byte(xdata, pos) 

2156 salt, pos = load_bytes(xdata, 16, pos) 

2157 iv, pos = load_bytes(xdata, 16, pos) 

2158 checkval = None 

2159 if flags & RAR5_XENC_CHECKVAL: 

2160 checkval, pos = load_bytes(xdata, 12, pos) 

2161 if flags & RAR5_XENC_TWEAKED: 

2162 h._md_expect = None 

2163 h._md_class = NoHashContext 

2164 

2165 h.file_encryption = (algo, flags, kdf_count, salt, iv, checkval) 

2166 h.flags |= RAR_FILE_PASSWORD 

2167 

2168 def _parse_file_hash(self, h, xdata, pos): 

2169 hash_type, pos = load_vint(xdata, pos) 

2170 if hash_type == RAR5_XHASH_BLAKE2SP: 

2171 h.blake2sp_hash, pos = load_bytes(xdata, 32, pos) 

2172 if (h.file_encryption[1] & RAR5_XENC_TWEAKED) == 0: 

2173 h._md_class = Blake2SP 

2174 h._md_expect = h.blake2sp_hash 

2175 

2176 def _parse_file_version(self, h, xdata, pos): 

2177 flags, pos = load_vint(xdata, pos) 

2178 version, pos = load_vint(xdata, pos) 

2179 h.file_version = (flags, version) 

2180 

2181 def _parse_file_redir(self, h, xdata, pos): 

2182 redir_type, pos = load_vint(xdata, pos) 

2183 redir_flags, pos = load_vint(xdata, pos) 

2184 redir_name, pos = load_vstr(xdata, pos) 

2185 redir_name = redir_name.decode("utf8", "replace") 

2186 h.file_redir = (redir_type, redir_flags, redir_name) 

2187 

2188 def _parse_file_owner(self, h, xdata, pos): 

2189 user_name = group_name = user_id = group_id = None 

2190 

2191 flags, pos = load_vint(xdata, pos) 

2192 if flags & RAR5_XOWNER_UNAME: 

2193 user_name, pos = load_vstr(xdata, pos) 

2194 if flags & RAR5_XOWNER_GNAME: 

2195 group_name, pos = load_vstr(xdata, pos) 

2196 if flags & RAR5_XOWNER_UID: 

2197 user_id, pos = load_vint(xdata, pos) 

2198 if flags & RAR5_XOWNER_GID: 

2199 group_id, pos = load_vint(xdata, pos) 

2200 

2201 h.file_owner = (user_name, group_name, user_id, group_id) 

2202 

2203 def process_entry(self, fd, item): 

2204 if item.block_type == RAR5_BLOCK_FILE: 

2205 if item.file_version: 

2206 pass # skip old versions 

2207 elif (item.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE) == 0: 

2208 # use only first part 

2209 self._info_map[item.filename.rstrip("/")] = item 

2210 self._info_list.append(item) 

2211 elif len(self._info_list) > 0: 

2212 # final crc is in last block 

2213 old = self._info_list[-1] 

2214 old.CRC = item.CRC 

2215 old._md_expect = item._md_expect 

2216 old.blake2sp_hash = item.blake2sp_hash 

2217 old.compress_size += item.compress_size 

2218 elif item.block_type == RAR5_BLOCK_SERVICE: 

2219 if item.filename == "CMT": 

2220 self._load_comment(fd, item) 

2221 

2222 def _load_comment(self, fd, item): 

2223 if item.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER): 

2224 return None 

2225 if item.compress_type != RAR_M0: 

2226 return None 

2227 if item.compress_size > RAR_MAX_COMMENT: 

2228 return None 

2229 if item.file_size > RAR_MAX_COMMENT: 

2230 return None 

2231 

2232 if item.flags & RAR_FILE_PASSWORD: 

2233 algo, ___flags, kdf_count, salt, iv, ___checkval = item.file_encryption 

2234 if algo != RAR5_XENC_CIPHER_AES256: 

2235 return None 

2236 key = self._gen_key(kdf_count, salt) 

2237 f = HeaderDecrypt(fd, key, iv) 

2238 cmt = f.read(item.file_size) 

2239 else: 

2240 # archive comment 

2241 with self._open_clear(item) as cmtstream: 

2242 cmt = cmtstream.read() 

2243 

2244 # rar bug? - appends zero to comment 

2245 cmt = cmt.split(b"\0", 1)[0] 

2246 self.comment = cmt.decode("utf8") 

2247 return None 

2248 

2249 def _open_hack(self, inf, pwd): 

2250 # len, type, blk_flags, flags 

2251 main_hdr = b"\x03\x01\x00\x00" 

2252 endarc_hdr = b"\x03\x05\x00\x00" 

2253 main_hdr = S_LONG.pack(crc32(main_hdr)) + main_hdr 

2254 endarc_hdr = S_LONG.pack(crc32(endarc_hdr)) + endarc_hdr 

2255 return self._open_hack_core(inf, pwd, RAR5_ID + main_hdr, endarc_hdr) 

2256 

2257 

2258## 

2259## Utility classes 

2260## 

2261 

2262class UnicodeFilename: 

2263 """Handle RAR3 unicode filename decompression. 

2264 """ 

2265 def __init__(self, name, encdata): 

2266 self.std_name = bytearray(name) 

2267 self.encdata = bytearray(encdata) 

2268 self.pos = self.encpos = 0 

2269 self.buf = bytearray() 

2270 self.failed = 0 

2271 

2272 def enc_byte(self): 

2273 """Copy encoded byte.""" 

2274 try: 

2275 c = self.encdata[self.encpos] 

2276 self.encpos += 1 

2277 return c 

2278 except IndexError: 

2279 self.failed = 1 

2280 return 0 

2281 

2282 def std_byte(self): 

2283 """Copy byte from 8-bit representation.""" 

2284 try: 

2285 return self.std_name[self.pos] 

2286 except IndexError: 

2287 self.failed = 1 

2288 return ord("?") 

2289 

2290 def put(self, lo, hi): 

2291 """Copy 16-bit value to result.""" 

2292 self.buf.append(lo) 

2293 self.buf.append(hi) 

2294 self.pos += 1 

2295 

2296 def decode(self): 

2297 """Decompress compressed UTF16 value.""" 

2298 hi = self.enc_byte() 

2299 flagbits = 0 

2300 while self.encpos < len(self.encdata): 

2301 if flagbits == 0: 

2302 flags = self.enc_byte() 

2303 flagbits = 8 

2304 flagbits -= 2 

2305 t = (flags >> flagbits) & 3 

2306 if t == 0: 

2307 self.put(self.enc_byte(), 0) 

2308 elif t == 1: 

2309 self.put(self.enc_byte(), hi) 

2310 elif t == 2: 

2311 self.put(self.enc_byte(), self.enc_byte()) 

2312 else: 

2313 n = self.enc_byte() 

2314 if n & 0x80: 

2315 c = self.enc_byte() 

2316 for _ in range((n & 0x7f) + 2): 

2317 lo = (self.std_byte() + c) & 0xFF 

2318 self.put(lo, hi) 

2319 else: 

2320 for _ in range(n + 2): 

2321 self.put(self.std_byte(), 0) 

2322 return self.buf.decode("utf-16le", "replace") 

2323 

2324 

2325class RarExtFile(io.RawIOBase): 

2326 """Base class for file-like object that :meth:`RarFile.open` returns. 

2327 

2328 Provides public methods and common crc checking. 

2329 

2330 Behaviour: 

2331 - no short reads - .read() and .readinfo() read as much as requested. 

2332 - no internal buffer, use io.BufferedReader for that. 

2333 """ 

2334 name = None #: Filename of the archive entry 

2335 mode = "rb" 

2336 _parser = None 

2337 _inf = None 

2338 _fd = None 

2339 _remain = 0 

2340 _returncode = 0 

2341 _md_context = None 

2342 _seeking = False 

2343 

2344 def _open_extfile(self, parser, inf): 

2345 self.name = inf.filename 

2346 self._parser = parser 

2347 self._inf = inf 

2348 

2349 if self._fd: 

2350 self._fd.close() 

2351 if self._seeking: 

2352 md_class = NoHashContext 

2353 else: 

2354 md_class = self._inf._md_class or NoHashContext 

2355 self._md_context = md_class() 

2356 self._fd = None 

2357 self._remain = self._inf.file_size 

2358 

2359 def read(self, n=-1): 

2360 """Read all or specified amount of data from archive entry.""" 

2361 

2362 # sanitize count 

2363 if n is None or n < 0: 

2364 n = self._remain 

2365 elif n > self._remain: 

2366 n = self._remain 

2367 if n == 0: 

2368 return b"" 

2369 

2370 buf = [] 

2371 orig = n 

2372 while n > 0: 

2373 # actual read 

2374 data = self._read(n) 

2375 if not data: 

2376 break 

2377 buf.append(data) 

2378 self._md_context.update(data) 

2379 self._remain -= len(data) 

2380 n -= len(data) 

2381 data = b"".join(buf) 

2382 if n > 0: 

2383 if self._returncode: 

2384 check_returncode(self._returncode, "", tool_setup().get_errmap()) 

2385 raise BadRarFile("Failed the read enough data: req=%d got=%d" % (orig, len(data))) 

2386 

2387 # done? 

2388 if not data or self._remain == 0: 

2389 # self.close() 

2390 self._check() 

2391 return data 

2392 

2393 def _check(self): 

2394 """Check final CRC.""" 

2395 final = self._md_context.digest() 

2396 exp = self._inf._md_expect 

2397 if exp is None: 

2398 return 

2399 if final is None: 

2400 return 

2401 if self._returncode: 

2402 check_returncode(self._returncode, "", tool_setup().get_errmap()) 

2403 if self._remain != 0: 

2404 raise BadRarFile("Failed the read enough data") 

2405 if final != exp: 

2406 raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % ( 

2407 self._inf.filename, exp, final)) 

2408 

2409 def _read(self, cnt): 

2410 """Actual read that gets sanitized cnt.""" 

2411 raise NotImplementedError("_read") 

2412 

2413 def close(self): 

2414 """Close open resources.""" 

2415 

2416 super().close() 

2417 

2418 if self._fd: 

2419 self._fd.close() 

2420 self._fd = None 

2421 

2422 def __del__(self): 

2423 """Hook delete to make sure tempfile is removed.""" 

2424 self.close() 

2425 

2426 def readinto(self, buf): 

2427 """Zero-copy read directly into buffer. 

2428 

2429 Returns bytes read. 

2430 """ 

2431 raise NotImplementedError("readinto") 

2432 

2433 def tell(self): 

2434 """Return current reading position in uncompressed data.""" 

2435 return self._inf.file_size - self._remain 

2436 

2437 def seek(self, offset, whence=0): 

2438 """Seek in data. 

2439 

2440 On uncompressed files, the seeking works by actual 

2441 seeks so it's fast. On compressed files its slow 

2442 - forward seeking happens by reading ahead, 

2443 backwards by re-opening and decompressing from the start. 

2444 """ 

2445 

2446 # disable crc check when seeking 

2447 if not self._seeking: 

2448 self._md_context = NoHashContext() 

2449 self._seeking = True 

2450 

2451 fsize = self._inf.file_size 

2452 cur_ofs = self.tell() 

2453 

2454 if whence == 0: # seek from beginning of file 

2455 new_ofs = offset 

2456 elif whence == 1: # seek from current position 

2457 new_ofs = cur_ofs + offset 

2458 elif whence == 2: # seek from end of file 

2459 new_ofs = fsize + offset 

2460 else: 

2461 raise ValueError("Invalid value for whence") 

2462 

2463 # sanity check 

2464 if new_ofs < 0: 

2465 new_ofs = 0 

2466 elif new_ofs > fsize: 

2467 new_ofs = fsize 

2468 

2469 # do the actual seek 

2470 if new_ofs >= cur_ofs: 

2471 self._skip(new_ofs - cur_ofs) 

2472 else: 

2473 # reopen and seek 

2474 self._open_extfile(self._parser, self._inf) 

2475 self._skip(new_ofs) 

2476 return self.tell() 

2477 

2478 def _skip(self, cnt): 

2479 """Read and discard data""" 

2480 empty_read(self, cnt, BSIZE) 

2481 

2482 def readable(self): 

2483 """Returns True""" 

2484 return True 

2485 

2486 def writable(self): 

2487 """Returns False. 

2488 

2489 Writing is not supported. 

2490 """ 

2491 return False 

2492 

2493 def seekable(self): 

2494 """Returns True. 

2495 

2496 Seeking is supported, although it's slow on compressed files. 

2497 """ 

2498 return True 

2499 

2500 def readall(self): 

2501 """Read all remaining data""" 

2502 # avoid RawIOBase default impl 

2503 return self.read() 

2504 

2505 

2506class PipeReader(RarExtFile): 

2507 """Read data from pipe, handle tempfile cleanup.""" 

2508 

2509 def __init__(self, parser, inf, cmd, tempfile=None): 

2510 super().__init__() 

2511 self._cmd = cmd 

2512 self._proc = None 

2513 self._tempfile = tempfile 

2514 self._open_extfile(parser, inf) 

2515 

2516 def _close_proc(self): 

2517 if not self._proc: 

2518 return 

2519 for f in (self._proc.stdout, self._proc.stderr, self._proc.stdin): 

2520 if f: 

2521 f.close() 

2522 self._proc.wait() 

2523 self._returncode = self._proc.returncode 

2524 self._proc = None 

2525 

2526 def _open_extfile(self, parser, inf): 

2527 super()._open_extfile(parser, inf) 

2528 

2529 # stop old process 

2530 self._close_proc() 

2531 

2532 # launch new process 

2533 self._returncode = 0 

2534 self._proc = custom_popen(self._cmd) 

2535 self._fd = self._proc.stdout 

2536 

2537 def _read(self, cnt): 

2538 """Read from pipe.""" 

2539 

2540 # normal read is usually enough 

2541 data = self._fd.read(cnt) 

2542 if len(data) == cnt or not data: 

2543 return data 

2544 

2545 # short read, try looping 

2546 buf = [data] 

2547 cnt -= len(data) 

2548 while cnt > 0: 

2549 data = self._fd.read(cnt) 

2550 if not data: 

2551 break 

2552 cnt -= len(data) 

2553 buf.append(data) 

2554 return b"".join(buf) 

2555 

2556 def close(self): 

2557 """Close open resources.""" 

2558 

2559 self._close_proc() 

2560 super().close() 

2561 

2562 if self._tempfile: 

2563 try: 

2564 os.unlink(self._tempfile) 

2565 except OSError: 

2566 pass 

2567 self._tempfile = None 

2568 

2569 def readinto(self, buf): 

2570 """Zero-copy read directly into buffer.""" 

2571 cnt = len(buf) 

2572 if cnt > self._remain: 

2573 cnt = self._remain 

2574 vbuf = memoryview(buf) 

2575 res = got = 0 

2576 while got < cnt: 

2577 res = self._fd.readinto(vbuf[got: cnt]) 

2578 if not res: 

2579 break 

2580 self._md_context.update(vbuf[got: got + res]) 

2581 self._remain -= res 

2582 got += res 

2583 return got 

2584 

2585 

2586class DirectReader(RarExtFile): 

2587 """Read uncompressed data directly from archive. 

2588 """ 

2589 _cur = None 

2590 _cur_avail = None 

2591 _volfile = None 

2592 

2593 def __init__(self, parser, inf): 

2594 super().__init__() 

2595 self._open_extfile(parser, inf) 

2596 

2597 def _open_extfile(self, parser, inf): 

2598 super()._open_extfile(parser, inf) 

2599 

2600 self._volfile = self._inf.volume_file 

2601 self._fd = XFile(self._volfile, 0) 

2602 self._fd.seek(self._inf.header_offset, 0) 

2603 self._cur = self._parser._parse_header(self._fd) 

2604 self._cur_avail = self._cur.add_size 

2605 

2606 def _skip(self, cnt): 

2607 """RAR Seek, skipping through rar files to get to correct position 

2608 """ 

2609 

2610 while cnt > 0: 

2611 # next vol needed? 

2612 if self._cur_avail == 0: 

2613 if not self._open_next(): 

2614 break 

2615 

2616 # fd is in read pos, do the read 

2617 if cnt > self._cur_avail: 

2618 cnt -= self._cur_avail 

2619 self._remain -= self._cur_avail 

2620 self._cur_avail = 0 

2621 else: 

2622 self._fd.seek(cnt, 1) 

2623 self._cur_avail -= cnt 

2624 self._remain -= cnt 

2625 cnt = 0 

2626 

2627 def _read(self, cnt): 

2628 """Read from potentially multi-volume archive.""" 

2629 

2630 pos = self._fd.tell() 

2631 need = self._cur.data_offset + self._cur.add_size - self._cur_avail 

2632 if pos != need: 

2633 self._fd.seek(need, 0) 

2634 

2635 buf = [] 

2636 while cnt > 0: 

2637 # next vol needed? 

2638 if self._cur_avail == 0: 

2639 if not self._open_next(): 

2640 break 

2641 

2642 # fd is in read pos, do the read 

2643 if cnt > self._cur_avail: 

2644 data = self._fd.read(self._cur_avail) 

2645 else: 

2646 data = self._fd.read(cnt) 

2647 if not data: 

2648 break 

2649 

2650 # got some data 

2651 cnt -= len(data) 

2652 self._cur_avail -= len(data) 

2653 buf.append(data) 

2654 

2655 if len(buf) == 1: 

2656 return buf[0] 

2657 return b"".join(buf) 

2658 

2659 def _open_next(self): 

2660 """Proceed to next volume.""" 

2661 

2662 # is the file split over archives? 

2663 if (self._cur.flags & RAR_FILE_SPLIT_AFTER) == 0: 

2664 return False 

2665 

2666 if self._fd: 

2667 self._fd.close() 

2668 self._fd = None 

2669 

2670 # open next part 

2671 self._volfile = self._parser._next_volname(self._volfile) 

2672 fd = open(self._volfile, "rb", 0) 

2673 self._fd = fd 

2674 sig = fd.read(len(self._parser._expect_sig)) 

2675 if sig != self._parser._expect_sig: 

2676 raise BadRarFile("Invalid signature") 

2677 

2678 # loop until first file header 

2679 while True: 

2680 cur = self._parser._parse_header(fd) 

2681 if not cur: 

2682 raise BadRarFile("Unexpected EOF") 

2683 if cur.type in (RAR_BLOCK_MARK, RAR_BLOCK_MAIN): 

2684 if cur.add_size: 

2685 fd.seek(cur.add_size, 1) 

2686 continue 

2687 if cur.orig_filename != self._inf.orig_filename: 

2688 raise BadRarFile("Did not found file entry") 

2689 self._cur = cur 

2690 self._cur_avail = cur.add_size 

2691 return True 

2692 

2693 def readinto(self, buf): 

2694 """Zero-copy read directly into buffer.""" 

2695 got = 0 

2696 vbuf = memoryview(buf) 

2697 while got < len(buf): 

2698 # next vol needed? 

2699 if self._cur_avail == 0: 

2700 if not self._open_next(): 

2701 break 

2702 

2703 # length for next read 

2704 cnt = len(buf) - got 

2705 if cnt > self._cur_avail: 

2706 cnt = self._cur_avail 

2707 

2708 # read into temp view 

2709 res = self._fd.readinto(vbuf[got: got + cnt]) 

2710 if not res: 

2711 break 

2712 self._md_context.update(vbuf[got: got + res]) 

2713 self._cur_avail -= res 

2714 self._remain -= res 

2715 got += res 

2716 return got 

2717 

2718 

2719class HeaderDecrypt: 

2720 """File-like object that decrypts from another file""" 

2721 def __init__(self, f, key, iv): 

2722 self.f = f 

2723 self.ciph = AES_CBC_Decrypt(key, iv) 

2724 self.buf = b"" 

2725 

2726 def tell(self): 

2727 """Current file pos - works only on block boundaries.""" 

2728 return self.f.tell() 

2729 

2730 def read(self, cnt=None): 

2731 """Read and decrypt.""" 

2732 if cnt > 8 * 1024: 

2733 raise BadRarFile("Bad count to header decrypt - wrong password?") 

2734 

2735 # consume old data 

2736 if cnt <= len(self.buf): 

2737 res = self.buf[:cnt] 

2738 self.buf = self.buf[cnt:] 

2739 return res 

2740 res = self.buf 

2741 self.buf = b"" 

2742 cnt -= len(res) 

2743 

2744 # decrypt new data 

2745 blklen = 16 

2746 while cnt > 0: 

2747 enc = self.f.read(blklen) 

2748 if len(enc) < blklen: 

2749 break 

2750 dec = self.ciph.decrypt(enc) 

2751 if cnt >= len(dec): 

2752 res += dec 

2753 cnt -= len(dec) 

2754 else: 

2755 res += dec[:cnt] 

2756 self.buf = dec[cnt:] 

2757 cnt = 0 

2758 

2759 return res 

2760 

2761 

2762class XFile: 

2763 """Input may be filename or file object. 

2764 """ 

2765 __slots__ = ("_fd", "_need_close", "_initial_pos") 

2766 

2767 def __init__(self, xfile, bufsize=1024): 

2768 if is_filelike(xfile): 

2769 self._initial_pos = xfile.tell() 

2770 self._need_close = False 

2771 self._fd = xfile 

2772 self._fd.seek(0) 

2773 else: 

2774 self._initial_pos = None 

2775 self._need_close = True 

2776 self._fd = open(xfile, "rb", bufsize) 

2777 

2778 def restore_pos(self): 

2779 if self._initial_pos is None: 

2780 return 

2781 try: 

2782 self._fd.seek(self._initial_pos) 

2783 except: 

2784 pass 

2785 

2786 def read(self, n=None): 

2787 """Read from file.""" 

2788 return self._fd.read(n) 

2789 

2790 def tell(self): 

2791 """Return file pos.""" 

2792 return self._fd.tell() 

2793 

2794 def seek(self, ofs, whence=0): 

2795 """Move file pos.""" 

2796 return self._fd.seek(ofs, whence) 

2797 

2798 def readinto(self, buf): 

2799 """Read into buffer.""" 

2800 return self._fd.readinto(buf) 

2801 

2802 def close(self): 

2803 """Close file object.""" 

2804 if self._need_close: 

2805 self._fd.close() 

2806 

2807 def __enter__(self): 

2808 return self 

2809 

2810 def __exit__(self, typ, val, tb): 

2811 self.close() 

2812 

2813 

2814class NoHashContext: 

2815 """No-op hash function.""" 

2816 def __init__(self, data=None): 

2817 """Initialize""" 

2818 def update(self, data): 

2819 """Update data""" 

2820 def digest(self): 

2821 """Final hash""" 

2822 def hexdigest(self): 

2823 """Hexadecimal digest.""" 

2824 

2825 

2826class CRC32Context: 

2827 """Hash context that uses CRC32.""" 

2828 __slots__ = ["_crc"] 

2829 

2830 def __init__(self, data=None): 

2831 self._crc = 0 

2832 if data: 

2833 self.update(data) 

2834 

2835 def update(self, data): 

2836 """Process data.""" 

2837 self._crc = crc32(data, self._crc) 

2838 

2839 def digest(self): 

2840 """Final hash.""" 

2841 return self._crc 

2842 

2843 def hexdigest(self): 

2844 """Hexadecimal digest.""" 

2845 return "%08x" % self.digest() 

2846 

2847 

2848class Blake2SP: 

2849 """Blake2sp hash context. 

2850 """ 

2851 __slots__ = ["_thread", "_buf", "_cur", "_digest"] 

2852 digest_size = 32 

2853 block_size = 64 

2854 parallelism = 8 

2855 

2856 def __init__(self, data=None): 

2857 self._buf = b"" 

2858 self._cur = 0 

2859 self._digest = None 

2860 self._thread = [] 

2861 

2862 for i in range(self.parallelism): 

2863 ctx = self._blake2s(i, 0, i == (self.parallelism - 1)) 

2864 self._thread.append(ctx) 

2865 

2866 if data: 

2867 self.update(data) 

2868 

2869 def _blake2s(self, ofs, depth, is_last): 

2870 return blake2s(node_offset=ofs, node_depth=depth, last_node=is_last, 

2871 depth=2, inner_size=32, fanout=self.parallelism) 

2872 

2873 def _add_block(self, blk): 

2874 self._thread[self._cur].update(blk) 

2875 self._cur = (self._cur + 1) % self.parallelism 

2876 

2877 def update(self, data): 

2878 """Hash data. 

2879 """ 

2880 view = memoryview(data) 

2881 bs = self.block_size 

2882 if self._buf: 

2883 need = bs - len(self._buf) 

2884 if len(view) < need: 

2885 self._buf += view.tobytes() 

2886 return 

2887 self._add_block(self._buf + view[:need].tobytes()) 

2888 view = view[need:] 

2889 while len(view) >= bs: 

2890 self._add_block(view[:bs]) 

2891 view = view[bs:] 

2892 self._buf = view.tobytes() 

2893 

2894 def digest(self): 

2895 """Return final digest value. 

2896 """ 

2897 if self._digest is None: 

2898 if self._buf: 

2899 self._add_block(self._buf) 

2900 self._buf = b"" 

2901 ctx = self._blake2s(0, 1, True) 

2902 for t in self._thread: 

2903 ctx.update(t.digest()) 

2904 self._digest = ctx.digest() 

2905 return self._digest 

2906 

2907 def hexdigest(self): 

2908 """Hexadecimal digest.""" 

2909 return hexlify(self.digest()).decode("ascii") 

2910 

2911 

2912class Rar3Sha1: 

2913 """Emulate buggy SHA1 from RAR3. 

2914 """ 

2915 digest_size = 20 

2916 block_size = 64 

2917 

2918 _BLK_BE = struct.Struct(b">16L") 

2919 _BLK_LE = struct.Struct(b"<16L") 

2920 

2921 __slots__ = ("_nbytes", "_md", "_rarbug") 

2922 

2923 def __init__(self, data=b"", rarbug=False): 

2924 self._md = sha1() 

2925 self._nbytes = 0 

2926 self._rarbug = rarbug 

2927 self.update(data) 

2928 

2929 def update(self, data): 

2930 """Process more data.""" 

2931 self._md.update(data) 

2932 bufpos = self._nbytes & 63 

2933 self._nbytes += len(data) 

2934 

2935 if self._rarbug and len(data) > 64: 

2936 dpos = self.block_size - bufpos 

2937 while dpos + self.block_size <= len(data): 

2938 self._corrupt(data, dpos) 

2939 dpos += self.block_size 

2940 

2941 def digest(self): 

2942 """Return final state.""" 

2943 return self._md.digest() 

2944 

2945 def hexdigest(self): 

2946 """Return final state as hex string.""" 

2947 return self._md.hexdigest() 

2948 

2949 def _corrupt(self, data, dpos): 

2950 """Corruption from SHA1 core.""" 

2951 ws = list(self._BLK_BE.unpack_from(data, dpos)) 

2952 for t in range(16, 80): 

2953 tmp = ws[(t - 3) & 15] ^ ws[(t - 8) & 15] ^ ws[(t - 14) & 15] ^ ws[(t - 16) & 15] 

2954 ws[t & 15] = ((tmp << 1) | (tmp >> (32 - 1))) & 0xFFFFFFFF 

2955 self._BLK_LE.pack_into(data, dpos, *ws) 

2956 

2957 

2958## 

2959## Utility functions 

2960## 

2961 

2962S_LONG = Struct("<L") 

2963S_SHORT = Struct("<H") 

2964S_BYTE = Struct("<B") 

2965 

2966S_BLK_HDR = Struct("<HBHH") 

2967S_FILE_HDR = Struct("<LLBLLBBHL") 

2968S_COMMENT_HDR = Struct("<HBBH") 

2969S_OLD_SUBBLOCK_HDR = Struct("<HB") 

2970 

2971def load_vint(buf, pos): 

2972 """Load RAR5 variable-size int.""" 

2973 limit = min(pos + 11, len(buf)) 

2974 res = ofs = 0 

2975 while pos < limit: 

2976 b = buf[pos] 

2977 res += ((b & 0x7F) << ofs) 

2978 pos += 1 

2979 ofs += 7 

2980 if b < 0x80: 

2981 return res, pos 

2982 raise BadRarFile("cannot load vint") 

2983 

2984 

2985def load_byte(buf, pos): 

2986 """Load single byte""" 

2987 end = pos + 1 

2988 if end > len(buf): 

2989 raise BadRarFile("cannot load byte") 

2990 return S_BYTE.unpack_from(buf, pos)[0], end 

2991 

2992 

2993def load_le32(buf, pos): 

2994 """Load little-endian 32-bit integer""" 

2995 end = pos + 4 

2996 if end > len(buf): 

2997 raise BadRarFile("cannot load le32") 

2998 return S_LONG.unpack_from(buf, pos)[0], end 

2999 

3000 

3001def load_bytes(buf, num, pos): 

3002 """Load sequence of bytes""" 

3003 end = pos + num 

3004 if end > len(buf): 

3005 raise BadRarFile("cannot load bytes") 

3006 return buf[pos: end], end 

3007 

3008 

3009def load_vstr(buf, pos): 

3010 """Load bytes prefixed by vint length""" 

3011 slen, pos = load_vint(buf, pos) 

3012 return load_bytes(buf, slen, pos) 

3013 

3014 

3015def load_dostime(buf, pos): 

3016 """Load LE32 dos timestamp""" 

3017 stamp, pos = load_le32(buf, pos) 

3018 tup = parse_dos_time(stamp) 

3019 return to_datetime(tup), pos 

3020 

3021 

3022def load_unixtime(buf, pos): 

3023 """Load LE32 unix timestamp""" 

3024 secs, pos = load_le32(buf, pos) 

3025 dt = datetime.fromtimestamp(secs, timezone.utc) 

3026 return dt, pos 

3027 

3028 

3029def load_windowstime(buf, pos): 

3030 """Load LE64 windows timestamp""" 

3031 # unix epoch (1970) in seconds from windows epoch (1601) 

3032 unix_epoch = 11644473600 

3033 val1, pos = load_le32(buf, pos) 

3034 val2, pos = load_le32(buf, pos) 

3035 secs, n1secs = divmod((val2 << 32) | val1, 10000000) 

3036 dt = datetime.fromtimestamp(secs - unix_epoch, timezone.utc) 

3037 dt = to_nsdatetime(dt, n1secs * 100) 

3038 return dt, pos 

3039 

3040 

3041# 

3042# volume numbering 

3043# 

3044 

3045_rc_num = re.compile('^[0-9]+$') 

3046 

3047 

3048def _next_newvol(volfile): 

3049 """New-style next volume 

3050 """ 

3051 name, ext = os.path.splitext(volfile) 

3052 if ext.lower() in ("", ".exe", ".sfx"): 

3053 volfile = name + ".rar" 

3054 i = len(volfile) - 1 

3055 while i >= 0: 

3056 if "0" <= volfile[i] <= "9": 

3057 return _inc_volname(volfile, i, False) 

3058 if volfile[i] in ("/", os.sep): 

3059 break 

3060 i -= 1 

3061 raise BadRarName("Cannot construct volume name: " + volfile) 

3062 

3063 

3064 

3065def _next_oldvol(volfile): 

3066 """Old-style next volume 

3067 """ 

3068 name, ext = os.path.splitext(volfile) 

3069 if ext.lower() in ("", ".exe", ".sfx"): 

3070 ext = ".rar" 

3071 sfx = ext[2:] 

3072 if _rc_num.match(sfx): 

3073 ext = _inc_volname(ext, len(ext) - 1, True) 

3074 else: 

3075 # .rar -> .r00 

3076 ext = ext[:2] + "00" 

3077 return name + ext 

3078 

3079 

3080def _inc_volname(volfile, i, inc_chars): 

3081 """increase digits with carry, otherwise just increment char 

3082 """ 

3083 fn = list(volfile) 

3084 while i >= 0: 

3085 if fn[i] == "9": 

3086 fn[i] = "0" 

3087 i -= 1 

3088 if i < 0: 

3089 fn.insert(0, "1") 

3090 elif "0" <= fn[i] < "9" or inc_chars: 

3091 fn[i] = chr(ord(fn[i]) + 1) 

3092 break 

3093 else: 

3094 fn.insert(i + 1, "1") 

3095 break 

3096 return "".join(fn) 

3097 

3098 

3099def _parse_ext_time(h, data, pos): 

3100 """Parse all RAR3 extended time fields 

3101 """ 

3102 # flags and rest of data can be missing 

3103 flags = 0 

3104 if pos + 2 <= len(data): 

3105 flags = S_SHORT.unpack_from(data, pos)[0] 

3106 pos += 2 

3107 

3108 mtime, pos = _parse_xtime(flags >> 3 * 4, data, pos, h.mtime) 

3109 h.ctime, pos = _parse_xtime(flags >> 2 * 4, data, pos) 

3110 h.atime, pos = _parse_xtime(flags >> 1 * 4, data, pos) 

3111 h.arctime, pos = _parse_xtime(flags >> 0 * 4, data, pos) 

3112 if mtime: 

3113 h.mtime = mtime 

3114 h.date_time = mtime.timetuple()[:6] 

3115 return pos 

3116 

3117 

3118def _parse_xtime(flag, data, pos, basetime=None): 

3119 """Parse one RAR3 extended time field 

3120 """ 

3121 res = None 

3122 if flag & 8: 

3123 if not basetime: 

3124 basetime, pos = load_dostime(data, pos) 

3125 

3126 # load second fractions of 100ns units 

3127 rem = 0 

3128 cnt = flag & 3 

3129 for _ in range(cnt): 

3130 b, pos = load_byte(data, pos) 

3131 rem = (b << 16) | (rem >> 8) 

3132 

3133 # dostime has room for 30 seconds only, correct if needed 

3134 if flag & 4 and basetime.second < 59: 

3135 basetime = basetime.replace(second=basetime.second + 1) 

3136 

3137 res = to_nsdatetime(basetime, rem * 100) 

3138 return res, pos 

3139 

3140 

3141def is_filelike(obj): 

3142 """Filename or file object? 

3143 """ 

3144 if isinstance(obj, (bytes, str, Path)): 

3145 return False 

3146 res = True 

3147 for a in ("read", "tell", "seek"): 

3148 res = res and hasattr(obj, a) 

3149 if not res: 

3150 raise ValueError("Invalid object passed as file") 

3151 return True 

3152 

3153 

3154def rar5_s2k(pwd, salt, kdf_count): 

3155 """String-to-key hash for RAR5. 

3156 """ 

3157 if not isinstance(pwd, str): 

3158 pwd = pwd.decode("utf8") 

3159 wstr = pwd.encode("utf-16le")[:RAR_MAX_PASSWORD*2] 

3160 ustr = wstr.decode("utf-16le").encode("utf8") 

3161 return pbkdf2_hmac("sha256", ustr, salt, kdf_count) 

3162 

3163 

3164def rar3_s2k(pwd, salt): 

3165 """String-to-key hash for RAR3. 

3166 """ 

3167 if not isinstance(pwd, str): 

3168 pwd = pwd.decode("utf8") 

3169 wstr = pwd.encode("utf-16le")[:RAR_MAX_PASSWORD*2] 

3170 seed = bytearray(wstr + salt) 

3171 h = Rar3Sha1(rarbug=True) 

3172 iv = b"" 

3173 for i in range(16): 

3174 for j in range(0x4000): 

3175 cnt = S_LONG.pack(i * 0x4000 + j) 

3176 h.update(seed) 

3177 h.update(cnt[:3]) 

3178 if j == 0: 

3179 iv += h.digest()[19:20] 

3180 key_be = h.digest()[:16] 

3181 key_le = pack("<LLLL", *unpack(">LLLL", key_be)) 

3182 return key_le, iv 

3183 

3184 

3185def rar3_decompress(vers, meth, data, declen=0, flags=0, crc=0, pwd=None, salt=None): 

3186 """Decompress blob of compressed data. 

3187 

3188 Used for data with non-standard header - eg. comments. 

3189 """ 

3190 # already uncompressed? 

3191 if meth == RAR_M0 and (flags & RAR_FILE_PASSWORD) == 0: 

3192 return data 

3193 

3194 # take only necessary flags 

3195 flags = flags & (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK) 

3196 flags |= RAR_LONG_BLOCK 

3197 

3198 # file header 

3199 fname = b"data" 

3200 date = ((2010 - 1980) << 25) + (12 << 21) + (31 << 16) 

3201 mode = DOS_MODE_ARCHIVE 

3202 fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc, 

3203 date, vers, meth, len(fname), mode) 

3204 fhdr += fname 

3205 if salt: 

3206 fhdr += salt 

3207 

3208 # full header 

3209 hlen = S_BLK_HDR.size + len(fhdr) 

3210 hdr = S_BLK_HDR.pack(0, RAR_BLOCK_FILE, flags, hlen) + fhdr 

3211 hcrc = crc32(hdr[2:]) & 0xFFFF 

3212 hdr = S_BLK_HDR.pack(hcrc, RAR_BLOCK_FILE, flags, hlen) + fhdr 

3213 

3214 # archive main header 

3215 mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + b"\0" * (2 + 4) 

3216 

3217 # decompress via temp rar 

3218 setup = tool_setup() 

3219 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR) 

3220 tmpf = os.fdopen(tmpfd, "wb") 

3221 try: 

3222 tmpf.write(RAR_ID + mh + hdr + data) 

3223 tmpf.close() 

3224 

3225 curpwd = (flags & RAR_FILE_PASSWORD) and pwd or None 

3226 cmd = setup.open_cmdline(curpwd, tmpname) 

3227 p = custom_popen(cmd) 

3228 return p.communicate()[0] 

3229 finally: 

3230 tmpf.close() 

3231 os.unlink(tmpname) 

3232 

3233 

3234def sanitize_filename(fname, pathsep, is_win32): 

3235 """Make filename safe for write access. 

3236 """ 

3237 if is_win32: 

3238 if len(fname) > 1 and fname[1] == ":": 

3239 fname = fname[2:] 

3240 rc = RC_BAD_CHARS_WIN32 

3241 else: 

3242 rc = RC_BAD_CHARS_UNIX 

3243 if rc.search(fname): 

3244 fname = rc.sub("_", fname) 

3245 

3246 parts = [] 

3247 for seg in fname.split("/"): 

3248 if seg in ("", ".", ".."): 

3249 continue 

3250 if is_win32 and seg[-1] in (" ", "."): 

3251 seg = seg[:-1] + "_" 

3252 parts.append(seg) 

3253 return pathsep.join(parts) 

3254 

3255 

3256def empty_read(src, size, blklen): 

3257 """Read and drop fixed amount of data. 

3258 """ 

3259 while size > 0: 

3260 if size > blklen: 

3261 res = src.read(blklen) 

3262 else: 

3263 res = src.read(size) 

3264 if not res: 

3265 raise BadRarFile("cannot load data") 

3266 size -= len(res) 

3267 

3268 

3269def to_datetime(t): 

3270 """Convert 6-part time tuple into datetime object. 

3271 """ 

3272 # extract values 

3273 year, mon, day, h, m, s = t 

3274 

3275 # assume the values are valid 

3276 try: 

3277 return datetime(year, mon, day, h, m, s) 

3278 except ValueError: 

3279 pass 

3280 

3281 # sanitize invalid values 

3282 mday = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) 

3283 mon = max(1, min(mon, 12)) 

3284 day = max(1, min(day, mday[mon])) 

3285 h = min(h, 23) 

3286 m = min(m, 59) 

3287 s = min(s, 59) 

3288 return datetime(year, mon, day, h, m, s) 

3289 

3290 

3291def parse_dos_time(stamp): 

3292 """Parse standard 32-bit DOS timestamp. 

3293 """ 

3294 sec, stamp = stamp & 0x1F, stamp >> 5 

3295 mn, stamp = stamp & 0x3F, stamp >> 6 

3296 hr, stamp = stamp & 0x1F, stamp >> 5 

3297 day, stamp = stamp & 0x1F, stamp >> 5 

3298 mon, stamp = stamp & 0x0F, stamp >> 4 

3299 yr = (stamp & 0x7F) + 1980 

3300 return (yr, mon, day, hr, mn, sec * 2) 

3301 

3302 

3303# pylint: disable=arguments-differ,signature-differs 

3304class nsdatetime(datetime): 

3305 """Datetime that carries nanoseconds. 

3306 

3307 Arithmetic operations will lose nanoseconds. 

3308 

3309 .. versionadded:: 4.0 

3310 """ 

3311 __slots__ = ("nanosecond",) 

3312 nanosecond: int #: Number of nanoseconds, 0 <= nanosecond <= 999999999 

3313 

3314 def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0, 

3315 microsecond=0, tzinfo=None, *, fold=0, nanosecond=0): 

3316 usec, mod = divmod(nanosecond, 1000) if nanosecond else (microsecond, 0) 

3317 if mod == 0: 

3318 return datetime(year, month, day, hour, minute, second, usec, tzinfo, fold=fold) 

3319 self = super().__new__(cls, year, month, day, hour, minute, second, usec, tzinfo, fold=fold) 

3320 self.nanosecond = nanosecond 

3321 return self 

3322 

3323 def isoformat(self, sep="T", timespec="auto"): 

3324 """Formats with nanosecond precision by default. 

3325 """ 

3326 if timespec == "auto": 

3327 pre, post = super().isoformat(sep, "microseconds").split(".", 1) 

3328 return f"{pre}.{self.nanosecond:09d}{post[6:]}" 

3329 return super().isoformat(sep, timespec) 

3330 

3331 def astimezone(self, tz=None): 

3332 """Convert to new timezone. 

3333 """ 

3334 tmp = super().astimezone(tz) 

3335 return self.__class__(tmp.year, tmp.month, tmp.day, tmp.hour, tmp.minute, tmp.second, 

3336 nanosecond=self.nanosecond, tzinfo=tmp.tzinfo, fold=tmp.fold) 

3337 

3338 def replace(self, year=None, month=None, day=None, hour=None, minute=None, second=None, 

3339 microsecond=None, tzinfo=None, *, fold=None, nanosecond=None): 

3340 """Return new timestamp with specified fields replaced. 

3341 """ 

3342 return self.__class__( 

3343 self.year if year is None else year, 

3344 self.month if month is None else month, 

3345 self.day if day is None else day, 

3346 self.hour if hour is None else hour, 

3347 self.minute if minute is None else minute, 

3348 self.second if second is None else second, 

3349 nanosecond=((self.nanosecond if microsecond is None else microsecond * 1000) 

3350 if nanosecond is None else nanosecond), 

3351 tzinfo=self.tzinfo if tzinfo is None else tzinfo, 

3352 fold=self.fold if fold is None else fold) 

3353 

3354 def __hash__(self): 

3355 return hash((super().__hash__(), self.nanosecond)) if self.nanosecond else super().__hash__() 

3356 

3357 def __eq__(self, other): 

3358 return super().__eq__(other) and self.nanosecond == ( 

3359 other.nanosecond if isinstance(other, nsdatetime) else other.microsecond * 1000) 

3360 

3361 def __gt__(self, other): 

3362 return super().__gt__(other) or (super().__eq__(other) and self.nanosecond > ( 

3363 other.nanosecond if isinstance(other, nsdatetime) else other.microsecond * 1000)) 

3364 

3365 def __lt__(self, other): 

3366 return not (self > other or self == other) 

3367 

3368 def __ge__(self, other): 

3369 return not self < other 

3370 

3371 def __le__(self, other): 

3372 return not self > other 

3373 

3374 def __ne__(self, other): 

3375 return not self == other 

3376 

3377 

3378def to_nsdatetime(dt, nsec): 

3379 """Apply nanoseconds to datetime. 

3380 """ 

3381 if not nsec: 

3382 return dt 

3383 return nsdatetime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, 

3384 tzinfo=dt.tzinfo, fold=dt.fold, nanosecond=nsec) 

3385 

3386 

3387def to_nsecs(dt): 

3388 """Convert datatime instance to nanoseconds. 

3389 """ 

3390 secs = int(dt.timestamp()) 

3391 nsecs = dt.nanosecond if isinstance(dt, nsdatetime) else dt.microsecond * 1000 

3392 return secs * 1000000000 + nsecs 

3393 

3394 

3395def custom_popen(cmd): 

3396 """Disconnect cmd from parent fds, read only from stdout. 

3397 """ 

3398 creationflags = 0x08000000 if WIN32 else 0 # CREATE_NO_WINDOW 

3399 try: 

3400 p = Popen(cmd, bufsize=0, stdout=PIPE, stderr=STDOUT, stdin=DEVNULL, 

3401 creationflags=creationflags) 

3402 except OSError as ex: 

3403 if ex.errno == errno.ENOENT: 

3404 raise RarCannotExec("Unrar not installed?") from None 

3405 if ex.errno == errno.EACCES or ex.errno == errno.EPERM: 

3406 raise RarCannotExec("Cannot execute unrar") from None 

3407 raise 

3408 return p 

3409 

3410 

3411def check_returncode(code, out, errmap): 

3412 """Raise exception according to unrar exit code. 

3413 """ 

3414 if code == 0: 

3415 return 

3416 

3417 if code > 0 and code < len(errmap): 

3418 exc = errmap[code] 

3419 elif code == 255: 

3420 exc = RarUserBreak 

3421 elif code < 0: 

3422 exc = RarSignalExit 

3423 else: 

3424 exc = RarUnknownError 

3425 

3426 # format message 

3427 if out: 

3428 msg = "%s [%d]: %s" % (exc.__doc__, code, out) 

3429 else: 

3430 msg = "%s [%d]" % (exc.__doc__, code) 

3431 

3432 raise exc(msg) 

3433 

3434 

3435def membuf_tempfile(memfile): 

3436 """Write in-memory file object to real file. 

3437 """ 

3438 memfile.seek(0, 0) 

3439 

3440 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR) 

3441 tmpf = os.fdopen(tmpfd, "wb") 

3442 

3443 try: 

3444 shutil.copyfileobj(memfile, tmpf, BSIZE) 

3445 tmpf.close() 

3446 except BaseException: 

3447 tmpf.close() 

3448 os.unlink(tmpname) 

3449 raise 

3450 return tmpname 

3451 

3452 

3453# 

3454# Find working command-line tool 

3455# 

3456 

3457class ToolSetup: 

3458 def __init__(self, setup): 

3459 self.setup = setup 

3460 

3461 def check(self): 

3462 cmdline = self.get_cmdline("check_cmd", None) 

3463 try: 

3464 p = custom_popen(cmdline) 

3465 out, _ = p.communicate() 

3466 return p.returncode == 0 

3467 except RarCannotExec: 

3468 return False 

3469 

3470 def open_cmdline(self, pwd, rarfn, filefn=None): 

3471 cmdline = self.get_cmdline("open_cmd", pwd) 

3472 cmdline.append(rarfn) 

3473 if filefn: 

3474 self.add_file_arg(cmdline, filefn) 

3475 return cmdline 

3476 

3477 def get_errmap(self): 

3478 return self.setup["errmap"] 

3479 

3480 def get_cmdline(self, key, pwd, nodash=False): 

3481 cmdline = list(self.setup[key]) 

3482 cmdline[0] = globals()[cmdline[0]] 

3483 if key == "check_cmd": 

3484 return cmdline 

3485 self.add_password_arg(cmdline, pwd) 

3486 if not nodash: 

3487 cmdline.append("--") 

3488 return cmdline 

3489 

3490 def add_file_arg(self, cmdline, filename): 

3491 cmdline.append(filename) 

3492 

3493 def add_password_arg(self, cmdline, pwd): 

3494 """Append password switch to commandline. 

3495 """ 

3496 if pwd is not None: 

3497 if not isinstance(pwd, str): 

3498 pwd = pwd.decode("utf8") 

3499 args = self.setup["password"] 

3500 if args is None: 

3501 tool = self.setup["open_cmd"][0] 

3502 raise RarCannotExec(f"{tool} does not support passwords") 

3503 elif isinstance(args, str): 

3504 cmdline.append(args + pwd) 

3505 else: 

3506 cmdline.extend(args) 

3507 cmdline.append(pwd) 

3508 else: 

3509 cmdline.extend(self.setup["no_password"]) 

3510 

3511 

3512UNRAR_CONFIG = { 

3513 "open_cmd": ("UNRAR_TOOL", "p", "-inul"), 

3514 "check_cmd": ("UNRAR_TOOL", "-inul", "-?"), 

3515 "password": "-p", 

3516 "no_password": ("-p-",), 

3517 # map return code to exception class, codes from rar.txt 

3518 "errmap": [None, 

3519 RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError, # 1..4 

3520 RarWriteError, RarOpenError, RarUserError, RarMemoryError, # 5..8 

3521 RarCreateError, RarNoFilesError, RarWrongPassword] # 9..11 

3522} 

3523 

3524# Problems with unar RAR backend: 

3525# - Does not support RAR2 locked files [fails to read] 

3526# - Does not support RAR5 Blake2sp hash [reading works] 

3527UNAR_CONFIG = { 

3528 "open_cmd": ("UNAR_TOOL", "-q", "-o", "-"), 

3529 "check_cmd": ("UNAR_TOOL", "-version"), 

3530 "password": ("-p",), 

3531 "no_password": ("-p", ""), 

3532 "errmap": [None], 

3533} 

3534 

3535# Problems with libarchive RAR backend: 

3536# - Does not support solid archives. 

3537# - Does not support password-protected archives. 

3538# - Does not support RARVM-based compression filters. 

3539BSDTAR_CONFIG = { 

3540 "open_cmd": ("BSDTAR_TOOL", "-x", "--to-stdout", "-f"), 

3541 "check_cmd": ("BSDTAR_TOOL", "--version"), 

3542 "password": None, 

3543 "no_password": (), 

3544 "errmap": [None], 

3545} 

3546 

3547SEVENZIP_CONFIG = { 

3548 "open_cmd": ("SEVENZIP_TOOL", "e", "-so", "-bb0"), 

3549 "check_cmd": ("SEVENZIP_TOOL", "i"), 

3550 "password": "-p", 

3551 "no_password": ("-p",), 

3552 "errmap": [None, 

3553 RarWarning, RarFatalError, None, None, # 1..4 

3554 None, None, RarUserError, RarMemoryError] # 5..8 

3555} 

3556 

3557SEVENZIP2_CONFIG = { 

3558 "open_cmd": ("SEVENZIP2_TOOL", "e", "-so", "-bb0"), 

3559 "check_cmd": ("SEVENZIP2_TOOL", "i"), 

3560 "password": "-p", 

3561 "no_password": ("-p",), 

3562 "errmap": [None, 

3563 RarWarning, RarFatalError, None, None, # 1..4 

3564 None, None, RarUserError, RarMemoryError] # 5..8 

3565} 

3566 

3567CURRENT_SETUP = None 

3568 

3569 

3570def tool_setup(unrar=True, unar=True, bsdtar=True, sevenzip=True, sevenzip2=True, force=False): 

3571 """Pick a tool, return cached ToolSetup. 

3572 """ 

3573 global CURRENT_SETUP 

3574 if force: 

3575 CURRENT_SETUP = None 

3576 if CURRENT_SETUP is not None: 

3577 return CURRENT_SETUP 

3578 lst = [] 

3579 if unrar: 

3580 lst.append(UNRAR_CONFIG) 

3581 if unar: 

3582 lst.append(UNAR_CONFIG) 

3583 if sevenzip: 

3584 lst.append(SEVENZIP_CONFIG) 

3585 if sevenzip2: 

3586 lst.append(SEVENZIP2_CONFIG) 

3587 if bsdtar: 

3588 lst.append(BSDTAR_CONFIG) 

3589 

3590 for conf in lst: 

3591 setup = ToolSetup(conf) 

3592 if setup.check(): 

3593 CURRENT_SETUP = setup 

3594 break 

3595 if CURRENT_SETUP is None: 

3596 raise RarCannotExec("Cannot find working tool") 

3597 return CURRENT_SETUP 

3598 

3599 

3600def main(args): 

3601 """Minimal command-line interface for rarfile module. 

3602 """ 

3603 import argparse 

3604 p = argparse.ArgumentParser(description=main.__doc__) 

3605 g = p.add_mutually_exclusive_group(required=True) 

3606 g.add_argument("-l", "--list", metavar="<rarfile>", 

3607 help="Show archive listing") 

3608 g.add_argument("-e", "--extract", nargs=2, 

3609 metavar=("<rarfile>", "<output_dir>"), 

3610 help="Extract archive into target dir") 

3611 g.add_argument("-t", "--test", metavar="<rarfile>", 

3612 help="Test if a archive is valid") 

3613 cmd = p.parse_args(args) 

3614 

3615 if cmd.list: 

3616 with RarFile(cmd.list) as rf: 

3617 rf.printdir() 

3618 elif cmd.test: 

3619 with RarFile(cmd.test) as rf: 

3620 rf.testrar() 

3621 elif cmd.extract: 

3622 with RarFile(cmd.extract[0]) as rf: 

3623 rf.extractall(cmd.extract[1]) 

3624 

3625 

3626if __name__ == "__main__": 

3627 main(sys.argv[1:]) 

3628