Always use / as sep
[rarfile.git] / rarfile.py
blobb2a2eacdc6b6a63a7374694cf4e2f36c63194b09
1 # rarfile.py
3 # Copyright (c) 2005-2019 Marko Kreen <markokr@gmail.com>
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.
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.
17 r"""RAR archive reader.
19 This is Python module for Rar archive reading. The interface
20 is made as :mod:`zipfile`-like as possible.
22 Basic 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.
29 Example::
31 import rarfile
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))
39 Archive files can also be accessed via file-like object returned
40 by :meth:`RarFile.open`::
42 import rarfile
44 with rarfile.RarFile("archive.rar") as rf:
45 with rf.open("README") as f:
46 for ln in f:
47 print(ln.strip())
49 For decompression to work, either ``unrar`` or ``unar`` tool must be in PATH.
51 """
54 ## Imports and compat - support various crypto options
57 import sys
58 import os
59 import errno
60 import struct
62 from struct import pack, unpack, Struct
63 from binascii import crc32 as rar_crc32, hexlify
64 from tempfile import mkstemp
65 from subprocess import Popen, PIPE, STDOUT
66 from io import RawIOBase, BytesIO
67 from hashlib import sha1, sha256, blake2s
68 from hmac import HMAC
69 from datetime import datetime, timedelta, timezone
70 from pathlib import Path
72 # only needed for encrypted headers
73 try:
74 try:
75 from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher
76 from cryptography.hazmat.backends import default_backend
77 from cryptography.hazmat.primitives import hashes
78 from cryptography.hazmat.primitives.kdf import pbkdf2
80 class AES_CBC_Decrypt(object):
81 """Decrypt API"""
82 def __init__(self, key, iv):
83 ciph = Cipher(algorithms.AES(key), modes.CBC(iv), default_backend())
84 self.decrypt = ciph.decryptor().update
86 def pbkdf2_sha256(password, salt, iters):
87 """PBKDF2 with HMAC-SHA256"""
88 ctx = pbkdf2.PBKDF2HMAC(hashes.SHA256(), 32, salt, iters, default_backend())
89 return ctx.derive(password)
91 except ImportError:
92 from Crypto.Cipher import AES
93 from Crypto.Protocol import KDF
95 class AES_CBC_Decrypt(object):
96 """Decrypt API"""
97 def __init__(self, key, iv):
98 self.decrypt = AES.new(key, AES.MODE_CBC, iv).decrypt
100 def pbkdf2_sha256(password, salt, iters):
101 """PBKDF2 with HMAC-SHA256"""
102 return KDF.PBKDF2(password, salt, 32, iters, hmac_sha256)
104 _have_crypto = 1
105 except ImportError:
106 _have_crypto = 0
109 def tohex(data):
110 """Return hex string."""
111 return hexlify(data).decode("ascii")
114 __version__ = "3.1"
116 # export only interesting items
117 __all__ = ["is_rarfile", "is_rarfile_sfx", "RarInfo", "RarFile", "RarExtFile"]
120 ## Module configuration. Can be tuned after importing.
124 #: executable for unrar tool
125 UNRAR_TOOL = "unrar"
127 #: executable for unar tool
128 UNAR_TOOL = "unar"
129 LSAR_TOOL = "lsar"
131 #: executable for bsdtar tool
132 BSDTAR_TOOL = "bsdtar"
134 #: default fallback charset
135 DEFAULT_CHARSET = "windows-1252"
137 #: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
138 TRY_ENCODINGS = ("utf8", "utf-16le")
140 #: whether to speed up decompression by using tmp archive
141 USE_EXTRACT_HACK = 1
143 #: limit the filesize for tmp archive usage
144 HACK_SIZE_LIMIT = 20 * 1024 * 1024
146 #: set specific directory for mkstemp() used by hack dir usage
147 HACK_TMP_DIR = None
149 #: Separator for path name components. RAR internally uses "\\".
150 #: Use "/" to be similar with zipfile.
151 PATH_SEP = "/"
154 ## rar constants
157 # block types
158 RAR_BLOCK_MARK = 0x72 # r
159 RAR_BLOCK_MAIN = 0x73 # s
160 RAR_BLOCK_FILE = 0x74 # t
161 RAR_BLOCK_OLD_COMMENT = 0x75 # u
162 RAR_BLOCK_OLD_EXTRA = 0x76 # v
163 RAR_BLOCK_OLD_SUB = 0x77 # w
164 RAR_BLOCK_OLD_RECOVERY = 0x78 # x
165 RAR_BLOCK_OLD_AUTH = 0x79 # y
166 RAR_BLOCK_SUB = 0x7a # z
167 RAR_BLOCK_ENDARC = 0x7b # {
169 # flags for RAR_BLOCK_MAIN
170 RAR_MAIN_VOLUME = 0x0001
171 RAR_MAIN_COMMENT = 0x0002
172 RAR_MAIN_LOCK = 0x0004
173 RAR_MAIN_SOLID = 0x0008
174 RAR_MAIN_NEWNUMBERING = 0x0010
175 RAR_MAIN_AUTH = 0x0020
176 RAR_MAIN_RECOVERY = 0x0040
177 RAR_MAIN_PASSWORD = 0x0080
178 RAR_MAIN_FIRSTVOLUME = 0x0100
179 RAR_MAIN_ENCRYPTVER = 0x0200
181 # flags for RAR_BLOCK_FILE
182 RAR_FILE_SPLIT_BEFORE = 0x0001
183 RAR_FILE_SPLIT_AFTER = 0x0002
184 RAR_FILE_PASSWORD = 0x0004
185 RAR_FILE_COMMENT = 0x0008
186 RAR_FILE_SOLID = 0x0010
187 RAR_FILE_DICTMASK = 0x00e0
188 RAR_FILE_DICT64 = 0x0000
189 RAR_FILE_DICT128 = 0x0020
190 RAR_FILE_DICT256 = 0x0040
191 RAR_FILE_DICT512 = 0x0060
192 RAR_FILE_DICT1024 = 0x0080
193 RAR_FILE_DICT2048 = 0x00a0
194 RAR_FILE_DICT4096 = 0x00c0
195 RAR_FILE_DIRECTORY = 0x00e0
196 RAR_FILE_LARGE = 0x0100
197 RAR_FILE_UNICODE = 0x0200
198 RAR_FILE_SALT = 0x0400
199 RAR_FILE_VERSION = 0x0800
200 RAR_FILE_EXTTIME = 0x1000
201 RAR_FILE_EXTFLAGS = 0x2000
203 # flags for RAR_BLOCK_ENDARC
204 RAR_ENDARC_NEXT_VOLUME = 0x0001
205 RAR_ENDARC_DATACRC = 0x0002
206 RAR_ENDARC_REVSPACE = 0x0004
207 RAR_ENDARC_VOLNR = 0x0008
209 # flags common to all blocks
210 RAR_SKIP_IF_UNKNOWN = 0x4000
211 RAR_LONG_BLOCK = 0x8000
213 # Host OS types
214 RAR_OS_MSDOS = 0
215 RAR_OS_OS2 = 1
216 RAR_OS_WIN32 = 2
217 RAR_OS_UNIX = 3
218 RAR_OS_MACOS = 4
219 RAR_OS_BEOS = 5
221 # Compression methods - "0".."5"
222 RAR_M0 = 0x30
223 RAR_M1 = 0x31
224 RAR_M2 = 0x32
225 RAR_M3 = 0x33
226 RAR_M4 = 0x34
227 RAR_M5 = 0x35
230 # RAR5 constants
233 RAR5_BLOCK_MAIN = 1
234 RAR5_BLOCK_FILE = 2
235 RAR5_BLOCK_SERVICE = 3
236 RAR5_BLOCK_ENCRYPTION = 4
237 RAR5_BLOCK_ENDARC = 5
239 RAR5_BLOCK_FLAG_EXTRA_DATA = 0x01
240 RAR5_BLOCK_FLAG_DATA_AREA = 0x02
241 RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN = 0x04
242 RAR5_BLOCK_FLAG_SPLIT_BEFORE = 0x08
243 RAR5_BLOCK_FLAG_SPLIT_AFTER = 0x10
244 RAR5_BLOCK_FLAG_DEPENDS_PREV = 0x20
245 RAR5_BLOCK_FLAG_KEEP_WITH_PARENT = 0x40
247 RAR5_MAIN_FLAG_ISVOL = 0x01
248 RAR5_MAIN_FLAG_HAS_VOLNR = 0x02
249 RAR5_MAIN_FLAG_SOLID = 0x04
250 RAR5_MAIN_FLAG_RECOVERY = 0x08
251 RAR5_MAIN_FLAG_LOCKED = 0x10
253 RAR5_FILE_FLAG_ISDIR = 0x01
254 RAR5_FILE_FLAG_HAS_MTIME = 0x02
255 RAR5_FILE_FLAG_HAS_CRC32 = 0x04
256 RAR5_FILE_FLAG_UNKNOWN_SIZE = 0x08
258 RAR5_COMPR_SOLID = 0x40
260 RAR5_ENC_FLAG_HAS_CHECKVAL = 0x01
262 RAR5_ENDARC_FLAG_NEXT_VOL = 0x01
264 RAR5_XFILE_ENCRYPTION = 1
265 RAR5_XFILE_HASH = 2
266 RAR5_XFILE_TIME = 3
267 RAR5_XFILE_VERSION = 4
268 RAR5_XFILE_REDIR = 5
269 RAR5_XFILE_OWNER = 6
270 RAR5_XFILE_SERVICE = 7
272 RAR5_XTIME_UNIXTIME = 0x01
273 RAR5_XTIME_HAS_MTIME = 0x02
274 RAR5_XTIME_HAS_CTIME = 0x04
275 RAR5_XTIME_HAS_ATIME = 0x08
277 RAR5_XENC_CIPHER_AES256 = 0
279 RAR5_XENC_CHECKVAL = 0x01
280 RAR5_XENC_TWEAKED = 0x02
282 RAR5_XHASH_BLAKE2SP = 0
284 RAR5_XREDIR_UNIX_SYMLINK = 1
285 RAR5_XREDIR_WINDOWS_SYMLINK = 2
286 RAR5_XREDIR_WINDOWS_JUNCTION = 3
287 RAR5_XREDIR_HARD_LINK = 4
288 RAR5_XREDIR_FILE_COPY = 5
290 RAR5_XREDIR_ISDIR = 0x01
292 RAR5_XOWNER_UNAME = 0x01
293 RAR5_XOWNER_GNAME = 0x02
294 RAR5_XOWNER_UID = 0x04
295 RAR5_XOWNER_GID = 0x08
297 RAR5_OS_WINDOWS = 0
298 RAR5_OS_UNIX = 1
301 ## internal constants
304 RAR_ID = b"Rar!\x1a\x07\x00"
305 RAR5_ID = b"Rar!\x1a\x07\x01\x00"
306 ZERO = b"\0"
307 EMPTY = b""
308 UTC = timezone(timedelta(0), "UTC")
309 BSIZE = 32 * 1024
311 SFX_MAX_SIZE = 2 * 1024 * 1024
312 RAR_V3 = 3
313 RAR_V5 = 5
315 def _get_rar_version(xfile):
316 """Check quickly whether file is rar archive.
318 with XFile(xfile) as fd:
319 buf = fd.read(len(RAR5_ID))
320 if buf.startswith(RAR_ID):
321 return RAR_V3
322 elif buf.startswith(RAR5_ID):
323 return RAR_V5
324 return 0
326 def _find_sfx_header(xfile):
327 sig = RAR_ID[:-1]
328 buf = BytesIO()
329 steps = (64, SFX_MAX_SIZE)
331 with XFile(xfile) as fd:
332 for step in steps:
333 data = fd.read(step)
334 if not data:
335 break
336 buf.write(data)
337 curdata = buf.getvalue()
338 findpos = 0
339 while True:
340 pos = curdata.find(sig, findpos)
341 if pos < 0:
342 break
343 if curdata[pos:pos+len(RAR_ID)] == RAR_ID:
344 return RAR_V3, pos
345 if curdata[pos:pos+len(RAR5_ID)] == RAR5_ID:
346 return RAR_V5, pos
347 findpos = pos + len(sig)
348 return 0, 0
351 ## Public interface
354 def is_rarfile(xfile):
355 """Check quickly whether file is rar archive.
357 return _get_rar_version(xfile) > 0
359 def is_rarfile_sfx(xfile):
360 """Check whether file is rar archive with support for SFX.
362 It will read 2M from file.
364 return _find_sfx_header(xfile)[0] > 0
366 class Error(Exception):
367 """Base class for rarfile errors."""
369 class BadRarFile(Error):
370 """Incorrect data in archive."""
372 class NotRarFile(Error):
373 """The file is not RAR archive."""
375 class BadRarName(Error):
376 """Cannot guess multipart name components."""
378 class NoRarEntry(Error):
379 """File not found in RAR"""
381 class PasswordRequired(Error):
382 """File requires password"""
384 class NeedFirstVolume(Error):
385 """Need to start from first volume."""
387 class NoCrypto(Error):
388 """Cannot parse encrypted headers - no crypto available."""
390 class RarExecError(Error):
391 """Problem reported by unrar/rar."""
393 class RarWarning(RarExecError):
394 """Non-fatal error"""
396 class RarFatalError(RarExecError):
397 """Fatal error"""
399 class RarCRCError(RarExecError):
400 """CRC error during unpacking"""
402 class RarLockedArchiveError(RarExecError):
403 """Must not modify locked archive"""
405 class RarWriteError(RarExecError):
406 """Write error"""
408 class RarOpenError(RarExecError):
409 """Open error"""
411 class RarUserError(RarExecError):
412 """User error"""
414 class RarMemoryError(RarExecError):
415 """Memory error"""
417 class RarCreateError(RarExecError):
418 """Create error"""
420 class RarNoFilesError(RarExecError):
421 """No files that match pattern were found"""
423 class RarUserBreak(RarExecError):
424 """User stop"""
426 class RarWrongPassword(RarExecError):
427 """Incorrect password"""
429 class RarUnknownError(RarExecError):
430 """Unknown exit code"""
432 class RarSignalExit(RarExecError):
433 """Unrar exited with signal"""
435 class RarCannotExec(RarExecError):
436 """Executable not found."""
439 class RarInfo(object):
440 r"""An entry in rar archive.
442 RAR3 extended timestamps are :class:`datetime.datetime` objects without timezone.
443 RAR5 extended timestamps are :class:`datetime.datetime` objects with UTC timezone.
445 Attributes:
447 filename
448 File name with relative path.
449 Path separator is "/". Always unicode string.
451 date_time
452 File modification timestamp. As tuple of (year, month, day, hour, minute, second).
453 RAR5 allows archives where it is missing, it's None then.
455 file_size
456 Uncompressed size.
458 compress_size
459 Compressed size.
461 compress_type
462 Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants.
464 extract_version
465 Minimal Rar version needed for decompressing. As (major*10 + minor),
466 so 2.9 is 29.
468 RAR3: 10, 20, 29
470 RAR5 does not have such field in archive, it's simply set to 50.
472 host_os
473 Host OS type, one of RAR_OS_* constants.
475 RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`,
476 :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`.
478 RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`.
480 mode
481 File attributes. May be either dos-style or unix-style, depending on host_os.
483 mtime
484 File modification time. Same value as :attr:`date_time`
485 but as :class:`datetime.datetime` object with extended precision.
487 ctime
488 Optional time field: creation time. As :class:`datetime.datetime` object.
490 atime
491 Optional time field: last access time. As :class:`datetime.datetime` object.
493 arctime
494 Optional time field: archival time. As :class:`datetime.datetime` object.
495 (RAR3-only)
498 CRC-32 of uncompressed file, unsigned int.
500 RAR5: may be None.
502 blake2sp_hash
503 Blake2SP hash over decompressed data. (RAR5-only)
505 comment
506 Optional file comment field. Unicode string. (RAR3-only)
508 file_redir
509 If not None, file is link of some sort. Contains tuple of (type, flags, target).
510 (RAR5-only)
512 Type is one of constants:
514 :data:`RAR5_XREDIR_UNIX_SYMLINK`
515 unix symlink to target.
516 :data:`RAR5_XREDIR_WINDOWS_SYMLINK`
517 windows symlink to target.
518 :data:`RAR5_XREDIR_WINDOWS_JUNCTION`
519 windows junction.
520 :data:`RAR5_XREDIR_HARD_LINK`
521 hard link to target.
522 :data:`RAR5_XREDIR_FILE_COPY`
523 current file is copy of another archive entry.
525 Flags may contain :data:`RAR5_XREDIR_ISDIR` bit.
527 volume
528 Volume nr, starting from 0.
530 volume_file
531 Volume file name, where file starts.
535 # zipfile-compatible fields
536 filename = None
537 file_size = None
538 compress_size = None
539 date_time = None
540 comment = None
541 CRC = None
542 volume = None
543 orig_filename = None
545 # optional extended time fields, datetime() objects.
546 mtime = None
547 ctime = None
548 atime = None
550 extract_version = None
551 mode = None
552 host_os = None
553 compress_type = None
555 # rar3-only fields
556 comment = None
557 arctime = None
559 # rar5-only fields
560 blake2sp_hash = None
561 file_redir = None
563 # internal fields
564 flags = 0
565 type = None
567 def isdir(self):
568 """Returns True if entry is a directory.
570 if self.type == RAR_BLOCK_FILE:
571 return (self.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY
572 return False
574 def needs_password(self):
575 """Returns True if data is stored password-protected.
577 if self.type == RAR_BLOCK_FILE:
578 return (self.flags & RAR_FILE_PASSWORD) > 0
579 return False
582 class RarFile(object):
583 """Parse RAR structure, provide access to files in archive.
586 #: Archive comment. Unicode string or None.
587 comment = None
589 def __init__(self, rarfile, mode="r", charset=None, info_callback=None,
590 crc_check=True, errors="stop"):
591 """Open and parse a RAR archive.
593 Parameters:
595 rarfile
596 archive file name
597 mode
598 only "r" is supported.
599 charset
600 fallback charset to use, if filenames are not already Unicode-enabled.
601 info_callback
602 debug callback, gets to see all archive entries.
603 crc_check
604 set to False to disable CRC checks
605 errors
606 Either "stop" to quietly stop parsing on errors,
607 or "strict" to raise errors. Default is "stop".
609 if isinstance(rarfile, Path):
610 self._rarfile = str(rarfile)
611 else:
612 self._rarfile = rarfile
614 self._charset = charset or DEFAULT_CHARSET
615 self._info_callback = info_callback
616 self._crc_check = crc_check
617 self._password = None
618 self._file_parser = None
620 if errors == "stop":
621 self._strict = False
622 elif errors == "strict":
623 self._strict = True
624 else:
625 raise ValueError("Invalid value for errors= parameter.")
627 if mode != "r":
628 raise NotImplementedError("RarFile supports only mode=r")
630 self._parse()
632 def __enter__(self):
633 """Open context."""
634 return self
636 def __exit__(self, typ, value, traceback):
637 """Exit context"""
638 self.close()
640 def setpassword(self, password):
641 """Sets the password to use when extracting.
643 self._password = password
644 if self._file_parser:
645 if self._file_parser.has_header_encryption():
646 self._file_parser = None
647 if not self._file_parser:
648 self._parse()
649 else:
650 self._file_parser.setpassword(self._password)
652 def needs_password(self):
653 """Returns True if any archive entries require password for extraction.
655 return self._file_parser.needs_password()
657 def namelist(self):
658 """Return list of filenames in archive.
660 return [f.filename for f in self.infolist()]
662 def infolist(self):
663 """Return RarInfo objects for all files/directories in archive.
665 return self._file_parser.infolist()
667 def volumelist(self):
668 """Returns filenames of archive volumes.
670 In case of single-volume archive, the list contains
671 just the name of main archive file.
673 return self._file_parser.volumelist()
675 def getinfo(self, fname):
676 """Return RarInfo for file.
678 return self._file_parser.getinfo(fname)
680 def open(self, fname, mode="r", psw=None):
681 """Returns file-like object (:class:`RarExtFile`) from where the data can be read.
683 The object implements :class:`io.RawIOBase` interface, so it can
684 be further wrapped with :class:`io.BufferedReader`
685 and :class:`io.TextIOWrapper`.
687 On older Python where io module is not available, it implements
688 only .read(), .seek(), .tell() and .close() methods.
690 The object is seekable, although the seeking is fast only on
691 uncompressed files, on compressed files the seeking is implemented
692 by reading ahead and/or restarting the decompression.
694 Parameters:
696 fname
697 file name or RarInfo instance.
698 mode
699 must be "r"
701 password to use for extracting.
704 if mode != "r":
705 raise NotImplementedError("RarFile.open() supports only mode=r")
707 # entry lookup
708 inf = self.getinfo(fname)
709 if inf.isdir():
710 raise TypeError("Directory does not have any data: " + inf.filename)
712 # check password
713 if inf.needs_password():
714 psw = psw or self._password
715 if psw is None:
716 raise PasswordRequired("File %s requires password" % inf.filename)
717 else:
718 psw = None
720 return self._file_parser.open(inf, psw)
722 def read(self, fname, psw=None):
723 """Return uncompressed data for archive entry.
725 For longer files using :meth:`RarFile.open` may be better idea.
727 Parameters:
729 fname
730 filename or RarInfo instance
732 password to use for extracting.
735 with self.open(fname, "r", psw) as f:
736 return f.read()
738 def close(self):
739 """Release open resources."""
740 pass
742 def printdir(self):
743 """Print archive file list to stdout."""
744 for f in self.infolist():
745 print(f.filename)
747 def extract(self, member, path=None, pwd=None):
748 """Extract single file into current directory.
750 Parameters:
752 member
753 filename or :class:`RarInfo` instance
754 path
755 optional destination path
757 optional password to use
759 if isinstance(member, RarInfo):
760 fname = member.filename
761 elif isinstance(member, Path):
762 fname = str(member)
763 else:
764 fname = member
765 self._extract([fname], path, pwd)
767 def extractall(self, path=None, members=None, pwd=None):
768 """Extract all files into current directory.
770 Parameters:
772 path
773 optional destination path
774 members
775 optional filename or :class:`RarInfo` instance list to extract
777 optional password to use
779 fnlist = []
780 if members is not None:
781 for m in members:
782 if isinstance(m, RarInfo):
783 fnlist.append(m.filename)
784 else:
785 fnlist.append(m)
786 self._extract(fnlist, path, pwd)
788 def testrar(self):
789 """Let "unrar" test the archive.
791 setup = tool_setup()
792 with XTempFile(self._rarfile) as rarfile:
793 cmd = setup.test_cmdline(self._password, rarfile)
794 p = custom_popen(cmd)
795 output = p.communicate()[0]
796 check_returncode(p, output)
798 def strerror(self):
799 """Return error string if parsing failed or None if no problems.
801 if not self._file_parser:
802 return "Not a RAR file"
803 return self._file_parser.strerror()
806 ## private methods
809 def _parse(self):
810 ver, sfx_ofs = _find_sfx_header(self._rarfile)
811 if ver == RAR_V3:
812 p3 = RAR3Parser(self._rarfile, self._password, self._crc_check,
813 self._charset, self._strict, self._info_callback,
814 sfx_ofs)
815 self._file_parser = p3 # noqa
816 elif ver == RAR_V5:
817 p5 = RAR5Parser(self._rarfile, self._password, self._crc_check,
818 self._charset, self._strict, self._info_callback,
819 sfx_ofs)
820 self._file_parser = p5 # noqa
821 else:
822 raise BadRarFile("Not a RAR file")
824 self._file_parser.parse()
825 self.comment = self._file_parser.comment
827 # call unrar to extract a file
828 def _extract(self, fnlist, path=None, psw=None):
829 setup = tool_setup()
831 if os.sep != PATH_SEP:
832 fnlist = [fn.replace(PATH_SEP, os.sep) for fn in fnlist]
834 if path and isinstance(path, Path):
835 path = str(path)
837 psw = psw or self._password
839 # rar file
840 with XTempFile(self._rarfile) as rarfn:
841 cmd = setup.extract_cmdline(psw, rarfn, fnlist, path)
843 # call
844 p = custom_popen(cmd)
845 output = p.communicate()[0]
846 check_returncode(p, output)
849 # File format parsing
852 class CommonParser(object):
853 """Shared parser parts."""
854 _main = None
855 _hdrenc_main = None
856 _needs_password = False
857 _fd = None
858 _expect_sig = None
859 _parse_error = None
860 _password = None
861 comment = None
863 def __init__(self, rarfile, password, crc_check, charset, strict, info_cb, sfx_offset):
864 self._rarfile = rarfile
865 self._password = password
866 self._crc_check = crc_check
867 self._charset = charset
868 self._strict = strict
869 self._info_callback = info_cb
870 self._info_list = []
871 self._info_map = {}
872 self._vol_list = []
873 self._sfx_offset = sfx_offset
875 def has_header_encryption(self):
876 """Returns True if headers are encrypted
878 if self._hdrenc_main:
879 return True
880 if self._main:
881 if self._main.flags & RAR_MAIN_PASSWORD:
882 return True
883 return False
885 def setpassword(self, psw):
886 """Set cached password."""
887 self._password = psw
889 def volumelist(self):
890 """Volume files"""
891 return self._vol_list
893 def needs_password(self):
894 """Is password required"""
895 return self._needs_password
897 def strerror(self):
898 """Last error"""
899 return self._parse_error
901 def infolist(self):
902 """List of RarInfo records.
904 return self._info_list
906 def getinfo(self, member):
907 """Return RarInfo for filename
909 if isinstance(member, RarInfo):
910 fname = member.filename
911 elif isinstance(member, Path):
912 fname = str(member)
913 else:
914 fname = member
916 # accept both ways here
917 if PATH_SEP == "/":
918 fname2 = fname.replace("\\", "/")
919 else:
920 fname2 = fname.replace("/", "\\")
922 try:
923 return self._info_map[fname]
924 except KeyError:
925 try:
926 return self._info_map[fname2]
927 except KeyError:
928 raise NoRarEntry("No such file: %s" % fname)
930 # read rar
931 def parse(self):
932 """Process file."""
933 self._fd = None
934 try:
935 self._parse_real()
936 finally:
937 if self._fd:
938 self._fd.close()
939 self._fd = None
941 def _parse_real(self):
942 fd = XFile(self._rarfile)
943 self._fd = fd
944 fd.seek(self._sfx_offset, 0)
945 sig = fd.read(len(self._expect_sig))
946 if sig != self._expect_sig:
947 if isinstance(self._rarfile, str):
948 raise NotRarFile("Not a Rar archive: {}".format(self._rarfile))
949 raise NotRarFile("Not a Rar archive")
951 volume = 0 # first vol (.rar) is 0
952 more_vols = False
953 endarc = False
954 volfile = self._rarfile
955 self._vol_list = [self._rarfile]
956 while 1:
957 if endarc:
958 h = None # don"t read past ENDARC
959 else:
960 h = self._parse_header(fd)
961 if not h:
962 if more_vols:
963 volume += 1
964 fd.close()
965 try:
966 volfile = self._next_volname(volfile)
967 fd = XFile(volfile)
968 except IOError:
969 self._set_error("Cannot open next volume: %s", volfile)
970 break
971 self._fd = fd
972 sig = fd.read(len(self._expect_sig))
973 if sig != self._expect_sig:
974 self._set_error("Invalid volume sig: %s", volfile)
975 break
976 more_vols = False
977 endarc = False
978 self._vol_list.append(volfile)
979 self._main = None
980 continue
981 break
982 h.volume = volume
983 h.volume_file = volfile
985 if h.type == RAR_BLOCK_MAIN and not self._main:
986 self._main = h
987 if volume == 0 and (h.flags & RAR_MAIN_NEWNUMBERING):
988 # RAR 2.x does not set FIRSTVOLUME,
989 # so check it only if NEWNUMBERING is used
990 if (h.flags & RAR_MAIN_FIRSTVOLUME) == 0:
991 if getattr(h, "main_volume_number", None) is not None:
992 # rar5 may have more info
993 raise NeedFirstVolume(
994 "Need to start from first volume (current: %r)"
995 % (h.main_volume_number,)
997 raise NeedFirstVolume("Need to start from first volume")
998 if h.flags & RAR_MAIN_PASSWORD:
999 self._needs_password = True
1000 if not self._password:
1001 break
1002 elif h.type == RAR_BLOCK_ENDARC:
1003 more_vols = (h.flags & RAR_ENDARC_NEXT_VOLUME) > 0
1004 endarc = True
1005 elif h.type == RAR_BLOCK_FILE:
1006 # RAR 2.x does not write RAR_BLOCK_ENDARC
1007 if h.flags & RAR_FILE_SPLIT_AFTER:
1008 more_vols = True
1009 # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
1010 if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE:
1011 raise NeedFirstVolume("Need to start from first volume")
1013 if h.needs_password():
1014 self._needs_password = True
1016 # store it
1017 self.process_entry(fd, h)
1019 if self._info_callback:
1020 self._info_callback(h)
1022 # go to next header
1023 if h.add_size > 0:
1024 fd.seek(h.data_offset + h.add_size, 0)
1026 def process_entry(self, fd, item):
1027 """Examine item, add into lookup cache."""
1028 raise NotImplementedError()
1030 def _decrypt_header(self, fd):
1031 raise NotImplementedError("_decrypt_header")
1033 def _parse_block_header(self, fd):
1034 raise NotImplementedError("_parse_block_header")
1036 def _open_hack(self, inf, psw):
1037 raise NotImplementedError("_open_hack")
1039 # read single header
1040 def _parse_header(self, fd):
1041 try:
1042 # handle encrypted headers
1043 if (self._main and self._main.flags & RAR_MAIN_PASSWORD) or self._hdrenc_main:
1044 if not self._password:
1045 return None
1046 fd = self._decrypt_header(fd)
1048 # now read actual header
1049 return self._parse_block_header(fd)
1050 except struct.error:
1051 self._set_error("Broken header in RAR file")
1052 return None
1054 # given current vol name, construct next one
1055 def _next_volname(self, volfile):
1056 if is_filelike(volfile):
1057 raise IOError("Working on single FD")
1058 if self._main.flags & RAR_MAIN_NEWNUMBERING:
1059 return _next_newvol(volfile)
1060 return _next_oldvol(volfile)
1062 def _set_error(self, msg, *args):
1063 if args:
1064 msg = msg % args
1065 self._parse_error = msg
1066 if self._strict:
1067 raise BadRarFile(msg)
1069 def open(self, inf, psw):
1070 """Return stream object for file data."""
1072 if inf.file_redir:
1073 # cannot leave to unrar as it expects copied file to exist
1074 if inf.file_redir[0] in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK):
1075 inf = self.getinfo(inf.file_redir[2])
1076 if not inf:
1077 raise BadRarFile("cannot find copied file")
1079 if inf.flags & RAR_FILE_SPLIT_BEFORE:
1080 raise NeedFirstVolume("Partial file, please start from first volume: " + inf.filename)
1082 # is temp write usable?
1083 use_hack = 1
1084 if not self._main:
1085 use_hack = 0
1086 elif self._main._must_disable_hack():
1087 use_hack = 0
1088 elif inf._must_disable_hack():
1089 use_hack = 0
1090 elif is_filelike(self._rarfile):
1091 pass
1092 elif inf.file_size > HACK_SIZE_LIMIT:
1093 use_hack = 0
1094 elif not USE_EXTRACT_HACK:
1095 use_hack = 0
1097 # now extract
1098 if inf.compress_type == RAR_M0 and (inf.flags & RAR_FILE_PASSWORD) == 0 and inf.file_redir is None:
1099 return self._open_clear(inf)
1100 elif use_hack:
1101 return self._open_hack(inf, psw)
1102 elif is_filelike(self._rarfile):
1103 return self._open_unrar_membuf(self._rarfile, inf, psw)
1104 else:
1105 return self._open_unrar(self._rarfile, inf, psw)
1107 def _open_clear(self, inf):
1108 return DirectReader(self, inf)
1110 def _open_hack_core(self, inf, psw, prefix, suffix):
1112 size = inf.compress_size + inf.header_size
1113 rf = XFile(inf.volume_file, 0)
1114 rf.seek(inf.header_offset)
1116 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR)
1117 tmpf = os.fdopen(tmpfd, "wb")
1119 try:
1120 tmpf.write(prefix)
1121 while size > 0:
1122 if size > BSIZE:
1123 buf = rf.read(BSIZE)
1124 else:
1125 buf = rf.read(size)
1126 if not buf:
1127 raise BadRarFile("read failed: " + inf.filename)
1128 tmpf.write(buf)
1129 size -= len(buf)
1130 tmpf.write(suffix)
1131 tmpf.close()
1132 rf.close()
1133 except:
1134 rf.close()
1135 tmpf.close()
1136 os.unlink(tmpname)
1137 raise
1139 return self._open_unrar(tmpname, inf, psw, tmpname)
1141 # write in-memory archive to temp file - needed for solid archives
1142 def _open_unrar_membuf(self, memfile, inf, psw):
1143 tmpname = membuf_tempfile(memfile)
1144 return self._open_unrar(tmpname, inf, psw, tmpname, force_file=True)
1146 # extract using unrar
1147 def _open_unrar(self, rarfile, inf, psw=None, tmpfile=None, force_file=False):
1148 setup = tool_setup()
1150 # not giving filename avoids encoding related problems
1151 fn = None
1152 if not tmpfile or force_file:
1153 fn = inf.filename
1154 if PATH_SEP != os.sep:
1155 fn = fn.replace(PATH_SEP, os.sep)
1157 # read from unrar pipe
1158 cmd = setup.open_cmdline(psw, rarfile, fn)
1159 return PipeReader(self, inf, cmd, tmpfile)
1162 # RAR3 format
1165 class Rar3Info(RarInfo):
1166 """RAR3 specific fields."""
1167 extract_version = 15
1168 salt = None
1169 add_size = 0
1170 header_crc = None
1171 header_size = None
1172 header_offset = None
1173 data_offset = None
1174 _md_class = None
1175 _md_expect = None
1177 # make sure some rar5 fields are always present
1178 file_redir = None
1179 blake2sp_hash = None
1181 def _must_disable_hack(self):
1182 if self.type == RAR_BLOCK_FILE:
1183 if self.flags & RAR_FILE_PASSWORD:
1184 return True
1185 elif self.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
1186 return True
1187 elif self.type == RAR_BLOCK_MAIN:
1188 if self.flags & (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD):
1189 return True
1190 return False
1193 class RAR3Parser(CommonParser):
1194 """Parse RAR3 file format.
1196 _expect_sig = RAR_ID
1197 _last_aes_key = (None, None, None) # (salt, key, iv)
1199 def _decrypt_header(self, fd):
1200 if not _have_crypto:
1201 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1202 salt = fd.read(8)
1203 if self._last_aes_key[0] == salt:
1204 key, iv = self._last_aes_key[1:]
1205 else:
1206 key, iv = rar3_s2k(self._password, salt)
1207 self._last_aes_key = (salt, key, iv)
1208 return HeaderDecrypt(fd, key, iv)
1210 # common header
1211 def _parse_block_header(self, fd):
1212 h = Rar3Info()
1213 h.header_offset = fd.tell()
1215 # read and parse base header
1216 buf = fd.read(S_BLK_HDR.size)
1217 if not buf:
1218 return None
1219 t = S_BLK_HDR.unpack_from(buf)
1220 h.header_crc, h.type, h.flags, h.header_size = t
1222 # read full header
1223 if h.header_size > S_BLK_HDR.size:
1224 hdata = buf + fd.read(h.header_size - S_BLK_HDR.size)
1225 else:
1226 hdata = buf
1227 h.data_offset = fd.tell()
1229 # unexpected EOF?
1230 if len(hdata) != h.header_size:
1231 self._set_error("Unexpected EOF when reading header")
1232 return None
1234 pos = S_BLK_HDR.size
1236 # block has data assiciated with it?
1237 if h.flags & RAR_LONG_BLOCK:
1238 h.add_size, pos = load_le32(hdata, pos)
1239 else:
1240 h.add_size = 0
1242 # parse interesting ones, decide header boundaries for crc
1243 if h.type == RAR_BLOCK_MARK:
1244 return h
1245 elif h.type == RAR_BLOCK_MAIN:
1246 pos += 6
1247 if h.flags & RAR_MAIN_ENCRYPTVER:
1248 pos += 1
1249 crc_pos = pos
1250 if h.flags & RAR_MAIN_COMMENT:
1251 self._parse_subblocks(h, hdata, pos)
1252 elif h.type == RAR_BLOCK_FILE:
1253 pos = self._parse_file_header(h, hdata, pos - 4)
1254 crc_pos = pos
1255 if h.flags & RAR_FILE_COMMENT:
1256 pos = self._parse_subblocks(h, hdata, pos)
1257 elif h.type == RAR_BLOCK_SUB:
1258 pos = self._parse_file_header(h, hdata, pos - 4)
1259 crc_pos = h.header_size
1260 elif h.type == RAR_BLOCK_OLD_AUTH:
1261 pos += 8
1262 crc_pos = pos
1263 elif h.type == RAR_BLOCK_OLD_EXTRA:
1264 pos += 7
1265 crc_pos = pos
1266 else:
1267 crc_pos = h.header_size
1269 # check crc
1270 if h.type == RAR_BLOCK_OLD_SUB:
1271 crcdat = hdata[2:] + fd.read(h.add_size)
1272 else:
1273 crcdat = hdata[2:crc_pos]
1275 calc_crc = rar_crc32(crcdat) & 0xFFFF
1277 # return good header
1278 if h.header_crc == calc_crc:
1279 return h
1281 # header parsing failed.
1282 self._set_error("Header CRC error (%02x): exp=%x got=%x (xlen = %d)",
1283 h.type, h.header_crc, calc_crc, len(crcdat))
1285 # instead panicing, send eof
1286 return None
1288 # read file-specific header
1289 def _parse_file_header(self, h, hdata, pos):
1290 fld = S_FILE_HDR.unpack_from(hdata, pos)
1291 pos += S_FILE_HDR.size
1293 h.compress_size = fld[0]
1294 h.file_size = fld[1]
1295 h.host_os = fld[2]
1296 h.CRC = fld[3]
1297 h.date_time = parse_dos_time(fld[4])
1298 h.mtime = to_datetime(h.date_time)
1299 h.extract_version = fld[5]
1300 h.compress_type = fld[6]
1301 name_size = fld[7]
1302 h.mode = fld[8]
1304 h._md_class = CRC32Context
1305 h._md_expect = h.CRC
1307 if h.flags & RAR_FILE_LARGE:
1308 h1, pos = load_le32(hdata, pos)
1309 h2, pos = load_le32(hdata, pos)
1310 h.compress_size |= h1 << 32
1311 h.file_size |= h2 << 32
1312 h.add_size = h.compress_size
1314 name, pos = load_bytes(hdata, name_size, pos)
1315 if h.flags & RAR_FILE_UNICODE:
1316 nul = name.find(ZERO)
1317 h.orig_filename = name[:nul]
1318 u = UnicodeFilename(h.orig_filename, name[nul + 1:])
1319 h.filename = u.decode()
1321 # if parsing failed fall back to simple name
1322 if u.failed:
1323 h.filename = self._decode(h.orig_filename)
1324 else:
1325 h.orig_filename = name
1326 h.filename = self._decode(name)
1328 # change separator, if requested
1329 if PATH_SEP != "\\":
1330 h.filename = h.filename.replace("\\", PATH_SEP)
1332 if h.flags & RAR_FILE_SALT:
1333 h.salt, pos = load_bytes(hdata, 8, pos)
1334 else:
1335 h.salt = None
1337 # optional extended time stamps
1338 if h.flags & RAR_FILE_EXTTIME:
1339 pos = _parse_ext_time(h, hdata, pos)
1340 else:
1341 h.mtime = h.atime = h.ctime = h.arctime = None
1343 return pos
1345 # find old-style comment subblock
1346 def _parse_subblocks(self, h, hdata, pos):
1347 while pos < len(hdata):
1348 # ordinary block header
1349 t = S_BLK_HDR.unpack_from(hdata, pos)
1350 ___scrc, stype, sflags, slen = t
1351 pos_next = pos + slen
1352 pos += S_BLK_HDR.size
1354 # corrupt header
1355 if pos_next < pos:
1356 break
1358 # followed by block-specific header
1359 if stype == RAR_BLOCK_OLD_COMMENT and pos + S_COMMENT_HDR.size <= pos_next:
1360 declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos)
1361 pos += S_COMMENT_HDR.size
1362 data = hdata[pos : pos_next]
1363 cmt = rar3_decompress(ver, meth, data, declen, sflags,
1364 crc, self._password)
1365 if not self._crc_check:
1366 h.comment = self._decode_comment(cmt)
1367 elif rar_crc32(cmt) & 0xFFFF == crc:
1368 h.comment = self._decode_comment(cmt)
1370 pos = pos_next
1371 return pos
1373 def _read_comment_v3(self, inf, psw=None):
1375 # read data
1376 with XFile(inf.volume_file) as rf:
1377 rf.seek(inf.data_offset)
1378 data = rf.read(inf.compress_size)
1380 # decompress
1381 cmt = rar3_decompress(inf.extract_version, inf.compress_type, data,
1382 inf.file_size, inf.flags, inf.CRC, psw, inf.salt)
1384 # check crc
1385 if self._crc_check:
1386 crc = rar_crc32(cmt)
1387 if crc != inf.CRC:
1388 return None
1390 return self._decode_comment(cmt)
1392 def _decode(self, val):
1393 for c in TRY_ENCODINGS:
1394 try:
1395 return val.decode(c)
1396 except UnicodeError:
1397 pass
1398 return val.decode(self._charset, "replace")
1400 def _decode_comment(self, val):
1401 return self._decode(val)
1403 def process_entry(self, fd, item):
1404 if item.type == RAR_BLOCK_FILE:
1405 # use only first part
1406 if (item.flags & RAR_FILE_SPLIT_BEFORE) == 0:
1407 self._info_map[item.filename] = item
1408 self._info_list.append(item)
1409 elif len(self._info_list) > 0:
1410 # final crc is in last block
1411 old = self._info_list[-1]
1412 old.CRC = item.CRC
1413 old._md_expect = item._md_expect
1414 old.compress_size += item.compress_size
1416 # parse new-style comment
1417 if item.type == RAR_BLOCK_SUB and item.filename == "CMT":
1418 if item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
1419 pass
1420 elif item.flags & RAR_FILE_SOLID:
1421 # file comment
1422 cmt = self._read_comment_v3(item, self._password)
1423 if len(self._info_list) > 0:
1424 old = self._info_list[-1]
1425 old.comment = cmt
1426 else:
1427 # archive comment
1428 cmt = self._read_comment_v3(item, self._password)
1429 self.comment = cmt
1431 if item.type == RAR_BLOCK_MAIN:
1432 if item.flags & RAR_MAIN_COMMENT:
1433 self.comment = item.comment
1434 if item.flags & RAR_MAIN_PASSWORD:
1435 self._needs_password = True
1437 # put file compressed data into temporary .rar archive, and run
1438 # unrar on that, thus avoiding unrar going over whole archive
1439 def _open_hack(self, inf, psw):
1440 # create main header: crc, type, flags, size, res1, res2
1441 prefix = RAR_ID + S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + ZERO * (2 + 4)
1442 return self._open_hack_core(inf, psw, prefix, EMPTY)
1445 # RAR5 format
1448 class Rar5Info(RarInfo):
1449 """Shared fields for RAR5 records.
1451 extract_version = 50
1452 header_crc = None
1453 header_size = None
1454 header_offset = None
1455 data_offset = None
1457 # type=all
1458 block_type = None
1459 block_flags = None
1460 add_size = 0
1461 block_extra_size = 0
1463 # type=MAIN
1464 volume_number = None
1465 _md_class = None
1466 _md_expect = None
1468 def _must_disable_hack(self):
1469 return False
1472 class Rar5BaseFile(Rar5Info):
1473 """Shared sturct for file & service record.
1475 type = -1
1476 file_flags = None
1477 file_encryption = (0, 0, 0, EMPTY, EMPTY, EMPTY)
1478 file_compress_flags = None
1479 file_redir = None
1480 file_owner = None
1481 file_version = None
1482 blake2sp_hash = None
1484 def _must_disable_hack(self):
1485 if self.flags & RAR_FILE_PASSWORD:
1486 return True
1487 if self.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER):
1488 return True
1489 if self.file_compress_flags & RAR5_COMPR_SOLID:
1490 return True
1491 if self.file_redir:
1492 return True
1493 return False
1496 class Rar5FileInfo(Rar5BaseFile):
1497 """RAR5 file record.
1499 type = RAR_BLOCK_FILE
1502 class Rar5ServiceInfo(Rar5BaseFile):
1503 """RAR5 service record.
1505 type = RAR_BLOCK_SUB
1508 class Rar5MainInfo(Rar5Info):
1509 """RAR5 archive main record.
1511 type = RAR_BLOCK_MAIN
1512 main_flags = None
1513 main_volume_number = None
1515 def _must_disable_hack(self):
1516 if self.main_flags & RAR5_MAIN_FLAG_SOLID:
1517 return True
1518 return False
1521 class Rar5EncryptionInfo(Rar5Info):
1522 """RAR5 archive header encryption record.
1524 type = RAR5_BLOCK_ENCRYPTION
1525 encryption_algo = None
1526 encryption_flags = None
1527 encryption_kdf_count = None
1528 encryption_salt = None
1529 encryption_check_value = None
1531 def needs_password(self):
1532 return True
1535 class Rar5EndArcInfo(Rar5Info):
1536 """RAR5 end of archive record.
1538 type = RAR_BLOCK_ENDARC
1539 endarc_flags = None
1542 class RAR5Parser(CommonParser):
1543 """Parse RAR5 format.
1545 _expect_sig = RAR5_ID
1546 _hdrenc_main = None
1548 # AES encrypted headers
1549 _last_aes256_key = (-1, None, None) # (kdf_count, salt, key)
1551 def _gen_key(self, kdf_count, salt):
1552 if self._last_aes256_key[:2] == (kdf_count, salt):
1553 return self._last_aes256_key[2]
1554 if kdf_count > 24:
1555 raise BadRarFile("Too large kdf_count")
1556 psw = self._password
1557 if isinstance(psw, str):
1558 psw = psw.encode("utf8")
1559 key = pbkdf2_sha256(psw, salt, 1 << kdf_count)
1560 self._last_aes256_key = (kdf_count, salt, key)
1561 return key
1563 def _decrypt_header(self, fd):
1564 if not _have_crypto:
1565 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1566 h = self._hdrenc_main
1567 key = self._gen_key(h.encryption_kdf_count, h.encryption_salt)
1568 iv = fd.read(16)
1569 return HeaderDecrypt(fd, key, iv)
1571 # common header
1572 def _parse_block_header(self, fd):
1573 header_offset = fd.tell()
1575 preload = 4 + 3
1576 start_bytes = fd.read(preload)
1577 header_crc, pos = load_le32(start_bytes, 0)
1578 hdrlen, pos = load_vint(start_bytes, pos)
1579 if hdrlen > 2 * 1024 * 1024:
1580 return None
1581 header_size = pos + hdrlen
1583 # read full header, check for EOF
1584 hdata = start_bytes + fd.read(header_size - len(start_bytes))
1585 if len(hdata) != header_size:
1586 self._set_error("Unexpected EOF when reading header")
1587 return None
1588 data_offset = fd.tell()
1590 calc_crc = rar_crc32(memoryview(hdata)[4:])
1591 if header_crc != calc_crc:
1592 # header parsing failed.
1593 self._set_error("Header CRC error: exp=%x got=%x (xlen = %d)",
1594 header_crc, calc_crc, len(hdata))
1595 return None
1597 block_type, pos = load_vint(hdata, pos)
1599 if block_type == RAR5_BLOCK_MAIN:
1600 h, pos = self._parse_block_common(Rar5MainInfo(), hdata)
1601 h = self._parse_main_block(h, hdata, pos)
1602 elif block_type == RAR5_BLOCK_FILE:
1603 h, pos = self._parse_block_common(Rar5FileInfo(), hdata)
1604 h = self._parse_file_block(h, hdata, pos)
1605 elif block_type == RAR5_BLOCK_SERVICE:
1606 h, pos = self._parse_block_common(Rar5ServiceInfo(), hdata)
1607 h = self._parse_file_block(h, hdata, pos)
1608 elif block_type == RAR5_BLOCK_ENCRYPTION:
1609 h, pos = self._parse_block_common(Rar5EncryptionInfo(), hdata)
1610 h = self._parse_encryption_block(h, hdata, pos)
1611 elif block_type == RAR5_BLOCK_ENDARC:
1612 h, pos = self._parse_block_common(Rar5EndArcInfo(), hdata)
1613 h = self._parse_endarc_block(h, hdata, pos)
1614 else:
1615 h = None
1616 if h:
1617 h.header_offset = header_offset
1618 h.data_offset = data_offset
1619 return h
1621 def _parse_block_common(self, h, hdata):
1622 h.header_crc, pos = load_le32(hdata, 0)
1623 hdrlen, pos = load_vint(hdata, pos)
1624 h.header_size = hdrlen + pos
1625 h.block_type, pos = load_vint(hdata, pos)
1626 h.block_flags, pos = load_vint(hdata, pos)
1628 if h.block_flags & RAR5_BLOCK_FLAG_EXTRA_DATA:
1629 h.block_extra_size, pos = load_vint(hdata, pos)
1630 if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA:
1631 h.add_size, pos = load_vint(hdata, pos)
1633 h.compress_size = h.add_size
1635 if h.block_flags & RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN:
1636 h.flags |= RAR_SKIP_IF_UNKNOWN
1637 if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA:
1638 h.flags |= RAR_LONG_BLOCK
1639 return h, pos
1641 def _parse_main_block(self, h, hdata, pos):
1642 h.main_flags, pos = load_vint(hdata, pos)
1643 if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR:
1644 h.main_volume_number, pos = load_vint(hdata, pos)
1646 h.flags |= RAR_MAIN_NEWNUMBERING
1647 if h.main_flags & RAR5_MAIN_FLAG_SOLID:
1648 h.flags |= RAR_MAIN_SOLID
1649 if h.main_flags & RAR5_MAIN_FLAG_ISVOL:
1650 h.flags |= RAR_MAIN_VOLUME
1651 if h.main_flags & RAR5_MAIN_FLAG_RECOVERY:
1652 h.flags |= RAR_MAIN_RECOVERY
1653 if self._hdrenc_main:
1654 h.flags |= RAR_MAIN_PASSWORD
1655 if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR == 0:
1656 h.flags |= RAR_MAIN_FIRSTVOLUME
1658 return h
1660 def _parse_file_block(self, h, hdata, pos):
1661 h.file_flags, pos = load_vint(hdata, pos)
1662 h.file_size, pos = load_vint(hdata, pos)
1663 h.mode, pos = load_vint(hdata, pos)
1665 if h.file_flags & RAR5_FILE_FLAG_HAS_MTIME:
1666 h.mtime, pos = load_unixtime(hdata, pos)
1667 h.date_time = h.mtime.timetuple()[:6]
1668 if h.file_flags & RAR5_FILE_FLAG_HAS_CRC32:
1669 h.CRC, pos = load_le32(hdata, pos)
1670 h._md_class = CRC32Context
1671 h._md_expect = h.CRC
1673 h.file_compress_flags, pos = load_vint(hdata, pos)
1674 h.file_host_os, pos = load_vint(hdata, pos)
1675 h.orig_filename, pos = load_vstr(hdata, pos)
1676 h.filename = h.orig_filename.decode("utf8", "replace")
1678 # use compatible values
1679 if h.file_host_os == RAR5_OS_WINDOWS:
1680 h.host_os = RAR_OS_WIN32
1681 else:
1682 h.host_os = RAR_OS_UNIX
1683 h.compress_type = RAR_M0 + ((h.file_compress_flags >> 7) & 7)
1685 if h.block_extra_size:
1686 # allow 1 byte of garbage
1687 while pos < len(hdata) - 1:
1688 xsize, pos = load_vint(hdata, pos)
1689 xdata, pos = load_bytes(hdata, xsize, pos)
1690 self._process_file_extra(h, xdata)
1692 if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE:
1693 h.flags |= RAR_FILE_SPLIT_BEFORE
1694 if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_AFTER:
1695 h.flags |= RAR_FILE_SPLIT_AFTER
1696 if h.file_flags & RAR5_FILE_FLAG_ISDIR:
1697 h.flags |= RAR_FILE_DIRECTORY
1698 if h.file_compress_flags & RAR5_COMPR_SOLID:
1699 h.flags |= RAR_FILE_SOLID
1701 return h
1703 def _parse_endarc_block(self, h, hdata, pos):
1704 h.endarc_flags, pos = load_vint(hdata, pos)
1705 if h.endarc_flags & RAR5_ENDARC_FLAG_NEXT_VOL:
1706 h.flags |= RAR_ENDARC_NEXT_VOLUME
1707 return h
1709 def _parse_encryption_block(self, h, hdata, pos):
1710 h.encryption_algo, pos = load_vint(hdata, pos)
1711 h.encryption_flags, pos = load_vint(hdata, pos)
1712 h.encryption_kdf_count, pos = load_byte(hdata, pos)
1713 h.encryption_salt, pos = load_bytes(hdata, 16, pos)
1714 if h.encryption_flags & RAR5_ENC_FLAG_HAS_CHECKVAL:
1715 h.encryption_check_value = load_bytes(hdata, 12, pos)
1716 if h.encryption_algo != RAR5_XENC_CIPHER_AES256:
1717 raise BadRarFile("Unsupported header encryption cipher")
1718 self._hdrenc_main = h
1719 return h
1721 # file extra record
1722 def _process_file_extra(self, h, xdata):
1723 xtype, pos = load_vint(xdata, 0)
1724 if xtype == RAR5_XFILE_TIME:
1725 self._parse_file_xtime(h, xdata, pos)
1726 elif xtype == RAR5_XFILE_ENCRYPTION:
1727 self._parse_file_encryption(h, xdata, pos)
1728 elif xtype == RAR5_XFILE_HASH:
1729 self._parse_file_hash(h, xdata, pos)
1730 elif xtype == RAR5_XFILE_VERSION:
1731 self._parse_file_version(h, xdata, pos)
1732 elif xtype == RAR5_XFILE_REDIR:
1733 self._parse_file_redir(h, xdata, pos)
1734 elif xtype == RAR5_XFILE_OWNER:
1735 self._parse_file_owner(h, xdata, pos)
1736 elif xtype == RAR5_XFILE_SERVICE:
1737 pass
1738 else:
1739 pass
1741 # extra block for file time record
1742 def _parse_file_xtime(self, h, xdata, pos):
1743 tflags, pos = load_vint(xdata, pos)
1744 ldr = load_windowstime
1745 if tflags & RAR5_XTIME_UNIXTIME:
1746 ldr = load_unixtime
1747 if tflags & RAR5_XTIME_HAS_MTIME:
1748 h.mtime, pos = ldr(xdata, pos)
1749 h.date_time = h.mtime.timetuple()[:6]
1750 if tflags & RAR5_XTIME_HAS_CTIME:
1751 h.ctime, pos = ldr(xdata, pos)
1752 if tflags & RAR5_XTIME_HAS_ATIME:
1753 h.atime, pos = ldr(xdata, pos)
1755 # just remember encryption info
1756 def _parse_file_encryption(self, h, xdata, pos):
1757 algo, pos = load_vint(xdata, pos)
1758 flags, pos = load_vint(xdata, pos)
1759 kdf_count, pos = load_byte(xdata, pos)
1760 salt, pos = load_bytes(xdata, 16, pos)
1761 iv, pos = load_bytes(xdata, 16, pos)
1762 checkval = None
1763 if flags & RAR5_XENC_CHECKVAL:
1764 checkval, pos = load_bytes(xdata, 12, pos)
1765 if flags & RAR5_XENC_TWEAKED:
1766 h._md_expect = None
1767 h._md_class = NoHashContext
1769 h.file_encryption = (algo, flags, kdf_count, salt, iv, checkval)
1770 h.flags |= RAR_FILE_PASSWORD
1772 def _parse_file_hash(self, h, xdata, pos):
1773 hash_type, pos = load_vint(xdata, pos)
1774 if hash_type == RAR5_XHASH_BLAKE2SP:
1775 h.blake2sp_hash, pos = load_bytes(xdata, 32, pos)
1776 if (h.file_encryption[1] & RAR5_XENC_TWEAKED) == 0:
1777 h._md_class = Blake2SP
1778 h._md_expect = h.blake2sp_hash
1780 def _parse_file_version(self, h, xdata, pos):
1781 flags, pos = load_vint(xdata, pos)
1782 version, pos = load_vint(xdata, pos)
1783 h.file_version = (flags, version)
1785 def _parse_file_redir(self, h, xdata, pos):
1786 redir_type, pos = load_vint(xdata, pos)
1787 redir_flags, pos = load_vint(xdata, pos)
1788 redir_name, pos = load_vstr(xdata, pos)
1789 redir_name = redir_name.decode("utf8", "replace")
1790 h.file_redir = (redir_type, redir_flags, redir_name)
1792 def _parse_file_owner(self, h, xdata, pos):
1793 user_name = group_name = user_id = group_id = None
1795 flags, pos = load_vint(xdata, pos)
1796 if flags & RAR5_XOWNER_UNAME:
1797 user_name, pos = load_vstr(xdata, pos)
1798 if flags & RAR5_XOWNER_GNAME:
1799 group_name, pos = load_vstr(xdata, pos)
1800 if flags & RAR5_XOWNER_UID:
1801 user_id, pos = load_vint(xdata, pos)
1802 if flags & RAR5_XOWNER_GID:
1803 group_id, pos = load_vint(xdata, pos)
1805 h.file_owner = (user_name, group_name, user_id, group_id)
1807 def process_entry(self, fd, item):
1808 if item.block_type == RAR5_BLOCK_FILE:
1809 # use only first part
1810 if (item.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE) == 0:
1811 self._info_map[item.filename] = item
1812 self._info_list.append(item)
1813 elif len(self._info_list) > 0:
1814 # final crc is in last block
1815 old = self._info_list[-1]
1816 old.CRC = item.CRC
1817 old._md_expect = item._md_expect
1818 old.blake2sp_hash = item.blake2sp_hash
1819 old.compress_size += item.compress_size
1820 elif item.block_type == RAR5_BLOCK_SERVICE:
1821 if item.filename == "CMT":
1822 self._load_comment(fd, item)
1824 def _load_comment(self, fd, item):
1825 if item.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER):
1826 return None
1827 if item.compress_type != RAR_M0:
1828 return None
1830 if item.flags & RAR_FILE_PASSWORD:
1831 algo, ___flags, kdf_count, salt, iv, ___checkval = item.file_encryption
1832 if algo != RAR5_XENC_CIPHER_AES256:
1833 return None
1834 key = self._gen_key(kdf_count, salt)
1835 f = HeaderDecrypt(fd, key, iv)
1836 cmt = f.read(item.file_size)
1837 else:
1838 # archive comment
1839 with self._open_clear(item) as cmtstream:
1840 cmt = cmtstream.read()
1842 # rar bug? - appends zero to comment
1843 cmt = cmt.split(ZERO, 1)[0]
1844 self.comment = cmt.decode("utf8")
1845 return None
1847 def _open_hack(self, inf, psw):
1848 # len, type, blk_flags, flags
1849 main_hdr = b"\x03\x01\x00\x00"
1850 endarc_hdr = b"\x03\x05\x00\x00"
1851 main_hdr = S_LONG.pack(rar_crc32(main_hdr)) + main_hdr
1852 endarc_hdr = S_LONG.pack(rar_crc32(endarc_hdr)) + endarc_hdr
1853 return self._open_hack_core(inf, psw, RAR5_ID + main_hdr, endarc_hdr)
1856 ## Utility classes
1859 class UnicodeFilename(object):
1860 """Handle RAR3 unicode filename decompression.
1862 def __init__(self, name, encdata):
1863 self.std_name = bytearray(name)
1864 self.encdata = bytearray(encdata)
1865 self.pos = self.encpos = 0
1866 self.buf = bytearray()
1867 self.failed = 0
1869 def enc_byte(self):
1870 """Copy encoded byte."""
1871 try:
1872 c = self.encdata[self.encpos]
1873 self.encpos += 1
1874 return c
1875 except IndexError:
1876 self.failed = 1
1877 return 0
1879 def std_byte(self):
1880 """Copy byte from 8-bit representation."""
1881 try:
1882 return self.std_name[self.pos]
1883 except IndexError:
1884 self.failed = 1
1885 return ord("?")
1887 def put(self, lo, hi):
1888 """Copy 16-bit value to result."""
1889 self.buf.append(lo)
1890 self.buf.append(hi)
1891 self.pos += 1
1893 def decode(self):
1894 """Decompress compressed UTF16 value."""
1895 hi = self.enc_byte()
1896 flagbits = 0
1897 while self.encpos < len(self.encdata):
1898 if flagbits == 0:
1899 flags = self.enc_byte()
1900 flagbits = 8
1901 flagbits -= 2
1902 t = (flags >> flagbits) & 3
1903 if t == 0:
1904 self.put(self.enc_byte(), 0)
1905 elif t == 1:
1906 self.put(self.enc_byte(), hi)
1907 elif t == 2:
1908 self.put(self.enc_byte(), self.enc_byte())
1909 else:
1910 n = self.enc_byte()
1911 if n & 0x80:
1912 c = self.enc_byte()
1913 for _ in range((n & 0x7f) + 2):
1914 lo = (self.std_byte() + c) & 0xFF
1915 self.put(lo, hi)
1916 else:
1917 for _ in range(n + 2):
1918 self.put(self.std_byte(), 0)
1919 return self.buf.decode("utf-16le", "replace")
1922 class RarExtFile(RawIOBase):
1923 """Base class for file-like object that :meth:`RarFile.open` returns.
1925 Provides public methods and common crc checking.
1927 Behaviour:
1928 - no short reads - .read() and .readinfo() read as much as requested.
1929 - no internal buffer, use io.BufferedReader for that.
1932 #: Filename of the archive entry
1933 name = None
1935 def __init__(self, parser, inf):
1936 """Open archive entry.
1938 super(RarExtFile, self).__init__()
1940 # standard io.* properties
1941 self.name = inf.filename
1942 self.mode = "rb"
1944 self._parser = parser
1945 self._inf = inf
1946 self._fd = None
1947 self._remain = 0
1948 self._returncode = 0
1950 self._md_context = None
1952 self._open()
1954 def _open(self):
1955 if self._fd:
1956 self._fd.close()
1957 md_class = self._inf._md_class or NoHashContext
1958 self._md_context = md_class()
1959 self._fd = None
1960 self._remain = self._inf.file_size
1962 def read(self, cnt=None):
1963 """Read all or specified amount of data from archive entry."""
1965 # sanitize cnt
1966 if cnt is None or cnt < 0:
1967 cnt = self._remain
1968 elif cnt > self._remain:
1969 cnt = self._remain
1970 if cnt == 0:
1971 return EMPTY
1973 buf = []
1974 orig = cnt
1975 while cnt > 0:
1976 # actual read
1977 data = self._read(cnt)
1978 if not data:
1979 break
1980 buf.append(data)
1981 self._md_context.update(data)
1982 self._remain -= len(data)
1983 cnt -= len(data)
1984 data = EMPTY.join(buf)
1985 if cnt > 0:
1986 raise BadRarFile("Failed the read enough data: req=%d got=%d" % (orig, len(data)))
1988 # done?
1989 if not data or self._remain == 0:
1990 # self.close()
1991 self._check()
1992 return data
1994 def _check(self):
1995 """Check final CRC."""
1996 final = self._md_context.digest()
1997 exp = self._inf._md_expect
1998 if exp is None:
1999 return
2000 if final is None:
2001 return
2002 if self._returncode:
2003 check_returncode(self, "")
2004 if self._remain != 0:
2005 raise BadRarFile("Failed the read enough data")
2006 if final != exp:
2007 raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % (
2008 self._inf.filename, exp, final))
2010 def _read(self, cnt):
2011 """Actual read that gets sanitized cnt."""
2012 raise NotImplementedError("_read")
2014 def close(self):
2015 """Close open resources."""
2017 super(RarExtFile, self).close()
2019 if self._fd:
2020 self._fd.close()
2021 self._fd = None
2023 def __del__(self):
2024 """Hook delete to make sure tempfile is removed."""
2025 self.close()
2027 def readinto(self, buf):
2028 """Zero-copy read directly into buffer.
2030 Returns bytes read.
2032 raise NotImplementedError("readinto")
2034 def tell(self):
2035 """Return current reading position in uncompressed data."""
2036 return self._inf.file_size - self._remain
2038 def seek(self, ofs, whence=0):
2039 """Seek in data.
2041 On uncompressed files, the seeking works by actual
2042 seeks so it's fast. On compresses files its slow
2043 - forward seeking happends by reading ahead,
2044 backwards by re-opening and decompressing from the start.
2047 # disable crc check when seeking
2048 self._md_context = NoHashContext()
2050 fsize = self._inf.file_size
2051 cur_ofs = self.tell()
2053 if whence == 0: # seek from beginning of file
2054 new_ofs = ofs
2055 elif whence == 1: # seek from current position
2056 new_ofs = cur_ofs + ofs
2057 elif whence == 2: # seek from end of file
2058 new_ofs = fsize + ofs
2059 else:
2060 raise ValueError("Invalid value for whence")
2062 # sanity check
2063 if new_ofs < 0:
2064 new_ofs = 0
2065 elif new_ofs > fsize:
2066 new_ofs = fsize
2068 # do the actual seek
2069 if new_ofs >= cur_ofs:
2070 self._skip(new_ofs - cur_ofs)
2071 else:
2072 # reopen and seek
2073 self._open()
2074 self._skip(new_ofs)
2075 return self.tell()
2077 def _skip(self, cnt):
2078 """Read and discard data"""
2079 while cnt > 0:
2080 if cnt > 8192:
2081 buf = self.read(8192)
2082 else:
2083 buf = self.read(cnt)
2084 if not buf:
2085 break
2086 cnt -= len(buf)
2088 def readable(self):
2089 """Returns True"""
2090 return True
2092 def writable(self):
2093 """Returns False.
2095 Writing is not supported.
2097 return False
2099 def seekable(self):
2100 """Returns True.
2102 Seeking is supported, although it's slow on compressed files.
2104 return True
2106 def readall(self):
2107 """Read all remaining data"""
2108 # avoid RawIOBase default impl
2109 return self.read()
2112 class PipeReader(RarExtFile):
2113 """Read data from pipe, handle tempfile cleanup."""
2115 def __init__(self, rf, inf, cmd, tempfile=None):
2116 self._cmd = cmd
2117 self._proc = None
2118 self._tempfile = tempfile
2119 super(PipeReader, self).__init__(rf, inf)
2121 def _close_proc(self):
2122 if not self._proc:
2123 return
2124 if self._proc.stdout:
2125 self._proc.stdout.close()
2126 if self._proc.stdin:
2127 self._proc.stdin.close()
2128 if self._proc.stderr:
2129 self._proc.stderr.close()
2130 self._proc.wait()
2131 self._returncode = self._proc.returncode
2132 self._proc = None
2134 def _open(self):
2135 super(PipeReader, self)._open()
2137 # stop old process
2138 self._close_proc()
2140 # launch new process
2141 self._returncode = 0
2142 self._proc = custom_popen(self._cmd)
2143 self._fd = self._proc.stdout
2145 # avoid situation where unrar waits on stdin
2146 if self._proc.stdin:
2147 self._proc.stdin.close()
2149 def _read(self, cnt):
2150 """Read from pipe."""
2152 # normal read is usually enough
2153 data = self._fd.read(cnt)
2154 if len(data) == cnt or not data:
2155 return data
2157 # short read, try looping
2158 buf = [data]
2159 cnt -= len(data)
2160 while cnt > 0:
2161 data = self._fd.read(cnt)
2162 if not data:
2163 break
2164 cnt -= len(data)
2165 buf.append(data)
2166 return EMPTY.join(buf)
2168 def close(self):
2169 """Close open resources."""
2171 self._close_proc()
2172 super(PipeReader, self).close()
2174 if self._tempfile:
2175 try:
2176 os.unlink(self._tempfile)
2177 except OSError:
2178 pass
2179 self._tempfile = None
2181 def readinto(self, buf):
2182 """Zero-copy read directly into buffer."""
2183 cnt = len(buf)
2184 if cnt > self._remain:
2185 cnt = self._remain
2186 vbuf = memoryview(buf)
2187 res = got = 0
2188 while got < cnt:
2189 res = self._fd.readinto(vbuf[got : cnt])
2190 if not res:
2191 break
2192 self._md_context.update(vbuf[got : got + res])
2193 self._remain -= res
2194 got += res
2195 return got
2198 class DirectReader(RarExtFile):
2199 """Read uncompressed data directly from archive.
2201 _cur = None
2202 _cur_avail = None
2203 _volfile = None
2205 def _open(self):
2206 super(DirectReader, self)._open()
2208 self._volfile = self._inf.volume_file
2209 self._fd = XFile(self._volfile, 0)
2210 self._fd.seek(self._inf.header_offset, 0)
2211 self._cur = self._parser._parse_header(self._fd)
2212 self._cur_avail = self._cur.add_size
2214 def _skip(self, cnt):
2215 """RAR Seek, skipping through rar files to get to correct position
2218 while cnt > 0:
2219 # next vol needed?
2220 if self._cur_avail == 0:
2221 if not self._open_next():
2222 break
2224 # fd is in read pos, do the read
2225 if cnt > self._cur_avail:
2226 cnt -= self._cur_avail
2227 self._remain -= self._cur_avail
2228 self._cur_avail = 0
2229 else:
2230 self._fd.seek(cnt, 1)
2231 self._cur_avail -= cnt
2232 self._remain -= cnt
2233 cnt = 0
2235 def _read(self, cnt):
2236 """Read from potentially multi-volume archive."""
2238 buf = []
2239 while cnt > 0:
2240 # next vol needed?
2241 if self._cur_avail == 0:
2242 if not self._open_next():
2243 break
2245 # fd is in read pos, do the read
2246 if cnt > self._cur_avail:
2247 data = self._fd.read(self._cur_avail)
2248 else:
2249 data = self._fd.read(cnt)
2250 if not data:
2251 break
2253 # got some data
2254 cnt -= len(data)
2255 self._cur_avail -= len(data)
2256 buf.append(data)
2258 if len(buf) == 1:
2259 return buf[0]
2260 return EMPTY.join(buf)
2262 def _open_next(self):
2263 """Proceed to next volume."""
2265 # is the file split over archives?
2266 if (self._cur.flags & RAR_FILE_SPLIT_AFTER) == 0:
2267 return False
2269 if self._fd:
2270 self._fd.close()
2271 self._fd = None
2273 # open next part
2274 self._volfile = self._parser._next_volname(self._volfile)
2275 fd = open(self._volfile, "rb", 0)
2276 self._fd = fd
2277 sig = fd.read(len(self._parser._expect_sig))
2278 if sig != self._parser._expect_sig:
2279 raise BadRarFile("Invalid signature")
2281 # loop until first file header
2282 while 1:
2283 cur = self._parser._parse_header(fd)
2284 if not cur:
2285 raise BadRarFile("Unexpected EOF")
2286 if cur.type in (RAR_BLOCK_MARK, RAR_BLOCK_MAIN):
2287 if cur.add_size:
2288 fd.seek(cur.add_size, 1)
2289 continue
2290 if cur.orig_filename != self._inf.orig_filename:
2291 raise BadRarFile("Did not found file entry")
2292 self._cur = cur
2293 self._cur_avail = cur.add_size
2294 return True
2296 def readinto(self, buf):
2297 """Zero-copy read directly into buffer."""
2298 got = 0
2299 vbuf = memoryview(buf)
2300 while got < len(buf):
2301 # next vol needed?
2302 if self._cur_avail == 0:
2303 if not self._open_next():
2304 break
2306 # length for next read
2307 cnt = len(buf) - got
2308 if cnt > self._cur_avail:
2309 cnt = self._cur_avail
2311 # read into temp view
2312 res = self._fd.readinto(vbuf[got : got + cnt])
2313 if not res:
2314 break
2315 self._md_context.update(vbuf[got : got + res])
2316 self._cur_avail -= res
2317 self._remain -= res
2318 got += res
2319 return got
2322 class HeaderDecrypt(object):
2323 """File-like object that decrypts from another file"""
2324 def __init__(self, f, key, iv):
2325 self.f = f
2326 self.ciph = AES_CBC_Decrypt(key, iv)
2327 self.buf = EMPTY
2329 def tell(self):
2330 """Current file pos - works only on block boundaries."""
2331 return self.f.tell()
2333 def read(self, cnt=None):
2334 """Read and decrypt."""
2335 if cnt > 8 * 1024:
2336 raise BadRarFile("Bad count to header decrypt - wrong password?")
2338 # consume old data
2339 if cnt <= len(self.buf):
2340 res = self.buf[:cnt]
2341 self.buf = self.buf[cnt:]
2342 return res
2343 res = self.buf
2344 self.buf = EMPTY
2345 cnt -= len(res)
2347 # decrypt new data
2348 blklen = 16
2349 while cnt > 0:
2350 enc = self.f.read(blklen)
2351 if len(enc) < blklen:
2352 break
2353 dec = self.ciph.decrypt(enc)
2354 if cnt >= len(dec):
2355 res += dec
2356 cnt -= len(dec)
2357 else:
2358 res += dec[:cnt]
2359 self.buf = dec[cnt:]
2360 cnt = 0
2362 return res
2365 # handle (filename|filelike) object
2366 class XFile(object):
2367 """Input may be filename or file object.
2369 __slots__ = ("_fd", "_need_close")
2371 def __init__(self, xfile, bufsize=1024):
2372 if is_filelike(xfile):
2373 self._need_close = False
2374 self._fd = xfile
2375 self._fd.seek(0)
2376 else:
2377 self._need_close = True
2378 self._fd = open(xfile, "rb", bufsize)
2380 def read(self, n=None):
2381 """Read from file."""
2382 return self._fd.read(n)
2384 def tell(self):
2385 """Return file pos."""
2386 return self._fd.tell()
2388 def seek(self, ofs, whence=0):
2389 """Move file pos."""
2390 return self._fd.seek(ofs, whence)
2392 def readinto(self, dst):
2393 """Read into buffer."""
2394 return self._fd.readinto(dst)
2396 def close(self):
2397 """Close file object."""
2398 if self._need_close:
2399 self._fd.close()
2401 def __enter__(self):
2402 return self
2404 def __exit__(self, typ, val, tb):
2405 self.close()
2408 class NoHashContext(object):
2409 """No-op hash function."""
2410 def __init__(self, data=None):
2411 """Initialize"""
2412 def update(self, data):
2413 """Update data"""
2414 def digest(self):
2415 """Final hash"""
2416 def hexdigest(self):
2417 """Hexadecimal digest."""
2420 class CRC32Context(object):
2421 """Hash context that uses CRC32."""
2422 __slots__ = ["_crc"]
2424 def __init__(self, data=None):
2425 self._crc = 0
2426 if data:
2427 self.update(data)
2429 def update(self, data):
2430 """Process data."""
2431 self._crc = rar_crc32(data, self._crc)
2433 def digest(self):
2434 """Final hash."""
2435 return self._crc
2437 def hexdigest(self):
2438 """Hexadecimal digest."""
2439 return "%08x" % self.digest()
2442 class Blake2SP(object):
2443 """Blake2sp hash context.
2445 __slots__ = ["_thread", "_buf", "_cur", "_digest"]
2446 digest_size = 32
2447 block_size = 64
2448 parallelism = 8
2450 def __init__(self, data=None):
2451 self._buf = b""
2452 self._cur = 0
2453 self._digest = None
2454 self._thread = []
2456 for i in range(self.parallelism):
2457 ctx = self._blake2s(i, 0, i == (self.parallelism - 1))
2458 self._thread.append(ctx)
2460 if data:
2461 self.update(data)
2463 def _blake2s(self, ofs, depth, is_last):
2464 return blake2s(node_offset=ofs, node_depth=depth, last_node=is_last,
2465 depth=2, inner_size=32, fanout=self.parallelism)
2467 def _add_block(self, blk):
2468 self._thread[self._cur].update(blk)
2469 self._cur = (self._cur + 1) % self.parallelism
2471 def update(self, data):
2472 """Hash data.
2474 view = memoryview(data)
2475 bs = self.block_size
2476 if self._buf:
2477 need = bs - len(self._buf)
2478 if len(view) < need:
2479 self._buf += view.tobytes()
2480 return
2481 self._add_block(self._buf + view[:need].tobytes())
2482 view = view[need:]
2483 while len(view) >= bs:
2484 self._add_block(view[:bs])
2485 view = view[bs:]
2486 self._buf = view.tobytes()
2488 def digest(self):
2489 """Return final digest value.
2491 if self._digest is None:
2492 if self._buf:
2493 self._add_block(self._buf)
2494 self._buf = EMPTY
2495 ctx = self._blake2s(0, 1, True)
2496 for t in self._thread:
2497 ctx.update(t.digest())
2498 self._digest = ctx.digest()
2499 return self._digest
2501 def hexdigest(self):
2502 """Hexadecimal digest."""
2503 return tohex(self.digest())
2506 class Rar3Sha1(object):
2507 """Bug-compat for SHA1
2509 digest_size = 20
2510 block_size = 64
2512 _BLK_BE = struct.Struct(b">16L")
2513 _BLK_LE = struct.Struct(b"<16L")
2515 __slots__ = ("_nbytes", "_md", "_rarbug")
2517 def __init__(self, data=b"", rarbug=False):
2518 self._md = sha1()
2519 self._nbytes = 0
2520 self._rarbug = rarbug
2521 self.update(data)
2523 def update(self, data):
2524 """Process more data."""
2525 self._md.update(data)
2526 bufpos = self._nbytes & 63
2527 self._nbytes += len(data)
2529 if self._rarbug and len(data) > 64:
2530 dpos = self.block_size - bufpos
2531 while dpos + self.block_size <= len(data):
2532 self._corrupt(data, dpos)
2533 dpos += self.block_size
2535 def digest(self):
2536 """Return final state."""
2537 return self._md.digest()
2539 def hexdigest(self):
2540 """Return final state as hex string."""
2541 return self._md.hexdigest()
2543 def _corrupt(self, data, dpos):
2544 """Corruption from SHA1 core."""
2545 ws = list(self._BLK_BE.unpack_from(data, dpos))
2546 for t in range(16, 80):
2547 tmp = ws[(t - 3) & 15] ^ ws[(t - 8) & 15] ^ ws[(t - 14) & 15] ^ ws[(t - 16) & 15]
2548 ws[t & 15] = ((tmp << 1) | (tmp >> (32 - 1))) & 0xFFFFFFFF
2549 self._BLK_LE.pack_into(data, dpos, *ws)
2553 ## Utility functions
2556 S_LONG = Struct("<L")
2557 S_SHORT = Struct("<H")
2558 S_BYTE = Struct("<B")
2560 S_BLK_HDR = Struct("<HBHH")
2561 S_FILE_HDR = Struct("<LLBLLBBHL")
2562 S_COMMENT_HDR = Struct("<HBBH")
2564 def load_vint(buf, pos):
2565 """Load variable-size int."""
2566 limit = min(pos + 11, len(buf))
2567 res = ofs = 0
2568 while pos < limit:
2569 b = buf[pos]
2570 res += ((b & 0x7F) << ofs)
2571 pos += 1
2572 ofs += 7
2573 if b < 0x80:
2574 return res, pos
2575 raise BadRarFile("cannot load vint")
2577 def load_byte(buf, pos):
2578 """Load single byte"""
2579 end = pos + 1
2580 if end > len(buf):
2581 raise BadRarFile("cannot load byte")
2582 return S_BYTE.unpack_from(buf, pos)[0], end
2584 def load_le32(buf, pos):
2585 """Load little-endian 32-bit integer"""
2586 end = pos + 4
2587 if end > len(buf):
2588 raise BadRarFile("cannot load le32")
2589 return S_LONG.unpack_from(buf, pos)[0], pos + 4
2591 def load_bytes(buf, num, pos):
2592 """Load sequence of bytes"""
2593 end = pos + num
2594 if end > len(buf):
2595 raise BadRarFile("cannot load bytes")
2596 return buf[pos : end], end
2598 def load_vstr(buf, pos):
2599 """Load bytes prefixed by vint length"""
2600 slen, pos = load_vint(buf, pos)
2601 return load_bytes(buf, slen, pos)
2603 def load_dostime(buf, pos):
2604 """Load LE32 dos timestamp"""
2605 stamp, pos = load_le32(buf, pos)
2606 tup = parse_dos_time(stamp)
2607 return to_datetime(tup), pos
2609 def load_unixtime(buf, pos):
2610 """Load LE32 unix timestamp"""
2611 secs, pos = load_le32(buf, pos)
2612 dt = datetime.fromtimestamp(secs, UTC)
2613 return dt, pos
2615 def load_windowstime(buf, pos):
2616 """Load LE64 windows timestamp"""
2617 # unix epoch (1970) in seconds from windows epoch (1601)
2618 unix_epoch = 11644473600
2619 val1, pos = load_le32(buf, pos)
2620 val2, pos = load_le32(buf, pos)
2621 secs, n1secs = divmod((val2 << 32) | val1, 10000000)
2622 dt = datetime.fromtimestamp(secs - unix_epoch, UTC)
2623 dt = dt.replace(microsecond=n1secs // 10)
2624 return dt, pos
2626 # new-style next volume
2627 def _next_newvol(volfile):
2628 i = len(volfile) - 1
2629 while i >= 0:
2630 if volfile[i] >= "0" and volfile[i] <= "9":
2631 return _inc_volname(volfile, i)
2632 i -= 1
2633 raise BadRarName("Cannot construct volume name: " + volfile)
2635 # old-style next volume
2636 def _next_oldvol(volfile):
2637 # rar -> r00
2638 if volfile[-4:].lower() == ".rar":
2639 return volfile[:-2] + "00"
2640 return _inc_volname(volfile, len(volfile) - 1)
2642 # increase digits with carry, otherwise just increment char
2643 def _inc_volname(volfile, i):
2644 fn = list(volfile)
2645 while i >= 0:
2646 if fn[i] != "9":
2647 fn[i] = chr(ord(fn[i]) + 1)
2648 break
2649 fn[i] = "0"
2650 i -= 1
2651 return "".join(fn)
2653 # rar3 extended time fields
2654 def _parse_ext_time(h, data, pos):
2655 # flags and rest of data can be missing
2656 flags = 0
2657 if pos + 2 <= len(data):
2658 flags = S_SHORT.unpack_from(data, pos)[0]
2659 pos += 2
2661 mtime, pos = _parse_xtime(flags >> 3 * 4, data, pos, h.mtime)
2662 h.ctime, pos = _parse_xtime(flags >> 2 * 4, data, pos)
2663 h.atime, pos = _parse_xtime(flags >> 1 * 4, data, pos)
2664 h.arctime, pos = _parse_xtime(flags >> 0 * 4, data, pos)
2665 if mtime:
2666 h.mtime = mtime
2667 h.date_time = mtime.timetuple()[:6]
2668 return pos
2670 # rar3 one extended time field
2671 def _parse_xtime(flag, data, pos, basetime=None):
2672 res = None
2673 if flag & 8:
2674 if not basetime:
2675 basetime, pos = load_dostime(data, pos)
2677 # load second fractions
2678 rem = 0
2679 cnt = flag & 3
2680 for _ in range(cnt):
2681 b, pos = load_byte(data, pos)
2682 rem = (b << 16) | (rem >> 8)
2684 # convert 100ns units to microseconds
2685 usec = rem // 10
2686 if usec > 1000000:
2687 usec = 999999
2689 # dostime has room for 30 seconds only, correct if needed
2690 if flag & 4 and basetime.second < 59:
2691 res = basetime.replace(microsecond=usec, second=basetime.second + 1)
2692 else:
2693 res = basetime.replace(microsecond=usec)
2694 return res, pos
2696 def is_filelike(obj):
2697 """Filename or file object?
2699 filename_types = (bytes, str, Path)
2701 if isinstance(obj, filename_types):
2702 return False
2703 res = True
2704 for a in ("read", "tell", "seek"):
2705 res = res and hasattr(obj, a)
2706 if not res:
2707 raise ValueError("Invalid object passed as file")
2708 return True
2710 def rar3_s2k(psw, salt):
2711 """String-to-key hash for RAR3.
2713 if not isinstance(psw, str):
2714 psw = psw.decode("utf8")
2715 seed = bytearray(psw.encode("utf-16le") + salt)
2716 h = Rar3Sha1(rarbug=True)
2717 iv = EMPTY
2718 for i in range(16):
2719 for j in range(0x4000):
2720 cnt = S_LONG.pack(i * 0x4000 + j)
2721 h.update(seed)
2722 h.update(cnt[:3])
2723 if j == 0:
2724 iv += h.digest()[19:20]
2725 key_be = h.digest()[:16]
2726 key_le = pack("<LLLL", *unpack(">LLLL", key_be))
2727 return key_le, iv
2729 def rar3_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=None):
2730 """Decompress blob of compressed data.
2732 Used for data with non-standard header - eg. comments.
2734 # already uncompressed?
2735 if meth == RAR_M0 and (flags & RAR_FILE_PASSWORD) == 0:
2736 return data
2738 # take only necessary flags
2739 flags = flags & (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK)
2740 flags |= RAR_LONG_BLOCK
2742 # file header
2743 fname = b"data"
2744 date = ((2010 - 1980) << 25) + (12 << 21) + (31 << 16)
2745 mode = 0x20
2746 fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc,
2747 date, vers, meth, len(fname), mode)
2748 fhdr += fname
2749 if flags & RAR_FILE_SALT:
2750 if not salt:
2751 return EMPTY
2752 fhdr += salt
2754 # full header
2755 hlen = S_BLK_HDR.size + len(fhdr)
2756 hdr = S_BLK_HDR.pack(0, RAR_BLOCK_FILE, flags, hlen) + fhdr
2757 hcrc = rar_crc32(hdr[2:]) & 0xFFFF
2758 hdr = S_BLK_HDR.pack(hcrc, RAR_BLOCK_FILE, flags, hlen) + fhdr
2760 # archive main header
2761 mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + ZERO * (2 + 4)
2763 # decompress via temp rar
2764 setup = tool_setup()
2765 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR)
2766 tmpf = os.fdopen(tmpfd, "wb")
2767 try:
2768 tmpf.write(RAR_ID + mh + hdr + data)
2769 tmpf.close()
2771 curpsw = (flags & RAR_FILE_PASSWORD) and psw or None
2772 cmd = setup.open_cmdline(curpsw, tmpname)
2773 p = custom_popen(cmd)
2774 return p.communicate()[0]
2775 finally:
2776 tmpf.close()
2777 os.unlink(tmpname)
2779 def to_datetime(t):
2780 """Convert 6-part time tuple into datetime object.
2782 if t is None:
2783 return None
2785 # extract values
2786 year, mon, day, h, m, s = t
2788 # assume the values are valid
2789 try:
2790 return datetime(year, mon, day, h, m, s)
2791 except ValueError:
2792 pass
2794 # sanitize invalid values
2795 mday = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
2796 if mon < 1:
2797 mon = 1
2798 if mon > 12:
2799 mon = 12
2800 if day < 1:
2801 day = 1
2802 if day > mday[mon]:
2803 day = mday[mon]
2804 if h > 23:
2805 h = 23
2806 if m > 59:
2807 m = 59
2808 if s > 59:
2809 s = 59
2810 if mon == 2 and day == 29:
2811 try:
2812 return datetime(year, mon, day, h, m, s)
2813 except ValueError:
2814 day = 28
2815 return datetime(year, mon, day, h, m, s)
2817 def parse_dos_time(stamp):
2818 """Parse standard 32-bit DOS timestamp.
2820 sec, stamp = stamp & 0x1F, stamp >> 5
2821 mn, stamp = stamp & 0x3F, stamp >> 6
2822 hr, stamp = stamp & 0x1F, stamp >> 5
2823 day, stamp = stamp & 0x1F, stamp >> 5
2824 mon, stamp = stamp & 0x0F, stamp >> 4
2825 yr = (stamp & 0x7F) + 1980
2826 return (yr, mon, day, hr, mn, sec * 2)
2828 def custom_popen(cmd):
2829 """Disconnect cmd from parent fds, read only from stdout.
2831 # needed for py2exe
2832 creationflags = 0
2833 if sys.platform == "win32":
2834 creationflags = 0x08000000 # CREATE_NO_WINDOW
2836 # run command
2837 try:
2838 p = Popen(cmd, bufsize=0, stdout=PIPE, stdin=PIPE, stderr=STDOUT,
2839 creationflags=creationflags)
2840 except OSError as ex:
2841 if ex.errno == errno.ENOENT:
2842 raise RarCannotExec("Unrar not installed?")
2843 if ex.errno == errno.EACCES or ex.errno == errno.EPERM:
2844 raise RarCannotExec("Cannot execute unrar")
2845 raise
2846 return p
2848 def check_returncode(p, out):
2849 """Raise exception according to unrar exit code.
2851 code = p.returncode
2852 if code == 0:
2853 return
2855 errmap = tool_setup().get_errmap()
2856 if code > 0 and code < len(errmap):
2857 exc = errmap[code]
2858 elif code == 255:
2859 exc = RarUserBreak
2860 elif code < 0:
2861 exc = RarSignalExit
2862 else:
2863 exc = RarUnknownError
2865 # format message
2866 if out:
2867 msg = "%s [%d]: %s" % (exc.__doc__, p.returncode, out)
2868 else:
2869 msg = "%s [%d]" % (exc.__doc__, p.returncode)
2871 raise exc(msg)
2873 def hmac_sha256(key, data):
2874 """HMAC-SHA256"""
2875 return HMAC(key, data, sha256).digest()
2877 def membuf_tempfile(memfile):
2878 """Write in-memory file object to real file."""
2879 memfile.seek(0, 0)
2881 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR)
2882 tmpf = os.fdopen(tmpfd, "wb")
2884 try:
2885 while True:
2886 buf = memfile.read(BSIZE)
2887 if not buf:
2888 break
2889 tmpf.write(buf)
2890 tmpf.close()
2891 except:
2892 tmpf.close()
2893 os.unlink(tmpname)
2894 raise
2895 return tmpname
2897 class XTempFile(object):
2898 """Real file for archive.
2900 __slots__ = ("_tmpfile", "_filename")
2902 def __init__(self, rarfile):
2903 if is_filelike(rarfile):
2904 self._tmpfile = membuf_tempfile(rarfile)
2905 self._filename = self._tmpfile
2906 else:
2907 self._tmpfile = None
2908 self._filename = rarfile
2910 def __enter__(self):
2911 return self._filename
2913 def __exit__(self, exc_type, exc_value, tb):
2914 if self._tmpfile:
2915 try:
2916 os.unlink(self._tmpfile)
2917 except OSError:
2918 pass
2919 self._tmpfile = None
2922 # Find working command-line tool
2925 class ToolSetup:
2926 def __init__(self, setup):
2927 self.setup = setup
2929 def check(self):
2930 cmdline = self.get_cmdline("check_cmd", None)
2931 try:
2932 p = custom_popen(cmdline)
2933 out, _ = p.communicate()
2934 return p.returncode == 0
2935 except RarCannotExec:
2936 return False
2938 def open_cmdline(self, psw, rarfn, filefn=None):
2939 cmdline = self.get_cmdline("open_cmd", psw)
2940 cmdline.append(rarfn)
2941 if filefn:
2942 self.add_file_arg(cmdline, filefn)
2943 return cmdline
2945 def test_cmdline(self, psw, rarfn):
2946 cmdline = self.get_cmdline("test_cmd", psw)
2947 cmdline.append(rarfn)
2948 return cmdline
2950 def extract_cmdline(self, psw, rarfn, fnlist, path):
2951 cmdline = self.get_cmdline("extract_cmd", psw, nodash=True)
2952 dstdir = "DSTDIR" in cmdline
2953 if dstdir:
2954 if not path:
2955 path = "."
2956 cmdline[cmdline.index("DSTDIR", 1)] = path
2958 cmdline.append("--")
2960 cmdline.append(rarfn)
2961 for fn in fnlist:
2962 self.add_file_arg(cmdline, fn)
2964 if path and not dstdir:
2965 cmdline.append(path + os.sep)
2966 return cmdline
2968 def get_errmap(self):
2969 return self.setup["errmap"]
2971 def get_cmdline(self, key, psw, nodash=False):
2972 cmdline = list(self.setup[key])
2973 cmdline[0] = globals()[cmdline[0]]
2974 self.add_password_arg(cmdline, psw)
2975 if not nodash:
2976 cmdline.append("--")
2977 return cmdline
2979 def add_file_arg(self, cmdline, filename):
2980 filename = filename.replace('\\', '/')
2981 cmdline.append(filename)
2983 def add_password_arg(self, cmdline, psw):
2984 """Append password switch to commandline.
2986 if psw is not None:
2987 if not isinstance(psw, str):
2988 psw = psw.decode("utf8")
2989 args = self.setup["password"]
2990 if isinstance(args, str):
2991 cmdline.append(args + psw)
2992 else:
2993 cmdline.extend(args)
2994 cmdline.append(psw)
2995 else:
2996 cmdline.extend(self.setup["no_password"])
2999 UNRAR_CONFIG = {
3000 "open_cmd": ("UNRAR_TOOL", "p", "-inul"),
3001 "extract_cmd": ("UNRAR_TOOL", "x", "-y", "-idq"),
3002 "test_cmd": ("UNRAR_TOOL", "t", "-idq"),
3003 "check_cmd": ("UNRAR_TOOL", "-inul"),
3004 "password": "-p",
3005 "no_password": ("-p-",),
3006 # map return code to exception class, codes from rar.txt
3007 "errmap": [None,
3008 RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError, # 1..4
3009 RarWriteError, RarOpenError, RarUserError, RarMemoryError, # 5..8
3010 RarCreateError, RarNoFilesError, RarWrongPassword] # 9..11
3013 # Problems with unar RAR backend:
3014 # - Does not support RAR2 locked files [fails to read]
3015 # - Does not support RAR5 Blake2sp hash [reading works]
3016 UNAR_CONFIG = {
3017 "open_cmd": ("UNAR_TOOL", "-q", "-o", "-"),
3018 "extract_cmd": ("UNAR_TOOL", "-q", "-f", "-D", "-o", "DSTDIR"),
3019 "test_cmd": ("LSAR_TOOL", "-test"),
3020 "check_cmd": ("UNAR_TOOL", "-version"),
3021 "password": ("-p",),
3022 "no_password": ("-p", ""),
3023 "errmap": [None],
3026 # Problems with libarchive RAR backend:
3027 # - Does not support solid archives.
3028 # - Does not support password-protected archives.
3029 # - Does not support RARVM-based compression filters.
3030 BSDTAR_CONFIG = {
3031 "open_cmd": ("BSDTAR_TOOL", "-x", "--to-stdout", "-f"),
3032 "extract_cmd": ("BSDTAR_TOOL", "-x", "-C", "DSTDIR", "-f"),
3033 "test_cmd": ("BSDTAR_TOOL", "-t", "-f"),
3034 "check_cmd": ("BSDTAR_TOOL", "--version"),
3035 "password": None,
3036 "no_password": (),
3037 "errmap": [None],
3040 CURRENT_SETUP = None
3042 def tool_setup(unrar=True, unar=True, bsdtar=True, force=False):
3043 """Pick a tool, return cached ToolSetup.
3045 global CURRENT_SETUP
3046 if force:
3047 CURRENT_SETUP = None
3048 if CURRENT_SETUP is not None:
3049 return CURRENT_SETUP
3050 lst = []
3051 if unrar:
3052 lst.append(UNRAR_CONFIG)
3053 if unar:
3054 lst.append(UNAR_CONFIG)
3055 if bsdtar:
3056 lst.append(BSDTAR_CONFIG)
3058 for conf in lst:
3059 setup = ToolSetup(conf)
3060 if setup.check():
3061 CURRENT_SETUP = setup
3062 break
3063 if CURRENT_SETUP is None:
3064 raise RarCannotExec("Cannot find working tool")
3065 return CURRENT_SETUP