3 # Copyright (c) 2005-2020 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 """RAR archive reader.
19 This is Python module for Rar archive reading. The interface
20 is made as :mod:`zipfile`-like as possible.
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.
33 rf = rarfile.RarFile("myarchive.rar")
34 for f in rf.infolist():
35 print(f.filename, f.file_size)
36 if f.filename == "README":
39 Archive files can also be accessed via file-like object returned
40 by :meth:`RarFile.open`::
44 with rarfile.RarFile("archive.rar") as rf:
45 with rf.open("README") as f:
49 For decompression to work, either ``unrar`` or ``unar`` tool must be in PATH.
60 from binascii
import crc32
, hexlify
61 from datetime
import datetime
, timezone
62 from hashlib
import blake2s
, pbkdf2_hmac
, sha1
63 from pathlib
import Path
64 from struct
import Struct
, pack
, unpack
65 from subprocess
import DEVNULL
, PIPE
, STDOUT
, Popen
66 from tempfile
import mkstemp
70 # only needed for encrypted headers
73 from cryptography
.hazmat
.backends
import default_backend
74 from cryptography
.hazmat
.primitives
.ciphers
import (
75 Cipher
, algorithms
, modes
,
79 from Crypto
.Cipher
import AES
85 class AES_CBC_Decrypt
:
87 def __init__(self
, key
, iv
):
89 self
.decrypt
= AES
.new(key
, AES
.MODE_CBC
, iv
).decrypt
91 ciph
= Cipher(algorithms
.AES(key
), modes
.CBC(iv
), default_backend())
92 self
.decrypt
= ciph
.decryptor().update
97 # export only interesting items
98 __all__
= ["get_rar_version", "is_rarfile", "is_rarfile_sfx", "RarInfo", "RarFile", "RarExtFile"]
101 ## Module configuration. Can be tuned after importing.
104 #: executable for unrar tool
107 #: executable for unar tool
110 #: executable for bsdtar tool
111 BSDTAR_TOOL
= "bsdtar"
113 #: default fallback charset
114 DEFAULT_CHARSET
= "windows-1252"
116 #: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
117 TRY_ENCODINGS
= ("utf8", "utf-16le")
119 #: whether to speed up decompression by using tmp archive
122 #: limit the filesize for tmp archive usage
123 HACK_SIZE_LIMIT
= 20 * 1024 * 1024
125 #: set specific directory for mkstemp() used by hack dir usage
128 #: Separator for path name components. Always "/".
136 RAR_BLOCK_MARK
= 0x72 # r
137 RAR_BLOCK_MAIN
= 0x73 # s
138 RAR_BLOCK_FILE
= 0x74 # t
139 RAR_BLOCK_OLD_COMMENT
= 0x75 # u
140 RAR_BLOCK_OLD_EXTRA
= 0x76 # v
141 RAR_BLOCK_OLD_SUB
= 0x77 # w
142 RAR_BLOCK_OLD_RECOVERY
= 0x78 # x
143 RAR_BLOCK_OLD_AUTH
= 0x79 # y
144 RAR_BLOCK_SUB
= 0x7a # z
145 RAR_BLOCK_ENDARC
= 0x7b # {
147 # flags for RAR_BLOCK_MAIN
148 RAR_MAIN_VOLUME
= 0x0001
149 RAR_MAIN_COMMENT
= 0x0002
150 RAR_MAIN_LOCK
= 0x0004
151 RAR_MAIN_SOLID
= 0x0008
152 RAR_MAIN_NEWNUMBERING
= 0x0010
153 RAR_MAIN_AUTH
= 0x0020
154 RAR_MAIN_RECOVERY
= 0x0040
155 RAR_MAIN_PASSWORD
= 0x0080
156 RAR_MAIN_FIRSTVOLUME
= 0x0100
157 RAR_MAIN_ENCRYPTVER
= 0x0200
159 # flags for RAR_BLOCK_FILE
160 RAR_FILE_SPLIT_BEFORE
= 0x0001
161 RAR_FILE_SPLIT_AFTER
= 0x0002
162 RAR_FILE_PASSWORD
= 0x0004
163 RAR_FILE_COMMENT
= 0x0008
164 RAR_FILE_SOLID
= 0x0010
165 RAR_FILE_DICTMASK
= 0x00e0
166 RAR_FILE_DICT64
= 0x0000
167 RAR_FILE_DICT128
= 0x0020
168 RAR_FILE_DICT256
= 0x0040
169 RAR_FILE_DICT512
= 0x0060
170 RAR_FILE_DICT1024
= 0x0080
171 RAR_FILE_DICT2048
= 0x00a0
172 RAR_FILE_DICT4096
= 0x00c0
173 RAR_FILE_DIRECTORY
= 0x00e0
174 RAR_FILE_LARGE
= 0x0100
175 RAR_FILE_UNICODE
= 0x0200
176 RAR_FILE_SALT
= 0x0400
177 RAR_FILE_VERSION
= 0x0800
178 RAR_FILE_EXTTIME
= 0x1000
179 RAR_FILE_EXTFLAGS
= 0x2000
181 # flags for RAR_BLOCK_ENDARC
182 RAR_ENDARC_NEXT_VOLUME
= 0x0001
183 RAR_ENDARC_DATACRC
= 0x0002
184 RAR_ENDARC_REVSPACE
= 0x0004
185 RAR_ENDARC_VOLNR
= 0x0008
187 # flags common to all blocks
188 RAR_SKIP_IF_UNKNOWN
= 0x4000
189 RAR_LONG_BLOCK
= 0x8000
192 RAR_OS_MSDOS
= 0 #: MSDOS (only in RAR3)
193 RAR_OS_OS2
= 1 #: OS2 (only in RAR3)
194 RAR_OS_WIN32
= 2 #: Windows
195 RAR_OS_UNIX
= 3 #: UNIX
196 RAR_OS_MACOS
= 4 #: MacOS (only in RAR3)
197 RAR_OS_BEOS
= 5 #: BeOS (only in RAR3)
199 # Compression methods - "0".."5"
200 RAR_M0
= 0x30 #: No compression.
201 RAR_M1
= 0x31 #: Compression level `-m1` - Fastest compression.
202 RAR_M2
= 0x32 #: Compression level `-m2`.
203 RAR_M3
= 0x33 #: Compression level `-m3`.
204 RAR_M4
= 0x34 #: Compression level `-m4`.
205 RAR_M5
= 0x35 #: Compression level `-m5` - Maximum compression.
213 RAR5_BLOCK_SERVICE
= 3
214 RAR5_BLOCK_ENCRYPTION
= 4
215 RAR5_BLOCK_ENDARC
= 5
217 RAR5_BLOCK_FLAG_EXTRA_DATA
= 0x01
218 RAR5_BLOCK_FLAG_DATA_AREA
= 0x02
219 RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN
= 0x04
220 RAR5_BLOCK_FLAG_SPLIT_BEFORE
= 0x08
221 RAR5_BLOCK_FLAG_SPLIT_AFTER
= 0x10
222 RAR5_BLOCK_FLAG_DEPENDS_PREV
= 0x20
223 RAR5_BLOCK_FLAG_KEEP_WITH_PARENT
= 0x40
225 RAR5_MAIN_FLAG_ISVOL
= 0x01
226 RAR5_MAIN_FLAG_HAS_VOLNR
= 0x02
227 RAR5_MAIN_FLAG_SOLID
= 0x04
228 RAR5_MAIN_FLAG_RECOVERY
= 0x08
229 RAR5_MAIN_FLAG_LOCKED
= 0x10
231 RAR5_FILE_FLAG_ISDIR
= 0x01
232 RAR5_FILE_FLAG_HAS_MTIME
= 0x02
233 RAR5_FILE_FLAG_HAS_CRC32
= 0x04
234 RAR5_FILE_FLAG_UNKNOWN_SIZE
= 0x08
236 RAR5_COMPR_SOLID
= 0x40
238 RAR5_ENC_FLAG_HAS_CHECKVAL
= 0x01
240 RAR5_ENDARC_FLAG_NEXT_VOL
= 0x01
242 RAR5_XFILE_ENCRYPTION
= 1
245 RAR5_XFILE_VERSION
= 4
248 RAR5_XFILE_SERVICE
= 7
250 RAR5_XTIME_UNIXTIME
= 0x01
251 RAR5_XTIME_HAS_MTIME
= 0x02
252 RAR5_XTIME_HAS_CTIME
= 0x04
253 RAR5_XTIME_HAS_ATIME
= 0x08
254 RAR5_XTIME_UNIXTIME_NS
= 0x10
256 RAR5_XENC_CIPHER_AES256
= 0
258 RAR5_XENC_CHECKVAL
= 0x01
259 RAR5_XENC_TWEAKED
= 0x02
261 RAR5_XHASH_BLAKE2SP
= 0
263 RAR5_XREDIR_UNIX_SYMLINK
= 1
264 RAR5_XREDIR_WINDOWS_SYMLINK
= 2
265 RAR5_XREDIR_WINDOWS_JUNCTION
= 3
266 RAR5_XREDIR_HARD_LINK
= 4
267 RAR5_XREDIR_FILE_COPY
= 5
269 RAR5_XREDIR_ISDIR
= 0x01
271 RAR5_XOWNER_UNAME
= 0x01
272 RAR5_XOWNER_GNAME
= 0x02
273 RAR5_XOWNER_UID
= 0x04
274 RAR5_XOWNER_GID
= 0x08
279 DOS_MODE_ARCHIVE
= 0x20
281 DOS_MODE_SYSTEM
= 0x04
282 DOS_MODE_HIDDEN
= 0x02
283 DOS_MODE_READONLY
= 0x01
286 ## internal constants
289 RAR_ID
= b
"Rar!\x1a\x07\x00"
290 RAR5_ID
= b
"Rar!\x1a\x07\x01\x00"
292 WIN32
= sys
.platform
== "win32"
293 BSIZE
= 512 * 1024 if WIN32
else 64 * 1024
295 SFX_MAX_SIZE
= 2 * 1024 * 1024
299 _BAD_CHARS
= r
"""\x00-\x1F<>|"?*"""
300 RC_BAD_CHARS_UNIX
= re
.compile(r
"[%s]" % _BAD_CHARS
)
301 RC_BAD_CHARS_WIN32
= re
.compile(r
"[%s:^\\]" % _BAD_CHARS
)
304 def _find_sfx_header(xfile
):
307 steps
= (64, SFX_MAX_SIZE
)
309 with
XFile(xfile
) as fd
:
315 curdata
= buf
.getvalue()
318 pos
= curdata
.find(sig
, findpos
)
321 if curdata
[pos
:pos
+ len(RAR_ID
)] == RAR_ID
:
323 if curdata
[pos
:pos
+ len(RAR5_ID
)] == RAR5_ID
:
325 findpos
= pos
+ len(sig
)
334 def get_rar_version(xfile
):
335 """Check quickly whether file is rar archive.
337 with
XFile(xfile
) as fd
:
338 buf
= fd
.read(len(RAR5_ID
))
339 if buf
.startswith(RAR_ID
):
341 elif buf
.startswith(RAR5_ID
):
346 def is_rarfile(xfile
):
347 """Check quickly whether file is rar archive.
350 return get_rar_version(xfile
) > 0
352 # File not found or not accessible, ignore
356 def is_rarfile_sfx(xfile
):
357 """Check whether file is rar archive with support for SFX.
359 It will read 2M from file.
361 return _find_sfx_header(xfile
)[0] > 0
364 class Error(Exception):
365 """Base class for rarfile errors."""
368 class BadRarFile(Error
):
369 """Incorrect data in archive."""
372 class NotRarFile(Error
):
373 """The file is not RAR archive."""
376 class BadRarName(Error
):
377 """Cannot guess multipart name components."""
380 class NoRarEntry(Error
):
381 """File not found in RAR"""
384 class PasswordRequired(Error
):
385 """File requires password"""
388 class NeedFirstVolume(Error
):
389 """Need to start from first volume.
394 Volume number of current file or None if not known
396 def __init__(self
, msg
, volume
):
397 super().__init
__(msg
)
398 self
.current_volume
= volume
401 class NoCrypto(Error
):
402 """Cannot parse encrypted headers - no crypto available."""
405 class RarExecError(Error
):
406 """Problem reported by unrar/rar."""
409 class RarWarning(RarExecError
):
410 """Non-fatal error"""
413 class RarFatalError(RarExecError
):
417 class RarCRCError(RarExecError
):
418 """CRC error during unpacking"""
421 class RarLockedArchiveError(RarExecError
):
422 """Must not modify locked archive"""
425 class RarWriteError(RarExecError
):
429 class RarOpenError(RarExecError
):
433 class RarUserError(RarExecError
):
437 class RarMemoryError(RarExecError
):
441 class RarCreateError(RarExecError
):
445 class RarNoFilesError(RarExecError
):
446 """No files that match pattern were found"""
449 class RarUserBreak(RarExecError
):
453 class RarWrongPassword(RarExecError
):
454 """Incorrect password"""
457 class RarUnknownError(RarExecError
):
458 """Unknown exit code"""
461 class RarSignalExit(RarExecError
):
462 """Unrar exited with signal"""
465 class RarCannotExec(RarExecError
):
466 """Executable not found."""
469 class UnsupportedWarning(UserWarning):
470 """Archive uses feature that are unsupported by rarfile.
472 .. versionadded:: 4.0
477 r
"""An entry in rar archive.
479 Timestamps as :class:`~datetime.datetime` are without timezone in RAR3,
480 with UTC timezone in RAR5 archives.
485 File name with relative path.
486 Path separator is "/". Always unicode string.
489 File modification timestamp. As tuple of (year, month, day, hour, minute, second).
490 RAR5 allows archives where it is missing, it's None then.
493 Optional file comment field. Unicode string. (RAR3-only)
502 Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants.
505 Minimal Rar version needed for decompressing. As (major*10 + minor),
510 RAR5 does not have such field in archive, it's simply set to 50.
513 Host OS type, one of RAR_OS_* constants.
515 RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`,
516 :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`.
518 RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`.
521 File attributes. May be either dos-style or unix-style, depending on host_os.
524 File modification time. Same value as :attr:`date_time`
525 but as :class:`~datetime.datetime` object with extended precision.
528 Optional time field: creation time. As :class:`~datetime.datetime` object.
531 Optional time field: last access time. As :class:`~datetime.datetime` object.
534 Optional time field: archival time. As :class:`~datetime.datetime` object.
538 CRC-32 of uncompressed file, unsigned int.
543 Blake2SP hash over decompressed data. (RAR5-only)
546 Volume nr, starting from 0.
549 Volume file name, where file starts.
552 If not None, file is link of some sort. Contains tuple of (type, flags, target).
555 Type is one of constants:
557 :data:`RAR5_XREDIR_UNIX_SYMLINK`
559 :data:`RAR5_XREDIR_WINDOWS_SYMLINK`
561 :data:`RAR5_XREDIR_WINDOWS_JUNCTION`
563 :data:`RAR5_XREDIR_HARD_LINK`
565 :data:`RAR5_XREDIR_FILE_COPY`
566 Current file is copy of another archive entry.
568 Flags may contain bits:
570 :data:`RAR5_XREDIR_ISDIR`
571 Symlink points to directory.
574 # zipfile-compatible fields
583 # optional extended time fields, datetime() objects.
588 extract_version
= None
607 """Returns True if entry is a directory.
609 .. versionadded:: 4.0
613 def is_symlink(self
):
614 """Returns True if entry is a symlink.
616 .. versionadded:: 4.0
621 """Returns True if entry is a normal file.
623 .. versionadded:: 4.0
627 def needs_password(self
):
628 """Returns True if data is stored password-protected.
630 if self
.type == RAR_BLOCK_FILE
:
631 return (self
.flags
& RAR_FILE_PASSWORD
) > 0
635 """Returns True if entry is a directory.
643 """Parse RAR structure, provide access to files in archive.
646 #: File name, if available. Unicode string or None.
649 #: Archive comment. Unicode string or None.
652 def __init__(self
, file, mode
="r", charset
=None, info_callback
=None,
653 crc_check
=True, errors
="stop", part_only
=False):
654 """Open and parse a RAR archive.
659 archive file name or file-like object.
661 only "r" is supported.
663 fallback charset to use, if filenames are not already Unicode-enabled.
665 debug callback, gets to see all archive entries.
667 set to False to disable CRC checks
669 Either "stop" to quietly stop parsing on errors,
670 or "strict" to raise errors. Default is "stop".
672 If True, read only single file and allow it to be middle-part
673 of multi-volume archive.
675 if is_filelike(file):
676 self
.filename
= getattr(file, "name", None)
678 if isinstance(file, Path
):
683 self
._charset
= charset
or DEFAULT_CHARSET
684 self
._info
_callback
= info_callback
685 self
._crc
_check
= crc_check
686 self
._part
_only
= part_only
687 self
._password
= None
688 self
._file
_parser
= None
692 elif errors
== "strict":
695 raise ValueError("Invalid value for errors= parameter.")
698 raise NotImplementedError("RarFile supports only mode=r")
706 def __exit__(self
, typ
, value
, traceback
):
711 """Iterate over members."""
712 return iter(self
.infolist())
714 def setpassword(self
, pwd
):
715 """Sets the password to use when extracting.
718 if self
._file
_parser
:
719 if self
._file
_parser
.has_header_encryption():
720 self
._file
_parser
= None
721 if not self
._file
_parser
:
724 self
._file
_parser
.setpassword(self
._password
)
726 def needs_password(self
):
727 """Returns True if any archive entries require password for extraction.
729 return self
._file
_parser
.needs_password()
732 """Return list of filenames in archive.
734 return [f
.filename
for f
in self
.infolist()]
737 """Return RarInfo objects for all files/directories in archive.
739 return self
._file
_parser
.infolist()
741 def volumelist(self
):
742 """Returns filenames of archive volumes.
744 In case of single-volume archive, the list contains
745 just the name of main archive file.
747 return self
._file
_parser
.volumelist()
749 def getinfo(self
, name
):
750 """Return RarInfo for file.
752 return self
._file
_parser
.getinfo(name
)
754 def open(self
, name
, mode
="r", pwd
=None):
755 """Returns file-like object (:class:`RarExtFile`) from where the data can be read.
757 The object implements :class:`io.RawIOBase` interface, so it can
758 be further wrapped with :class:`io.BufferedReader`
759 and :class:`io.TextIOWrapper`.
761 On older Python where io module is not available, it implements
762 only .read(), .seek(), .tell() and .close() methods.
764 The object is seekable, although the seeking is fast only on
765 uncompressed files, on compressed files the seeking is implemented
766 by reading ahead and/or restarting the decompression.
771 file name or RarInfo instance.
775 password to use for extracting.
779 raise NotImplementedError("RarFile.open() supports only mode=r")
782 inf
= self
.getinfo(name
)
784 raise io
.UnsupportedOperation("Directory does not have any data: " + inf
.filename
)
787 if inf
.needs_password():
788 pwd
= pwd
or self
._password
790 raise PasswordRequired("File %s requires password" % inf
.filename
)
794 return self
._file
_parser
.open(inf
, pwd
)
796 def read(self
, name
, pwd
=None):
797 """Return uncompressed data for archive entry.
799 For longer files using :meth:`~RarFile.open` may be better idea.
804 filename or RarInfo instance
806 password to use for extracting.
809 with self
.open(name
, "r", pwd
) as f
:
813 """Release open resources."""
816 def printdir(self
, file=None):
817 """Print archive file list to stdout or given file.
821 for f
in self
.infolist():
822 print(f
.filename
, file=file)
824 def extract(self
, member
, path
=None, pwd
=None):
825 """Extract single file into current directory.
830 filename or :class:`RarInfo` instance
832 optional destination path
834 optional password to use
836 inf
= self
.getinfo(member
)
837 return self
._extract
_one
(inf
, path
, pwd
, True)
839 def extractall(self
, path
=None, members
=None, pwd
=None):
840 """Extract all files into current directory.
845 optional destination path
847 optional filename or :class:`RarInfo` instance list to extract
849 optional password to use
852 members
= self
.namelist()
857 inf
= self
.getinfo(m
)
858 dst
= self
._extract
_one
(inf
, path
, pwd
, not inf
.is_dir())
861 dirs
.append((dst
, inf
))
864 dirs
.sort(reverse
=True)
865 for dst
, inf
in dirs
:
866 self
._set
_attrs
(inf
, dst
)
868 def testrar(self
, pwd
=None):
869 """Read all files and test CRC.
871 for member
in self
.infolist():
873 with self
.open(member
, 'r', pwd
) as f
:
874 empty_read(f
, member
.file_size
, BSIZE
)
877 """Return error string if parsing failed or None if no problems.
879 if not self
._file
_parser
:
880 return "Not a RAR file"
881 return self
._file
_parser
.strerror()
888 """Run parser for file type
890 ver
, sfx_ofs
= _find_sfx_header(self
._rarfile
)
892 p3
= RAR3Parser(self
._rarfile
, self
._password
, self
._crc
_check
,
893 self
._charset
, self
._strict
, self
._info
_callback
,
894 sfx_ofs
, self
._part
_only
)
895 self
._file
_parser
= p3
# noqa
897 p5
= RAR5Parser(self
._rarfile
, self
._password
, self
._crc
_check
,
898 self
._charset
, self
._strict
, self
._info
_callback
,
899 sfx_ofs
, self
._part
_only
)
900 self
._file
_parser
= p5
# noqa
902 raise NotRarFile("Not a RAR file")
904 self
._file
_parser
.parse()
905 self
.comment
= self
._file
_parser
.comment
907 def _extract_one(self
, info
, path
, pwd
, set_attrs
):
908 fname
= sanitize_filename(
909 info
.filename
, os
.path
.sep
, WIN32
915 path
= os
.fspath(path
)
916 dstfn
= os
.path
.join(path
, fname
)
918 dirname
= os
.path
.dirname(dstfn
)
919 if dirname
and dirname
!= ".":
920 os
.makedirs(dirname
, exist_ok
=True)
923 return self
._make
_file
(info
, dstfn
, pwd
, set_attrs
)
925 return self
._make
_dir
(info
, dstfn
, pwd
, set_attrs
)
926 if info
.is_symlink():
927 return self
._make
_symlink
(info
, dstfn
, pwd
, set_attrs
)
930 def _create_helper(self
, name
, flags
, info
):
931 return os
.open(name
, flags
)
933 def _make_file(self
, info
, dstfn
, pwd
, set_attrs
):
934 def helper(name
, flags
):
935 return self
._create
_helper
(name
, flags
, info
)
936 with self
.open(info
, "r", pwd
) as src
:
937 with
open(dstfn
, "wb", opener
=helper
) as dst
:
938 shutil
.copyfileobj(src
, dst
)
940 self
._set
_attrs
(info
, dstfn
)
943 def _make_dir(self
, info
, dstfn
, pwd
, set_attrs
):
944 os
.makedirs(dstfn
, exist_ok
=True)
946 self
._set
_attrs
(info
, dstfn
)
949 def _make_symlink(self
, info
, dstfn
, pwd
, set_attrs
):
950 target_is_directory
= False
951 if info
.host_os
== RAR_OS_UNIX
:
952 link_name
= self
.read(info
, pwd
)
953 target_is_directory
= (info
.flags
& RAR_FILE_DIRECTORY
) == RAR_FILE_DIRECTORY
954 elif info
.file_redir
:
955 redir_type
, redir_flags
, link_name
= info
.file_redir
956 if redir_type
== RAR5_XREDIR_WINDOWS_JUNCTION
:
957 warnings
.warn(f
"Windows junction not supported - {info.filename}", UnsupportedWarning
)
959 target_is_directory
= (redir_type
& RAR5_XREDIR_ISDIR
) > 0
961 warnings
.warn(f
"Unsupported link type - {info.filename}", UnsupportedWarning
)
964 os
.symlink(link_name
, dstfn
, target_is_directory
=target_is_directory
)
967 def _set_attrs(self
, info
, dstfn
):
968 if info
.host_os
== RAR_OS_UNIX
:
969 os
.chmod(dstfn
, info
.mode
& 0o777)
970 elif info
.host_os
in (RAR_OS_WIN32
, RAR_OS_MSDOS
):
971 # only keep R/O attr, except for dirs on win32
972 if info
.mode
& DOS_MODE_READONLY
and (info
.is_file() or not WIN32
):
974 new_mode
= st
.st_mode
& ~
0o222
975 os
.chmod(dstfn
, new_mode
)
978 mtime_ns
= to_nsecs(info
.mtime
)
979 atime_ns
= to_nsecs(info
.atime
) if info
.atime
else mtime_ns
980 os
.utime(dstfn
, ns
=(atime_ns
, mtime_ns
))
984 # File format parsing
988 """Shared parser parts."""
991 _needs_password
= False
998 def __init__(self
, rarfile
, password
, crc_check
, charset
, strict
,
999 info_cb
, sfx_offset
, part_only
):
1000 self
._rarfile
= rarfile
1001 self
._password
= password
1002 self
._crc
_check
= crc_check
1003 self
._charset
= charset
1004 self
._strict
= strict
1005 self
._info
_callback
= info_cb
1006 self
._info
_list
= []
1009 self
._sfx
_offset
= sfx_offset
1010 self
._part
_only
= part_only
1012 def has_header_encryption(self
):
1013 """Returns True if headers are encrypted
1015 if self
._hdrenc
_main
:
1018 if self
._main
.flags
& RAR_MAIN_PASSWORD
:
1022 def setpassword(self
, pwd
):
1023 """Set cached password."""
1024 self
._password
= pwd
1026 def volumelist(self
):
1028 return self
._vol
_list
1030 def needs_password(self
):
1031 """Is password required"""
1032 return self
._needs
_password
1036 return self
._parse
_error
1039 """List of RarInfo records.
1041 return self
._info
_list
1043 def getinfo(self
, member
):
1044 """Return RarInfo for filename
1046 if isinstance(member
, RarInfo
):
1047 fname
= member
.filename
1048 elif isinstance(member
, Path
):
1053 if fname
.endswith("/"):
1054 fname
= fname
.rstrip("/")
1057 return self
._info
_map
[fname
]
1059 raise NoRarEntry("No such file: %s" % fname
) from None
1071 def _parse_real(self
):
1072 """Actually read file.
1074 fd
= XFile(self
._rarfile
)
1076 fd
.seek(self
._sfx
_offset
, 0)
1077 sig
= fd
.read(len(self
._expect
_sig
))
1078 if sig
!= self
._expect
_sig
:
1079 raise NotRarFile("Not a Rar archive")
1081 volume
= 0 # first vol (.rar) is 0
1084 volfile
= self
._rarfile
1085 self
._vol
_list
= [self
._rarfile
]
1086 raise_need_first_vol
= False
1089 h
= None # don"t read past ENDARC
1091 h
= self
._parse
_header
(fd
)
1093 if raise_need_first_vol
:
1094 # did not find ENDARC with VOLNR
1095 raise NeedFirstVolume("Need to start from first volume", None)
1096 if more_vols
and not self
._part
_only
:
1100 volfile
= self
._next
_volname
(volfile
)
1103 self
._set
_error
("Cannot open next volume: %s", volfile
)
1106 sig
= fd
.read(len(self
._expect
_sig
))
1107 if sig
!= self
._expect
_sig
:
1108 self
._set
_error
("Invalid volume sig: %s", volfile
)
1112 self
._vol
_list
.append(volfile
)
1117 h
.volume_file
= volfile
1119 if h
.type == RAR_BLOCK_MAIN
and not self
._main
:
1121 if volume
== 0 and (h
.flags
& RAR_MAIN_NEWNUMBERING
) and not self
._part
_only
:
1122 # RAR 2.x does not set FIRSTVOLUME,
1123 # so check it only if NEWNUMBERING is used
1124 if (h
.flags
& RAR_MAIN_FIRSTVOLUME
) == 0:
1125 if getattr(h
, "main_volume_number", None) is not None:
1126 # rar5 may have more info
1127 raise NeedFirstVolume(
1128 "Need to start from first volume (current: %r)"
1129 % (h
.main_volume_number
,),
1130 h
.main_volume_number
1132 # delay raise until we have volnr from ENDARC
1133 raise_need_first_vol
= True
1134 if h
.flags
& RAR_MAIN_PASSWORD
:
1135 self
._needs
_password
= True
1136 if not self
._password
:
1138 elif h
.type == RAR_BLOCK_ENDARC
:
1139 more_vols
= (h
.flags
& RAR_ENDARC_NEXT_VOLUME
) > 0
1141 if raise_need_first_vol
and (h
.flags
& RAR_ENDARC_VOLNR
) > 0:
1142 raise NeedFirstVolume(
1143 "Need to start from first volume (current: %r)"
1144 % (h
.endarc_volnr
,),
1147 elif h
.type == RAR_BLOCK_FILE
:
1148 # RAR 2.x does not write RAR_BLOCK_ENDARC
1149 if h
.flags
& RAR_FILE_SPLIT_AFTER
:
1151 # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
1152 if volume
== 0 and h
.flags
& RAR_FILE_SPLIT_BEFORE
:
1153 if not self
._part
_only
:
1154 raise_need_first_vol
= True
1156 if h
.needs_password():
1157 self
._needs
_password
= True
1160 self
.process_entry(fd
, h
)
1162 if self
._info
_callback
:
1163 self
._info
_callback
(h
)
1167 fd
.seek(h
.data_offset
+ h
.add_size
, 0)
1169 def process_entry(self
, fd
, item
):
1170 """Examine item, add into lookup cache."""
1171 raise NotImplementedError()
1173 def _decrypt_header(self
, fd
):
1174 raise NotImplementedError("_decrypt_header")
1176 def _parse_block_header(self
, fd
):
1177 raise NotImplementedError("_parse_block_header")
1179 def _open_hack(self
, inf
, pwd
):
1180 raise NotImplementedError("_open_hack")
1182 def _parse_header(self
, fd
):
1183 """Read single header
1186 # handle encrypted headers
1187 if (self
._main
and self
._main
.flags
& RAR_MAIN_PASSWORD
) or self
._hdrenc
_main
:
1188 if not self
._password
:
1190 fd
= self
._decrypt
_header
(fd
)
1192 # now read actual header
1193 return self
._parse
_block
_header
(fd
)
1194 except struct
.error
:
1195 self
._set
_error
("Broken header in RAR file")
1198 def _next_volname(self
, volfile
):
1199 """Given current vol name, construct next one
1201 if is_filelike(volfile
):
1202 raise IOError("Working on single FD")
1203 if self
._main
.flags
& RAR_MAIN_NEWNUMBERING
:
1204 return _next_newvol(volfile
)
1205 return _next_oldvol(volfile
)
1207 def _set_error(self
, msg
, *args
):
1210 self
._parse
_error
= msg
1212 raise BadRarFile(msg
)
1214 def open(self
, inf
, pwd
):
1215 """Return stream object for file data."""
1218 redir_type
, redir_flags
, redir_name
= inf
.file_redir
1219 # cannot leave to unrar as it expects copied file to exist
1220 if redir_type
in (RAR5_XREDIR_FILE_COPY
, RAR5_XREDIR_HARD_LINK
):
1221 inf
= self
.getinfo(redir_name
)
1223 raise BadRarFile("cannot find copied file")
1224 elif redir_type
in (
1225 RAR5_XREDIR_UNIX_SYMLINK
, RAR5_XREDIR_WINDOWS_SYMLINK
,
1226 RAR5_XREDIR_WINDOWS_JUNCTION
,
1228 return io
.BytesIO(redir_name
.encode("utf8"))
1229 if inf
.flags
& RAR_FILE_SPLIT_BEFORE
:
1230 raise NeedFirstVolume("Partial file, please start from first volume: " + inf
.filename
, None)
1232 # is temp write usable?
1236 elif self
._main
._must
_disable
_hack
():
1238 elif inf
._must
_disable
_hack
():
1240 elif is_filelike(self
._rarfile
):
1242 elif inf
.file_size
> HACK_SIZE_LIMIT
:
1244 elif not USE_EXTRACT_HACK
:
1248 if inf
.compress_type
== RAR_M0
and (inf
.flags
& RAR_FILE_PASSWORD
) == 0 and inf
.file_redir
is None:
1249 return self
._open
_clear
(inf
)
1251 return self
._open
_hack
(inf
, pwd
)
1252 elif is_filelike(self
._rarfile
):
1253 return self
._open
_unrar
_membuf
(self
._rarfile
, inf
, pwd
)
1255 return self
._open
_unrar
(self
._rarfile
, inf
, pwd
)
1257 def _open_clear(self
, inf
):
1258 return DirectReader(self
, inf
)
1260 def _open_hack_core(self
, inf
, pwd
, prefix
, suffix
):
1262 size
= inf
.compress_size
+ inf
.header_size
1263 rf
= XFile(inf
.volume_file
, 0)
1264 rf
.seek(inf
.header_offset
)
1266 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
1267 tmpf
= os
.fdopen(tmpfd
, "wb")
1273 buf
= rf
.read(BSIZE
)
1277 raise BadRarFile("read failed: " + inf
.filename
)
1283 except BaseException
:
1289 return self
._open
_unrar
(tmpname
, inf
, pwd
, tmpname
)
1291 def _open_unrar_membuf(self
, memfile
, inf
, pwd
):
1292 """Write in-memory archive to temp file, needed for solid archives.
1294 tmpname
= membuf_tempfile(memfile
)
1295 return self
._open
_unrar
(tmpname
, inf
, pwd
, tmpname
, force_file
=True)
1297 def _open_unrar(self
, rarfile
, inf
, pwd
=None, tmpfile
=None, force_file
=False):
1298 """Extract using unrar
1300 setup
= tool_setup()
1302 # not giving filename avoids encoding related problems
1304 if not tmpfile
or force_file
:
1307 # read from unrar pipe
1308 cmd
= setup
.open_cmdline(pwd
, rarfile
, fn
)
1309 return PipeReader(self
, inf
, cmd
, tmpfile
)
1316 class Rar3Info(RarInfo
):
1317 """RAR3 specific fields."""
1318 extract_version
= 15
1323 header_offset
= None
1329 # make sure some rar5 fields are always present
1331 blake2sp_hash
= None
1333 endarc_datacrc
= None
1336 def _must_disable_hack(self
):
1337 if self
.type == RAR_BLOCK_FILE
:
1338 if self
.flags
& RAR_FILE_PASSWORD
:
1340 elif self
.flags
& (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER
):
1342 elif self
.type == RAR_BLOCK_MAIN
:
1343 if self
.flags
& (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD
):
1348 """Returns True if entry is a directory."""
1349 if self
.type == RAR_BLOCK_FILE
and not self
.is_symlink():
1350 return (self
.flags
& RAR_FILE_DIRECTORY
) == RAR_FILE_DIRECTORY
1353 def is_symlink(self
):
1354 """Returns True if entry is a symlink."""
1356 self
.type == RAR_BLOCK_FILE
and
1357 self
.host_os
== RAR_OS_UNIX
and
1358 self
.mode
& 0xF000 == 0xA000
1362 """Returns True if entry is a normal file."""
1364 self
.type == RAR_BLOCK_FILE
and
1365 not (self
.is_dir() or self
.is_symlink())
1369 class RAR3Parser(CommonParser
):
1370 """Parse RAR3 file format.
1372 _expect_sig
= RAR_ID
1373 _last_aes_key
= (None, None, None) # (salt, key, iv)
1375 def _decrypt_header(self
, fd
):
1376 if not _have_crypto
:
1377 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1379 if self
._last
_aes
_key
[0] == salt
:
1380 key
, iv
= self
._last
_aes
_key
[1:]
1382 key
, iv
= rar3_s2k(self
._password
, salt
)
1383 self
._last
_aes
_key
= (salt
, key
, iv
)
1384 return HeaderDecrypt(fd
, key
, iv
)
1386 def _parse_block_header(self
, fd
):
1387 """Parse common block header
1390 h
.header_offset
= fd
.tell()
1392 # read and parse base header
1393 buf
= fd
.read(S_BLK_HDR
.size
)
1396 if len(buf
) < S_BLK_HDR
.size
:
1397 self
._set
_error
("Unexpected EOF when reading header")
1399 t
= S_BLK_HDR
.unpack_from(buf
)
1400 h
.header_crc
, h
.type, h
.flags
, h
.header_size
= t
1403 if h
.header_size
> S_BLK_HDR
.size
:
1404 hdata
= buf
+ fd
.read(h
.header_size
- S_BLK_HDR
.size
)
1407 h
.data_offset
= fd
.tell()
1410 if len(hdata
) != h
.header_size
:
1411 self
._set
_error
("Unexpected EOF when reading header")
1414 pos
= S_BLK_HDR
.size
1416 # block has data assiciated with it?
1417 if h
.flags
& RAR_LONG_BLOCK
:
1418 h
.add_size
, pos
= load_le32(hdata
, pos
)
1422 # parse interesting ones, decide header boundaries for crc
1423 if h
.type == RAR_BLOCK_MARK
:
1425 elif h
.type == RAR_BLOCK_MAIN
:
1427 if h
.flags
& RAR_MAIN_ENCRYPTVER
:
1430 if h
.flags
& RAR_MAIN_COMMENT
:
1431 self
._parse
_subblocks
(h
, hdata
, pos
)
1432 elif h
.type == RAR_BLOCK_FILE
:
1433 pos
= self
._parse
_file
_header
(h
, hdata
, pos
- 4)
1435 if h
.flags
& RAR_FILE_COMMENT
:
1436 pos
= self
._parse
_subblocks
(h
, hdata
, pos
)
1437 elif h
.type == RAR_BLOCK_SUB
:
1438 pos
= self
._parse
_file
_header
(h
, hdata
, pos
- 4)
1439 crc_pos
= h
.header_size
1440 elif h
.type == RAR_BLOCK_OLD_AUTH
:
1443 elif h
.type == RAR_BLOCK_OLD_EXTRA
:
1446 elif h
.type == RAR_BLOCK_ENDARC
:
1447 if h
.flags
& RAR_ENDARC_DATACRC
:
1448 h
.endarc_datacrc
, pos
= load_le32(hdata
, pos
)
1449 if h
.flags
& RAR_ENDARC_VOLNR
:
1450 h
.endarc_volnr
= S_SHORT
.unpack_from(hdata
, pos
)[0]
1452 crc_pos
= h
.header_size
1454 crc_pos
= h
.header_size
1457 if h
.type == RAR_BLOCK_OLD_SUB
:
1458 crcdat
= hdata
[2:] + fd
.read(h
.add_size
)
1460 crcdat
= hdata
[2:crc_pos
]
1462 calc_crc
= crc32(crcdat
) & 0xFFFF
1464 # return good header
1465 if h
.header_crc
== calc_crc
:
1468 # header parsing failed.
1469 self
._set
_error
("Header CRC error (%02x): exp=%x got=%x (xlen = %d)",
1470 h
.type, h
.header_crc
, calc_crc
, len(crcdat
))
1472 # instead panicing, send eof
1475 def _parse_file_header(self
, h
, hdata
, pos
):
1476 """Read file-specific header
1478 fld
= S_FILE_HDR
.unpack_from(hdata
, pos
)
1479 pos
+= S_FILE_HDR
.size
1481 h
.compress_size
= fld
[0]
1482 h
.file_size
= fld
[1]
1485 h
.date_time
= parse_dos_time(fld
[4])
1486 h
.mtime
= to_datetime(h
.date_time
)
1487 h
.extract_version
= fld
[5]
1488 h
.compress_type
= fld
[6]
1489 h
._name
_size
= name_size
= fld
[7]
1492 h
._md
_class
= CRC32Context
1493 h
._md
_expect
= h
.CRC
1495 if h
.flags
& RAR_FILE_LARGE
:
1496 h1
, pos
= load_le32(hdata
, pos
)
1497 h2
, pos
= load_le32(hdata
, pos
)
1498 h
.compress_size |
= h1
<< 32
1499 h
.file_size |
= h2
<< 32
1500 h
.add_size
= h
.compress_size
1502 name
, pos
= load_bytes(hdata
, name_size
, pos
)
1503 if h
.flags
& RAR_FILE_UNICODE
and b
"\0" in name
:
1504 # stored in custom encoding
1505 nul
= name
.find(b
"\0")
1506 h
.orig_filename
= name
[:nul
]
1507 u
= UnicodeFilename(h
.orig_filename
, name
[nul
+ 1:])
1508 h
.filename
= u
.decode()
1510 # if parsing failed fall back to simple name
1512 h
.filename
= self
._decode
(h
.orig_filename
)
1513 elif h
.flags
& RAR_FILE_UNICODE
:
1515 h
.orig_filename
= name
1516 h
.filename
= name
.decode("utf8", "replace")
1518 # stored in random encoding
1519 h
.orig_filename
= name
1520 h
.filename
= self
._decode
(name
)
1522 # change separator, set dir suffix
1523 h
.filename
= h
.filename
.replace("\\", "/").rstrip("/")
1525 h
.filename
= h
.filename
+ "/"
1527 if h
.flags
& RAR_FILE_SALT
:
1528 h
.salt
, pos
= load_bytes(hdata
, 8, pos
)
1532 # optional extended time stamps
1533 if h
.flags
& RAR_FILE_EXTTIME
:
1534 pos
= _parse_ext_time(h
, hdata
, pos
)
1536 h
.mtime
= h
.atime
= h
.ctime
= h
.arctime
= None
1540 def _parse_subblocks(self
, h
, hdata
, pos
):
1541 """Find old-style comment subblock
1543 while pos
< len(hdata
):
1544 # ordinary block header
1545 t
= S_BLK_HDR
.unpack_from(hdata
, pos
)
1546 ___scrc
, stype
, sflags
, slen
= t
1547 pos_next
= pos
+ slen
1548 pos
+= S_BLK_HDR
.size
1554 # followed by block-specific header
1555 if stype
== RAR_BLOCK_OLD_COMMENT
and pos
+ S_COMMENT_HDR
.size
<= pos_next
:
1556 declen
, ver
, meth
, crc
= S_COMMENT_HDR
.unpack_from(hdata
, pos
)
1557 pos
+= S_COMMENT_HDR
.size
1558 data
= hdata
[pos
: pos_next
]
1559 cmt
= rar3_decompress(ver
, meth
, data
, declen
, sflags
,
1560 crc
, self
._password
)
1561 if not self
._crc
_check
or (crc32(cmt
) & 0xFFFF == crc
):
1562 h
.comment
= self
._decode
_comment
(cmt
)
1567 def _read_comment_v3(self
, inf
, pwd
=None):
1570 with
XFile(inf
.volume_file
) as rf
:
1571 rf
.seek(inf
.data_offset
)
1572 data
= rf
.read(inf
.compress_size
)
1575 cmt
= rar3_decompress(inf
.extract_version
, inf
.compress_type
, data
,
1576 inf
.file_size
, inf
.flags
, inf
.CRC
, pwd
, inf
.salt
)
1584 return self
._decode
_comment
(cmt
)
1586 def _decode(self
, val
):
1587 for c
in TRY_ENCODINGS
:
1589 return val
.decode(c
)
1590 except UnicodeError:
1592 return val
.decode(self
._charset
, "replace")
1594 def _decode_comment(self
, val
):
1595 return self
._decode
(val
)
1597 def process_entry(self
, fd
, item
):
1598 if item
.type == RAR_BLOCK_FILE
:
1599 # use only first part
1600 if item
.flags
& RAR_FILE_VERSION
:
1601 pass # skip old versions
1602 elif (item
.flags
& RAR_FILE_SPLIT_BEFORE
) == 0:
1603 self
._info
_map
[item
.filename
.rstrip("/")] = item
1604 self
._info
_list
.append(item
)
1605 elif len(self
._info
_list
) > 0:
1606 # final crc is in last block
1607 old
= self
._info
_list
[-1]
1609 old
._md
_expect
= item
._md
_expect
1610 old
.compress_size
+= item
.compress_size
1612 # parse new-style comment
1613 if item
.type == RAR_BLOCK_SUB
and item
.filename
== "CMT":
1614 if item
.flags
& (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER
):
1616 elif item
.flags
& RAR_FILE_SOLID
:
1618 cmt
= self
._read
_comment
_v
3(item
, self
._password
)
1619 if len(self
._info
_list
) > 0:
1620 old
= self
._info
_list
[-1]
1624 cmt
= self
._read
_comment
_v
3(item
, self
._password
)
1627 if item
.type == RAR_BLOCK_MAIN
:
1628 if item
.flags
& RAR_MAIN_COMMENT
:
1629 self
.comment
= item
.comment
1630 if item
.flags
& RAR_MAIN_PASSWORD
:
1631 self
._needs
_password
= True
1633 # put file compressed data into temporary .rar archive, and run
1634 # unrar on that, thus avoiding unrar going over whole archive
1635 def _open_hack(self
, inf
, pwd
):
1636 # create main header: crc, type, flags, size, res1, res2
1637 prefix
= RAR_ID
+ S_BLK_HDR
.pack(0x90CF, 0x73, 0, 13) + b
"\0" * (2 + 4)
1638 return self
._open
_hack
_core
(inf
, pwd
, prefix
, b
"")
1645 class Rar5Info(RarInfo
):
1646 """Shared fields for RAR5 records.
1648 extract_version
= 50
1651 header_offset
= None
1658 block_extra_size
= 0
1661 volume_number
= None
1665 def _must_disable_hack(self
):
1669 class Rar5BaseFile(Rar5Info
):
1670 """Shared sturct for file & service record.
1674 file_encryption
= (0, 0, 0, b
"", b
"", b
"")
1675 file_compress_flags
= None
1679 blake2sp_hash
= None
1681 def _must_disable_hack(self
):
1682 if self
.flags
& RAR_FILE_PASSWORD
:
1684 if self
.block_flags
& (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER
):
1686 if self
.file_compress_flags
& RAR5_COMPR_SOLID
:
1693 class Rar5FileInfo(Rar5BaseFile
):
1694 """RAR5 file record.
1696 type = RAR_BLOCK_FILE
1698 def is_symlink(self
):
1699 """Returns True if entry is a symlink."""
1700 # pylint: disable=unsubscriptable-object
1702 self
.file_redir
is not None and
1703 self
.file_redir
[0] in (
1704 RAR5_XREDIR_UNIX_SYMLINK
,
1705 RAR5_XREDIR_WINDOWS_SYMLINK
,
1706 RAR5_XREDIR_WINDOWS_JUNCTION
,
1711 """Returns True if entry is a normal file."""
1712 return not (self
.is_dir() or self
.is_symlink())
1715 """Returns True if entry is a directory."""
1716 if not self
.file_redir
:
1717 if self
.file_flags
& RAR5_FILE_FLAG_ISDIR
:
1722 class Rar5ServiceInfo(Rar5BaseFile
):
1723 """RAR5 service record.
1725 type = RAR_BLOCK_SUB
1728 class Rar5MainInfo(Rar5Info
):
1729 """RAR5 archive main record.
1731 type = RAR_BLOCK_MAIN
1733 main_volume_number
= None
1735 def _must_disable_hack(self
):
1736 if self
.main_flags
& RAR5_MAIN_FLAG_SOLID
:
1741 class Rar5EncryptionInfo(Rar5Info
):
1742 """RAR5 archive header encryption record.
1744 type = RAR5_BLOCK_ENCRYPTION
1745 encryption_algo
= None
1746 encryption_flags
= None
1747 encryption_kdf_count
= None
1748 encryption_salt
= None
1749 encryption_check_value
= None
1751 def needs_password(self
):
1755 class Rar5EndArcInfo(Rar5Info
):
1756 """RAR5 end of archive record.
1758 type = RAR_BLOCK_ENDARC
1762 class RAR5Parser(CommonParser
):
1763 """Parse RAR5 format.
1765 _expect_sig
= RAR5_ID
1768 # AES encrypted headers
1769 _last_aes256_key
= (-1, None, None) # (kdf_count, salt, key)
1771 def _gen_key(self
, kdf_count
, salt
):
1772 if self
._last
_aes
256_key
[:2] == (kdf_count
, salt
):
1773 return self
._last
_aes
256_key
[2]
1775 raise BadRarFile("Too large kdf_count")
1776 pwd
= self
._password
1777 if isinstance(pwd
, str):
1778 pwd
= pwd
.encode("utf8")
1779 key
= pbkdf2_hmac("sha256", pwd
, salt
, 1 << kdf_count
)
1780 self
._last
_aes
256_key
= (kdf_count
, salt
, key
)
1783 def _decrypt_header(self
, fd
):
1784 if not _have_crypto
:
1785 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1786 h
= self
._hdrenc
_main
1787 key
= self
._gen
_key
(h
.encryption_kdf_count
, h
.encryption_salt
)
1789 return HeaderDecrypt(fd
, key
, iv
)
1791 def _parse_block_header(self
, fd
):
1792 """Parse common block header
1794 header_offset
= fd
.tell()
1797 start_bytes
= fd
.read(preload
)
1798 if len(start_bytes
) < preload
:
1799 self
._set
_error
("Unexpected EOF when reading header")
1801 while start_bytes
[-1] & 0x80:
1804 self
._set
_error
("Unexpected EOF when reading header")
1807 header_crc
, pos
= load_le32(start_bytes
, 0)
1808 hdrlen
, pos
= load_vint(start_bytes
, pos
)
1809 if hdrlen
> 2 * 1024 * 1024:
1811 header_size
= pos
+ hdrlen
1813 # read full header, check for EOF
1814 hdata
= start_bytes
+ fd
.read(header_size
- len(start_bytes
))
1815 if len(hdata
) != header_size
:
1816 self
._set
_error
("Unexpected EOF when reading header")
1818 data_offset
= fd
.tell()
1820 calc_crc
= crc32(memoryview(hdata
)[4:])
1821 if header_crc
!= calc_crc
:
1822 # header parsing failed.
1823 self
._set
_error
("Header CRC error: exp=%x got=%x (xlen = %d)",
1824 header_crc
, calc_crc
, len(hdata
))
1827 block_type
, pos
= load_vint(hdata
, pos
)
1829 if block_type
== RAR5_BLOCK_MAIN
:
1830 h
, pos
= self
._parse
_block
_common
(Rar5MainInfo(), hdata
)
1831 h
= self
._parse
_main
_block
(h
, hdata
, pos
)
1832 elif block_type
== RAR5_BLOCK_FILE
:
1833 h
, pos
= self
._parse
_block
_common
(Rar5FileInfo(), hdata
)
1834 h
= self
._parse
_file
_block
(h
, hdata
, pos
)
1835 elif block_type
== RAR5_BLOCK_SERVICE
:
1836 h
, pos
= self
._parse
_block
_common
(Rar5ServiceInfo(), hdata
)
1837 h
= self
._parse
_file
_block
(h
, hdata
, pos
)
1838 elif block_type
== RAR5_BLOCK_ENCRYPTION
:
1839 h
, pos
= self
._parse
_block
_common
(Rar5EncryptionInfo(), hdata
)
1840 h
= self
._parse
_encryption
_block
(h
, hdata
, pos
)
1841 elif block_type
== RAR5_BLOCK_ENDARC
:
1842 h
, pos
= self
._parse
_block
_common
(Rar5EndArcInfo(), hdata
)
1843 h
= self
._parse
_endarc
_block
(h
, hdata
, pos
)
1847 h
.header_offset
= header_offset
1848 h
.data_offset
= data_offset
1851 def _parse_block_common(self
, h
, hdata
):
1852 h
.header_crc
, pos
= load_le32(hdata
, 0)
1853 hdrlen
, pos
= load_vint(hdata
, pos
)
1854 h
.header_size
= hdrlen
+ pos
1855 h
.block_type
, pos
= load_vint(hdata
, pos
)
1856 h
.block_flags
, pos
= load_vint(hdata
, pos
)
1858 if h
.block_flags
& RAR5_BLOCK_FLAG_EXTRA_DATA
:
1859 h
.block_extra_size
, pos
= load_vint(hdata
, pos
)
1860 if h
.block_flags
& RAR5_BLOCK_FLAG_DATA_AREA
:
1861 h
.add_size
, pos
= load_vint(hdata
, pos
)
1863 h
.compress_size
= h
.add_size
1865 if h
.block_flags
& RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN
:
1866 h
.flags |
= RAR_SKIP_IF_UNKNOWN
1867 if h
.block_flags
& RAR5_BLOCK_FLAG_DATA_AREA
:
1868 h
.flags |
= RAR_LONG_BLOCK
1871 def _parse_main_block(self
, h
, hdata
, pos
):
1872 h
.main_flags
, pos
= load_vint(hdata
, pos
)
1873 if h
.main_flags
& RAR5_MAIN_FLAG_HAS_VOLNR
:
1874 h
.main_volume_number
, pos
= load_vint(hdata
, pos
)
1876 h
.flags |
= RAR_MAIN_NEWNUMBERING
1877 if h
.main_flags
& RAR5_MAIN_FLAG_SOLID
:
1878 h
.flags |
= RAR_MAIN_SOLID
1879 if h
.main_flags
& RAR5_MAIN_FLAG_ISVOL
:
1880 h
.flags |
= RAR_MAIN_VOLUME
1881 if h
.main_flags
& RAR5_MAIN_FLAG_RECOVERY
:
1882 h
.flags |
= RAR_MAIN_RECOVERY
1883 if self
._hdrenc
_main
:
1884 h
.flags |
= RAR_MAIN_PASSWORD
1885 if h
.main_flags
& RAR5_MAIN_FLAG_HAS_VOLNR
== 0:
1886 h
.flags |
= RAR_MAIN_FIRSTVOLUME
1890 def _parse_file_block(self
, h
, hdata
, pos
):
1891 h
.file_flags
, pos
= load_vint(hdata
, pos
)
1892 h
.file_size
, pos
= load_vint(hdata
, pos
)
1893 h
.mode
, pos
= load_vint(hdata
, pos
)
1895 if h
.file_flags
& RAR5_FILE_FLAG_HAS_MTIME
:
1896 h
.mtime
, pos
= load_unixtime(hdata
, pos
)
1897 h
.date_time
= h
.mtime
.timetuple()[:6]
1898 if h
.file_flags
& RAR5_FILE_FLAG_HAS_CRC32
:
1899 h
.CRC
, pos
= load_le32(hdata
, pos
)
1900 h
._md
_class
= CRC32Context
1901 h
._md
_expect
= h
.CRC
1903 h
.file_compress_flags
, pos
= load_vint(hdata
, pos
)
1904 h
.file_host_os
, pos
= load_vint(hdata
, pos
)
1905 h
.orig_filename
, pos
= load_vstr(hdata
, pos
)
1906 h
.filename
= h
.orig_filename
.decode("utf8", "replace").rstrip("/")
1908 # use compatible values
1909 if h
.file_host_os
== RAR5_OS_WINDOWS
:
1910 h
.host_os
= RAR_OS_WIN32
1912 h
.host_os
= RAR_OS_UNIX
1913 h
.compress_type
= RAR_M0
+ ((h
.file_compress_flags
>> 7) & 7)
1915 if h
.block_extra_size
:
1916 # allow 1 byte of garbage
1917 while pos
< len(hdata
) - 1:
1918 xsize
, pos
= load_vint(hdata
, pos
)
1919 xdata
, pos
= load_bytes(hdata
, xsize
, pos
)
1920 self
._process
_file
_extra
(h
, xdata
)
1922 if h
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_BEFORE
:
1923 h
.flags |
= RAR_FILE_SPLIT_BEFORE
1924 if h
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_AFTER
:
1925 h
.flags |
= RAR_FILE_SPLIT_AFTER
1926 if h
.file_flags
& RAR5_FILE_FLAG_ISDIR
:
1927 h
.flags |
= RAR_FILE_DIRECTORY
1928 if h
.file_compress_flags
& RAR5_COMPR_SOLID
:
1929 h
.flags |
= RAR_FILE_SOLID
1932 h
.filename
= h
.filename
+ "/"
1935 def _parse_endarc_block(self
, h
, hdata
, pos
):
1936 h
.endarc_flags
, pos
= load_vint(hdata
, pos
)
1937 if h
.endarc_flags
& RAR5_ENDARC_FLAG_NEXT_VOL
:
1938 h
.flags |
= RAR_ENDARC_NEXT_VOLUME
1941 def _parse_encryption_block(self
, h
, hdata
, pos
):
1942 h
.encryption_algo
, pos
= load_vint(hdata
, pos
)
1943 h
.encryption_flags
, pos
= load_vint(hdata
, pos
)
1944 h
.encryption_kdf_count
, pos
= load_byte(hdata
, pos
)
1945 h
.encryption_salt
, pos
= load_bytes(hdata
, 16, pos
)
1946 if h
.encryption_flags
& RAR5_ENC_FLAG_HAS_CHECKVAL
:
1947 h
.encryption_check_value
= load_bytes(hdata
, 12, pos
)
1948 if h
.encryption_algo
!= RAR5_XENC_CIPHER_AES256
:
1949 raise BadRarFile("Unsupported header encryption cipher")
1950 self
._hdrenc
_main
= h
1953 def _process_file_extra(self
, h
, xdata
):
1954 xtype
, pos
= load_vint(xdata
, 0)
1955 if xtype
== RAR5_XFILE_TIME
:
1956 self
._parse
_file
_xtime
(h
, xdata
, pos
)
1957 elif xtype
== RAR5_XFILE_ENCRYPTION
:
1958 self
._parse
_file
_encryption
(h
, xdata
, pos
)
1959 elif xtype
== RAR5_XFILE_HASH
:
1960 self
._parse
_file
_hash
(h
, xdata
, pos
)
1961 elif xtype
== RAR5_XFILE_VERSION
:
1962 self
._parse
_file
_version
(h
, xdata
, pos
)
1963 elif xtype
== RAR5_XFILE_REDIR
:
1964 self
._parse
_file
_redir
(h
, xdata
, pos
)
1965 elif xtype
== RAR5_XFILE_OWNER
:
1966 self
._parse
_file
_owner
(h
, xdata
, pos
)
1967 elif xtype
== RAR5_XFILE_SERVICE
:
1972 # extra block for file time record
1973 def _parse_file_xtime(self
, h
, xdata
, pos
):
1974 tflags
, pos
= load_vint(xdata
, pos
)
1976 ldr
= load_windowstime
1977 if tflags
& RAR5_XTIME_UNIXTIME
:
1980 if tflags
& RAR5_XTIME_HAS_MTIME
:
1981 h
.mtime
, pos
= ldr(xdata
, pos
)
1982 h
.date_time
= h
.mtime
.timetuple()[:6]
1983 if tflags
& RAR5_XTIME_HAS_CTIME
:
1984 h
.ctime
, pos
= ldr(xdata
, pos
)
1985 if tflags
& RAR5_XTIME_HAS_ATIME
:
1986 h
.atime
, pos
= ldr(xdata
, pos
)
1988 if tflags
& RAR5_XTIME_UNIXTIME_NS
:
1989 if tflags
& RAR5_XTIME_HAS_MTIME
:
1990 nsec
, pos
= load_le32(xdata
, pos
)
1991 h
.mtime
= to_nsdatetime(h
.mtime
, nsec
)
1992 if tflags
& RAR5_XTIME_HAS_CTIME
:
1993 nsec
, pos
= load_le32(xdata
, pos
)
1994 h
.ctime
= to_nsdatetime(h
.ctime
, nsec
)
1995 if tflags
& RAR5_XTIME_HAS_ATIME
:
1996 nsec
, pos
= load_le32(xdata
, pos
)
1997 h
.atime
= to_nsdatetime(h
.atime
, nsec
)
1999 # just remember encryption info
2000 def _parse_file_encryption(self
, h
, xdata
, pos
):
2001 algo
, pos
= load_vint(xdata
, pos
)
2002 flags
, pos
= load_vint(xdata
, pos
)
2003 kdf_count
, pos
= load_byte(xdata
, pos
)
2004 salt
, pos
= load_bytes(xdata
, 16, pos
)
2005 iv
, pos
= load_bytes(xdata
, 16, pos
)
2007 if flags
& RAR5_XENC_CHECKVAL
:
2008 checkval
, pos
= load_bytes(xdata
, 12, pos
)
2009 if flags
& RAR5_XENC_TWEAKED
:
2011 h
._md
_class
= NoHashContext
2013 h
.file_encryption
= (algo
, flags
, kdf_count
, salt
, iv
, checkval
)
2014 h
.flags |
= RAR_FILE_PASSWORD
2016 def _parse_file_hash(self
, h
, xdata
, pos
):
2017 hash_type
, pos
= load_vint(xdata
, pos
)
2018 if hash_type
== RAR5_XHASH_BLAKE2SP
:
2019 h
.blake2sp_hash
, pos
= load_bytes(xdata
, 32, pos
)
2020 if (h
.file_encryption
[1] & RAR5_XENC_TWEAKED
) == 0:
2021 h
._md
_class
= Blake2SP
2022 h
._md
_expect
= h
.blake2sp_hash
2024 def _parse_file_version(self
, h
, xdata
, pos
):
2025 flags
, pos
= load_vint(xdata
, pos
)
2026 version
, pos
= load_vint(xdata
, pos
)
2027 h
.file_version
= (flags
, version
)
2029 def _parse_file_redir(self
, h
, xdata
, pos
):
2030 redir_type
, pos
= load_vint(xdata
, pos
)
2031 redir_flags
, pos
= load_vint(xdata
, pos
)
2032 redir_name
, pos
= load_vstr(xdata
, pos
)
2033 redir_name
= redir_name
.decode("utf8", "replace")
2034 h
.file_redir
= (redir_type
, redir_flags
, redir_name
)
2036 def _parse_file_owner(self
, h
, xdata
, pos
):
2037 user_name
= group_name
= user_id
= group_id
= None
2039 flags
, pos
= load_vint(xdata
, pos
)
2040 if flags
& RAR5_XOWNER_UNAME
:
2041 user_name
, pos
= load_vstr(xdata
, pos
)
2042 if flags
& RAR5_XOWNER_GNAME
:
2043 group_name
, pos
= load_vstr(xdata
, pos
)
2044 if flags
& RAR5_XOWNER_UID
:
2045 user_id
, pos
= load_vint(xdata
, pos
)
2046 if flags
& RAR5_XOWNER_GID
:
2047 group_id
, pos
= load_vint(xdata
, pos
)
2049 h
.file_owner
= (user_name
, group_name
, user_id
, group_id
)
2051 def process_entry(self
, fd
, item
):
2052 if item
.block_type
== RAR5_BLOCK_FILE
:
2053 if item
.file_version
:
2054 pass # skip old versions
2055 elif (item
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_BEFORE
) == 0:
2056 # use only first part
2057 self
._info
_map
[item
.filename
.rstrip("/")] = item
2058 self
._info
_list
.append(item
)
2059 elif len(self
._info
_list
) > 0:
2060 # final crc is in last block
2061 old
= self
._info
_list
[-1]
2063 old
._md
_expect
= item
._md
_expect
2064 old
.blake2sp_hash
= item
.blake2sp_hash
2065 old
.compress_size
+= item
.compress_size
2066 elif item
.block_type
== RAR5_BLOCK_SERVICE
:
2067 if item
.filename
== "CMT":
2068 self
._load
_comment
(fd
, item
)
2070 def _load_comment(self
, fd
, item
):
2071 if item
.block_flags
& (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER
):
2073 if item
.compress_type
!= RAR_M0
:
2076 if item
.flags
& RAR_FILE_PASSWORD
:
2077 algo
, ___flags
, kdf_count
, salt
, iv
, ___checkval
= item
.file_encryption
2078 if algo
!= RAR5_XENC_CIPHER_AES256
:
2080 key
= self
._gen
_key
(kdf_count
, salt
)
2081 f
= HeaderDecrypt(fd
, key
, iv
)
2082 cmt
= f
.read(item
.file_size
)
2085 with self
._open
_clear
(item
) as cmtstream
:
2086 cmt
= cmtstream
.read()
2088 # rar bug? - appends zero to comment
2089 cmt
= cmt
.split(b
"\0", 1)[0]
2090 self
.comment
= cmt
.decode("utf8")
2093 def _open_hack(self
, inf
, pwd
):
2094 # len, type, blk_flags, flags
2095 main_hdr
= b
"\x03\x01\x00\x00"
2096 endarc_hdr
= b
"\x03\x05\x00\x00"
2097 main_hdr
= S_LONG
.pack(crc32(main_hdr
)) + main_hdr
2098 endarc_hdr
= S_LONG
.pack(crc32(endarc_hdr
)) + endarc_hdr
2099 return self
._open
_hack
_core
(inf
, pwd
, RAR5_ID
+ main_hdr
, endarc_hdr
)
2106 class UnicodeFilename
:
2107 """Handle RAR3 unicode filename decompression.
2109 def __init__(self
, name
, encdata
):
2110 self
.std_name
= bytearray(name
)
2111 self
.encdata
= bytearray(encdata
)
2112 self
.pos
= self
.encpos
= 0
2113 self
.buf
= bytearray()
2117 """Copy encoded byte."""
2119 c
= self
.encdata
[self
.encpos
]
2127 """Copy byte from 8-bit representation."""
2129 return self
.std_name
[self
.pos
]
2134 def put(self
, lo
, hi
):
2135 """Copy 16-bit value to result."""
2141 """Decompress compressed UTF16 value."""
2142 hi
= self
.enc_byte()
2144 while self
.encpos
< len(self
.encdata
):
2146 flags
= self
.enc_byte()
2149 t
= (flags
>> flagbits
) & 3
2151 self
.put(self
.enc_byte(), 0)
2153 self
.put(self
.enc_byte(), hi
)
2155 self
.put(self
.enc_byte(), self
.enc_byte())
2160 for _
in range((n
& 0x7f) + 2):
2161 lo
= (self
.std_byte() + c
) & 0xFF
2164 for _
in range(n
+ 2):
2165 self
.put(self
.std_byte(), 0)
2166 return self
.buf
.decode("utf-16le", "replace")
2169 class RarExtFile(io
.RawIOBase
):
2170 """Base class for file-like object that :meth:`RarFile.open` returns.
2172 Provides public methods and common crc checking.
2175 - no short reads - .read() and .readinfo() read as much as requested.
2176 - no internal buffer, use io.BufferedReader for that.
2178 name
= None #: Filename of the archive entry
2187 def _open_extfile(self
, parser
, inf
):
2188 self
.name
= inf
.filename
2189 self
._parser
= parser
2194 md_class
= self
._inf
._md
_class
or NoHashContext
2195 self
._md
_context
= md_class()
2197 self
._remain
= self
._inf
.file_size
2199 def read(self
, n
=-1):
2200 """Read all or specified amount of data from archive entry."""
2203 if n
is None or n
< 0:
2205 elif n
> self
._remain
:
2214 data
= self
._read
(n
)
2218 self
._md
_context
.update(data
)
2219 self
._remain
-= len(data
)
2221 data
= b
"".join(buf
)
2223 raise BadRarFile("Failed the read enough data: req=%d got=%d" % (orig
, len(data
)))
2226 if not data
or self
._remain
== 0:
2232 """Check final CRC."""
2233 final
= self
._md
_context
.digest()
2234 exp
= self
._inf
._md
_expect
2239 if self
._returncode
:
2240 check_returncode(self
._returncode
, "", tool_setup().get_errmap())
2241 if self
._remain
!= 0:
2242 raise BadRarFile("Failed the read enough data")
2244 raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % (
2245 self
._inf
.filename
, exp
, final
))
2247 def _read(self
, cnt
):
2248 """Actual read that gets sanitized cnt."""
2249 raise NotImplementedError("_read")
2252 """Close open resources."""
2261 """Hook delete to make sure tempfile is removed."""
2264 def readinto(self
, buf
):
2265 """Zero-copy read directly into buffer.
2269 raise NotImplementedError("readinto")
2272 """Return current reading position in uncompressed data."""
2273 return self
._inf
.file_size
- self
._remain
2275 def seek(self
, offset
, whence
=0):
2278 On uncompressed files, the seeking works by actual
2279 seeks so it's fast. On compresses files its slow
2280 - forward seeking happends by reading ahead,
2281 backwards by re-opening and decompressing from the start.
2284 # disable crc check when seeking
2285 self
._md
_context
= NoHashContext()
2287 fsize
= self
._inf
.file_size
2288 cur_ofs
= self
.tell()
2290 if whence
== 0: # seek from beginning of file
2292 elif whence
== 1: # seek from current position
2293 new_ofs
= cur_ofs
+ offset
2294 elif whence
== 2: # seek from end of file
2295 new_ofs
= fsize
+ offset
2297 raise ValueError("Invalid value for whence")
2302 elif new_ofs
> fsize
:
2305 # do the actual seek
2306 if new_ofs
>= cur_ofs
:
2307 self
._skip
(new_ofs
- cur_ofs
)
2310 self
._open
_extfile
(self
._parser
, self
._inf
)
2314 def _skip(self
, cnt
):
2315 """Read and discard data"""
2316 empty_read(self
, cnt
, BSIZE
)
2325 Writing is not supported.
2332 Seeking is supported, although it's slow on compressed files.
2337 """Read all remaining data"""
2338 # avoid RawIOBase default impl
2342 class PipeReader(RarExtFile
):
2343 """Read data from pipe, handle tempfile cleanup."""
2345 def __init__(self
, parser
, inf
, cmd
, tempfile
=None):
2349 self
._tempfile
= tempfile
2350 self
._open
_extfile
(parser
, inf
)
2352 def _close_proc(self
):
2355 for f
in (self
._proc
.stdout
, self
._proc
.stderr
, self
._proc
.stdin
):
2359 self
._returncode
= self
._proc
.returncode
2362 def _open_extfile(self
, parser
, inf
):
2363 super()._open
_extfile
(parser
, inf
)
2368 # launch new process
2369 self
._returncode
= 0
2370 self
._proc
= custom_popen(self
._cmd
)
2371 self
._fd
= self
._proc
.stdout
2373 def _read(self
, cnt
):
2374 """Read from pipe."""
2376 # normal read is usually enough
2377 data
= self
._fd
.read(cnt
)
2378 if len(data
) == cnt
or not data
:
2381 # short read, try looping
2385 data
= self
._fd
.read(cnt
)
2390 return b
"".join(buf
)
2393 """Close open resources."""
2400 os
.unlink(self
._tempfile
)
2403 self
._tempfile
= None
2405 def readinto(self
, buf
):
2406 """Zero-copy read directly into buffer."""
2408 if cnt
> self
._remain
:
2410 vbuf
= memoryview(buf
)
2413 res
= self
._fd
.readinto(vbuf
[got
: cnt
])
2416 self
._md
_context
.update(vbuf
[got
: got
+ res
])
2422 class DirectReader(RarExtFile
):
2423 """Read uncompressed data directly from archive.
2429 def __init__(self
, parser
, inf
):
2431 self
._open
_extfile
(parser
, inf
)
2433 def _open_extfile(self
, parser
, inf
):
2434 super()._open
_extfile
(parser
, inf
)
2436 self
._volfile
= self
._inf
.volume_file
2437 self
._fd
= XFile(self
._volfile
, 0)
2438 self
._fd
.seek(self
._inf
.header_offset
, 0)
2439 self
._cur
= self
._parser
._parse
_header
(self
._fd
)
2440 self
._cur
_avail
= self
._cur
.add_size
2442 def _skip(self
, cnt
):
2443 """RAR Seek, skipping through rar files to get to correct position
2448 if self
._cur
_avail
== 0:
2449 if not self
._open
_next
():
2452 # fd is in read pos, do the read
2453 if cnt
> self
._cur
_avail
:
2454 cnt
-= self
._cur
_avail
2455 self
._remain
-= self
._cur
_avail
2458 self
._fd
.seek(cnt
, 1)
2459 self
._cur
_avail
-= cnt
2463 def _read(self
, cnt
):
2464 """Read from potentially multi-volume archive."""
2469 if self
._cur
_avail
== 0:
2470 if not self
._open
_next
():
2473 # fd is in read pos, do the read
2474 if cnt
> self
._cur
_avail
:
2475 data
= self
._fd
.read(self
._cur
_avail
)
2477 data
= self
._fd
.read(cnt
)
2483 self
._cur
_avail
-= len(data
)
2488 return b
"".join(buf
)
2490 def _open_next(self
):
2491 """Proceed to next volume."""
2493 # is the file split over archives?
2494 if (self
._cur
.flags
& RAR_FILE_SPLIT_AFTER
) == 0:
2502 self
._volfile
= self
._parser
._next
_volname
(self
._volfile
)
2503 fd
= open(self
._volfile
, "rb", 0)
2505 sig
= fd
.read(len(self
._parser
._expect
_sig
))
2506 if sig
!= self
._parser
._expect
_sig
:
2507 raise BadRarFile("Invalid signature")
2509 # loop until first file header
2511 cur
= self
._parser
._parse
_header
(fd
)
2513 raise BadRarFile("Unexpected EOF")
2514 if cur
.type in (RAR_BLOCK_MARK
, RAR_BLOCK_MAIN
):
2516 fd
.seek(cur
.add_size
, 1)
2518 if cur
.orig_filename
!= self
._inf
.orig_filename
:
2519 raise BadRarFile("Did not found file entry")
2521 self
._cur
_avail
= cur
.add_size
2524 def readinto(self
, buf
):
2525 """Zero-copy read directly into buffer."""
2527 vbuf
= memoryview(buf
)
2528 while got
< len(buf
):
2530 if self
._cur
_avail
== 0:
2531 if not self
._open
_next
():
2534 # length for next read
2535 cnt
= len(buf
) - got
2536 if cnt
> self
._cur
_avail
:
2537 cnt
= self
._cur
_avail
2539 # read into temp view
2540 res
= self
._fd
.readinto(vbuf
[got
: got
+ cnt
])
2543 self
._md
_context
.update(vbuf
[got
: got
+ res
])
2544 self
._cur
_avail
-= res
2550 class HeaderDecrypt
:
2551 """File-like object that decrypts from another file"""
2552 def __init__(self
, f
, key
, iv
):
2554 self
.ciph
= AES_CBC_Decrypt(key
, iv
)
2558 """Current file pos - works only on block boundaries."""
2559 return self
.f
.tell()
2561 def read(self
, cnt
=None):
2562 """Read and decrypt."""
2564 raise BadRarFile("Bad count to header decrypt - wrong password?")
2567 if cnt
<= len(self
.buf
):
2568 res
= self
.buf
[:cnt
]
2569 self
.buf
= self
.buf
[cnt
:]
2578 enc
= self
.f
.read(blklen
)
2579 if len(enc
) < blklen
:
2581 dec
= self
.ciph
.decrypt(enc
)
2587 self
.buf
= dec
[cnt
:]
2594 """Input may be filename or file object.
2596 __slots__
= ("_fd", "_need_close")
2598 def __init__(self
, xfile
, bufsize
=1024):
2599 if is_filelike(xfile
):
2600 self
._need
_close
= False
2604 self
._need
_close
= True
2605 self
._fd
= open(xfile
, "rb", bufsize
)
2607 def read(self
, n
=None):
2608 """Read from file."""
2609 return self
._fd
.read(n
)
2612 """Return file pos."""
2613 return self
._fd
.tell()
2615 def seek(self
, ofs
, whence
=0):
2616 """Move file pos."""
2617 return self
._fd
.seek(ofs
, whence
)
2619 def readinto(self
, buf
):
2620 """Read into buffer."""
2621 return self
._fd
.readinto(buf
)
2624 """Close file object."""
2625 if self
._need
_close
:
2628 def __enter__(self
):
2631 def __exit__(self
, typ
, val
, tb
):
2635 class NoHashContext
:
2636 """No-op hash function."""
2637 def __init__(self
, data
=None):
2639 def update(self
, data
):
2643 def hexdigest(self
):
2644 """Hexadecimal digest."""
2648 """Hash context that uses CRC32."""
2649 __slots__
= ["_crc"]
2651 def __init__(self
, data
=None):
2656 def update(self
, data
):
2658 self
._crc
= crc32(data
, self
._crc
)
2664 def hexdigest(self
):
2665 """Hexadecimal digest."""
2666 return "%08x" % self
.digest()
2670 """Blake2sp hash context.
2672 __slots__
= ["_thread", "_buf", "_cur", "_digest"]
2677 def __init__(self
, data
=None):
2683 for i
in range(self
.parallelism
):
2684 ctx
= self
._blake
2s
(i
, 0, i
== (self
.parallelism
- 1))
2685 self
._thread
.append(ctx
)
2690 def _blake2s(self
, ofs
, depth
, is_last
):
2691 return blake2s(node_offset
=ofs
, node_depth
=depth
, last_node
=is_last
,
2692 depth
=2, inner_size
=32, fanout
=self
.parallelism
)
2694 def _add_block(self
, blk
):
2695 self
._thread
[self
._cur
].update(blk
)
2696 self
._cur
= (self
._cur
+ 1) % self
.parallelism
2698 def update(self
, data
):
2701 view
= memoryview(data
)
2702 bs
= self
.block_size
2704 need
= bs
- len(self
._buf
)
2705 if len(view
) < need
:
2706 self
._buf
+= view
.tobytes()
2708 self
._add
_block
(self
._buf
+ view
[:need
].tobytes())
2710 while len(view
) >= bs
:
2711 self
._add
_block
(view
[:bs
])
2713 self
._buf
= view
.tobytes()
2716 """Return final digest value.
2718 if self
._digest
is None:
2720 self
._add
_block
(self
._buf
)
2722 ctx
= self
._blake
2s
(0, 1, True)
2723 for t
in self
._thread
:
2724 ctx
.update(t
.digest())
2725 self
._digest
= ctx
.digest()
2728 def hexdigest(self
):
2729 """Hexadecimal digest."""
2730 return hexlify(self
.digest()).decode("ascii")
2734 """Emulate buggy SHA1 from RAR3.
2739 _BLK_BE
= struct
.Struct(b
">16L")
2740 _BLK_LE
= struct
.Struct(b
"<16L")
2742 __slots__
= ("_nbytes", "_md", "_rarbug")
2744 def __init__(self
, data
=b
"", rarbug
=False):
2747 self
._rarbug
= rarbug
2750 def update(self
, data
):
2751 """Process more data."""
2752 self
._md
.update(data
)
2753 bufpos
= self
._nbytes
& 63
2754 self
._nbytes
+= len(data
)
2756 if self
._rarbug
and len(data
) > 64:
2757 dpos
= self
.block_size
- bufpos
2758 while dpos
+ self
.block_size
<= len(data
):
2759 self
._corrupt
(data
, dpos
)
2760 dpos
+= self
.block_size
2763 """Return final state."""
2764 return self
._md
.digest()
2766 def hexdigest(self
):
2767 """Return final state as hex string."""
2768 return self
._md
.hexdigest()
2770 def _corrupt(self
, data
, dpos
):
2771 """Corruption from SHA1 core."""
2772 ws
= list(self
._BLK
_BE
.unpack_from(data
, dpos
))
2773 for t
in range(16, 80):
2774 tmp
= ws
[(t
- 3) & 15] ^ ws
[(t
- 8) & 15] ^ ws
[(t
- 14) & 15] ^ ws
[(t
- 16) & 15]
2775 ws
[t
& 15] = ((tmp
<< 1) |
(tmp
>> (32 - 1))) & 0xFFFFFFFF
2776 self
._BLK
_LE
.pack_into(data
, dpos
, *ws
)
2780 ## Utility functions
2783 S_LONG
= Struct("<L")
2784 S_SHORT
= Struct("<H")
2785 S_BYTE
= Struct("<B")
2787 S_BLK_HDR
= Struct("<HBHH")
2788 S_FILE_HDR
= Struct("<LLBLLBBHL")
2789 S_COMMENT_HDR
= Struct("<HBBH")
2792 def load_vint(buf
, pos
):
2793 """Load RAR5 variable-size int."""
2794 limit
= min(pos
+ 11, len(buf
))
2798 res
+= ((b
& 0x7F) << ofs
)
2803 raise BadRarFile("cannot load vint")
2806 def load_byte(buf
, pos
):
2807 """Load single byte"""
2810 raise BadRarFile("cannot load byte")
2811 return S_BYTE
.unpack_from(buf
, pos
)[0], end
2814 def load_le32(buf
, pos
):
2815 """Load little-endian 32-bit integer"""
2818 raise BadRarFile("cannot load le32")
2819 return S_LONG
.unpack_from(buf
, pos
)[0], end
2822 def load_bytes(buf
, num
, pos
):
2823 """Load sequence of bytes"""
2826 raise BadRarFile("cannot load bytes")
2827 return buf
[pos
: end
], end
2830 def load_vstr(buf
, pos
):
2831 """Load bytes prefixed by vint length"""
2832 slen
, pos
= load_vint(buf
, pos
)
2833 return load_bytes(buf
, slen
, pos
)
2836 def load_dostime(buf
, pos
):
2837 """Load LE32 dos timestamp"""
2838 stamp
, pos
= load_le32(buf
, pos
)
2839 tup
= parse_dos_time(stamp
)
2840 return to_datetime(tup
), pos
2843 def load_unixtime(buf
, pos
):
2844 """Load LE32 unix timestamp"""
2845 secs
, pos
= load_le32(buf
, pos
)
2846 dt
= datetime
.fromtimestamp(secs
, timezone
.utc
)
2850 def load_windowstime(buf
, pos
):
2851 """Load LE64 windows timestamp"""
2852 # unix epoch (1970) in seconds from windows epoch (1601)
2853 unix_epoch
= 11644473600
2854 val1
, pos
= load_le32(buf
, pos
)
2855 val2
, pos
= load_le32(buf
, pos
)
2856 secs
, n1secs
= divmod((val2
<< 32) | val1
, 10000000)
2857 dt
= datetime
.fromtimestamp(secs
- unix_epoch
, timezone
.utc
)
2858 dt
= to_nsdatetime(dt
, n1secs
* 100)
2866 _rc_num
= re
.compile('^[0-9]+$')
2869 def _next_newvol(volfile
):
2870 """New-style next volume
2872 name
, ext
= os
.path
.splitext(volfile
)
2873 if ext
.lower() in ("", ".exe", ".sfx"):
2874 volfile
= name
+ ".rar"
2875 i
= len(volfile
) - 1
2877 if "0" <= volfile
[i
] <= "9":
2878 return _inc_volname(volfile
, i
, False)
2879 if volfile
[i
] in ("/", os
.sep
):
2882 raise BadRarName("Cannot construct volume name: " + volfile
)
2886 def _next_oldvol(volfile
):
2887 """Old-style next volume
2889 name
, ext
= os
.path
.splitext(volfile
)
2890 if ext
.lower() in ("", ".exe", ".sfx"):
2893 if _rc_num
.match(sfx
):
2894 ext
= _inc_volname(ext
, len(ext
) - 1, True)
2897 ext
= ext
[:2] + "00"
2901 def _inc_volname(volfile
, i
, inc_chars
):
2902 """increase digits with carry, otherwise just increment char
2911 elif "0" <= fn
[i
] < "9" or inc_chars
:
2912 fn
[i
] = chr(ord(fn
[i
]) + 1)
2915 fn
.insert(i
+ 1, "1")
2920 def _parse_ext_time(h
, data
, pos
):
2921 """Parse all RAR3 extended time fields
2923 # flags and rest of data can be missing
2925 if pos
+ 2 <= len(data
):
2926 flags
= S_SHORT
.unpack_from(data
, pos
)[0]
2929 mtime
, pos
= _parse_xtime(flags
>> 3 * 4, data
, pos
, h
.mtime
)
2930 h
.ctime
, pos
= _parse_xtime(flags
>> 2 * 4, data
, pos
)
2931 h
.atime
, pos
= _parse_xtime(flags
>> 1 * 4, data
, pos
)
2932 h
.arctime
, pos
= _parse_xtime(flags
>> 0 * 4, data
, pos
)
2935 h
.date_time
= mtime
.timetuple()[:6]
2939 def _parse_xtime(flag
, data
, pos
, basetime
=None):
2940 """Parse one RAR3 extended time field
2945 basetime
, pos
= load_dostime(data
, pos
)
2947 # load second fractions of 100ns units
2950 for _
in range(cnt
):
2951 b
, pos
= load_byte(data
, pos
)
2952 rem
= (b
<< 16) |
(rem
>> 8)
2954 # dostime has room for 30 seconds only, correct if needed
2955 if flag
& 4 and basetime
.second
< 59:
2956 basetime
= basetime
.replace(second
=basetime
.second
+ 1)
2958 res
= to_nsdatetime(basetime
, rem
* 100)
2962 def is_filelike(obj
):
2963 """Filename or file object?
2965 if isinstance(obj
, (bytes
, str, Path
)):
2968 for a
in ("read", "tell", "seek"):
2969 res
= res
and hasattr(obj
, a
)
2971 raise ValueError("Invalid object passed as file")
2975 def rar3_s2k(pwd
, salt
):
2976 """String-to-key hash for RAR3.
2978 if not isinstance(pwd
, str):
2979 pwd
= pwd
.decode("utf8")
2980 seed
= bytearray(pwd
.encode("utf-16le") + salt
)
2981 h
= Rar3Sha1(rarbug
=True)
2984 for j
in range(0x4000):
2985 cnt
= S_LONG
.pack(i
* 0x4000 + j
)
2989 iv
+= h
.digest()[19:20]
2990 key_be
= h
.digest()[:16]
2991 key_le
= pack("<LLLL", *unpack(">LLLL", key_be
))
2995 def rar3_decompress(vers
, meth
, data
, declen
=0, flags
=0, crc
=0, pwd
=None, salt
=None):
2996 """Decompress blob of compressed data.
2998 Used for data with non-standard header - eg. comments.
3000 # already uncompressed?
3001 if meth
== RAR_M0
and (flags
& RAR_FILE_PASSWORD
) == 0:
3004 # take only necessary flags
3005 flags
= flags
& (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK
)
3006 flags |
= RAR_LONG_BLOCK
3010 date
= ((2010 - 1980) << 25) + (12 << 21) + (31 << 16)
3011 mode
= DOS_MODE_ARCHIVE
3012 fhdr
= S_FILE_HDR
.pack(len(data
), declen
, RAR_OS_MSDOS
, crc
,
3013 date
, vers
, meth
, len(fname
), mode
)
3019 hlen
= S_BLK_HDR
.size
+ len(fhdr
)
3020 hdr
= S_BLK_HDR
.pack(0, RAR_BLOCK_FILE
, flags
, hlen
) + fhdr
3021 hcrc
= crc32(hdr
[2:]) & 0xFFFF
3022 hdr
= S_BLK_HDR
.pack(hcrc
, RAR_BLOCK_FILE
, flags
, hlen
) + fhdr
3024 # archive main header
3025 mh
= S_BLK_HDR
.pack(0x90CF, RAR_BLOCK_MAIN
, 0, 13) + b
"\0" * (2 + 4)
3027 # decompress via temp rar
3028 setup
= tool_setup()
3029 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
3030 tmpf
= os
.fdopen(tmpfd
, "wb")
3032 tmpf
.write(RAR_ID
+ mh
+ hdr
+ data
)
3035 curpwd
= (flags
& RAR_FILE_PASSWORD
) and pwd
or None
3036 cmd
= setup
.open_cmdline(curpwd
, tmpname
)
3037 p
= custom_popen(cmd
)
3038 return p
.communicate()[0]
3044 def sanitize_filename(fname
, pathsep
, is_win32
):
3045 """Simulate unrar sanitization.
3048 if len(fname
) > 1 and fname
[1] == ":":
3050 rc
= RC_BAD_CHARS_WIN32
3052 rc
= RC_BAD_CHARS_UNIX
3053 if rc
.search(fname
):
3054 fname
= rc
.sub("_", fname
)
3057 for seg
in fname
.split("/"):
3058 if seg
in ("", ".", ".."):
3060 if is_win32
and seg
[-1] in (" ", "."):
3061 seg
= seg
[:-1] + "_"
3063 return pathsep
.join(parts
)
3066 def empty_read(src
, size
, blklen
):
3067 """Read and drop fixed amount of data.
3071 res
= src
.read(blklen
)
3073 res
= src
.read(size
)
3075 raise BadRarFile("cannot load data")
3080 """Convert 6-part time tuple into datetime object.
3083 year
, mon
, day
, h
, m
, s
= t
3085 # assume the values are valid
3087 return datetime(year
, mon
, day
, h
, m
, s
)
3091 # sanitize invalid values
3092 mday
= (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
3093 mon
= max(1, min(mon
, 12))
3094 day
= max(1, min(day
, mday
[mon
]))
3098 return datetime(year
, mon
, day
, h
, m
, s
)
3101 def parse_dos_time(stamp
):
3102 """Parse standard 32-bit DOS timestamp.
3104 sec
, stamp
= stamp
& 0x1F, stamp
>> 5
3105 mn
, stamp
= stamp
& 0x3F, stamp
>> 6
3106 hr
, stamp
= stamp
& 0x1F, stamp
>> 5
3107 day
, stamp
= stamp
& 0x1F, stamp
>> 5
3108 mon
, stamp
= stamp
& 0x0F, stamp
>> 4
3109 yr
= (stamp
& 0x7F) + 1980
3110 return (yr
, mon
, day
, hr
, mn
, sec
* 2)
3113 # pylint: disable=arguments-differ,signature-differs
3114 class nsdatetime(datetime
):
3115 """Datetime that carries nanoseconds.
3117 Arithmetic not supported, will lose nanoseconds.
3119 .. versionadded:: 4.0
3121 __slots__
= ("nanosecond",)
3122 nanosecond
: int #: Number of nanoseconds, 0 <= nanosecond < 999999999
3124 def __new__(cls
, year
, month
=None, day
=None, hour
=0, minute
=0, second
=0,
3125 microsecond
=0, tzinfo
=None, *, fold
=0, nanosecond
=0):
3126 usec
, mod
= divmod(nanosecond
, 1000) if nanosecond
else (microsecond
, 0)
3128 return datetime(year
, month
, day
, hour
, minute
, second
, usec
, tzinfo
, fold
=fold
)
3129 self
= super().__new
__(cls
, year
, month
, day
, hour
, minute
, second
, usec
, tzinfo
, fold
=fold
)
3130 self
.nanosecond
= nanosecond
3133 def isoformat(self
, sep
="T", timespec
="auto"):
3134 """Formats with nanosecond precision by default.
3136 if timespec
== "auto":
3137 pre
, post
= super().isoformat(sep
, "microseconds").split(".", 1)
3138 return f
"{pre}.{self.nanosecond:09d}{post[6:]}"
3139 return super().isoformat(sep
, timespec
)
3141 def astimezone(self
, tz
=None):
3142 """Convert to new timezone.
3144 tmp
= super().astimezone(tz
)
3145 return self
.__class
__(tmp
.year
, tmp
.month
, tmp
.day
, tmp
.hour
, tmp
.minute
, tmp
.second
,
3146 nanosecond
=self
.nanosecond
, tzinfo
=tmp
.tzinfo
, fold
=tmp
.fold
)
3148 def replace(self
, year
=None, month
=None, day
=None, hour
=None, minute
=None, second
=None,
3149 microsecond
=None, tzinfo
=None, *, fold
=None, nanosecond
=None):
3150 """Return new timestamp with specified fields replaced.
3152 return self
.__class
__(
3153 self
.year
if year
is None else year
,
3154 self
.month
if month
is None else month
,
3155 self
.day
if day
is None else day
,
3156 self
.hour
if hour
is None else hour
,
3157 self
.minute
if minute
is None else minute
,
3158 self
.second
if second
is None else second
,
3159 nanosecond
=((self
.nanosecond
if microsecond
is None else microsecond
* 1000)
3160 if nanosecond
is None else nanosecond
),
3161 tzinfo
=self
.tzinfo
if tzinfo
is None else tzinfo
,
3162 fold
=self
.fold
if fold
is None else fold
)
3165 return hash((super().__hash
__(), self
.nanosecond
)) if self
.nanosecond
else super().__hash
__()
3167 def __eq__(self
, other
):
3168 return super().__eq
__(other
) and self
.nanosecond
== (
3169 other
.nanosecond
if isinstance(other
, nsdatetime
) else other
.microsecond
* 1000)
3171 def __gt__(self
, other
):
3172 return super().__gt
__(other
) or (super().__eq
__(other
) and self
.nanosecond
> (
3173 other
.nanosecond
if isinstance(other
, nsdatetime
) else other
.microsecond
* 1000))
3175 def __lt__(self
, other
):
3176 return not (self
> other
or self
== other
)
3178 def __ge__(self
, other
):
3179 return not self
< other
3181 def __le__(self
, other
):
3182 return not self
> other
3184 def __ne__(self
, other
):
3185 return not self
== other
3188 def to_nsdatetime(dt
, nsec
):
3189 """Apply nanoseconds to datetime.
3193 return nsdatetime(dt
.year
, dt
.month
, dt
.day
, dt
.hour
, dt
.minute
, dt
.second
,
3194 tzinfo
=dt
.tzinfo
, fold
=dt
.fold
, nanosecond
=nsec
)
3198 """Convert datatime instance to nanoseconds.
3200 secs
= int(dt
.timestamp())
3201 nsecs
= dt
.nanosecond
if isinstance(dt
, nsdatetime
) else dt
.microsecond
* 1000
3202 return secs
* 1000000000 + nsecs
3205 def custom_popen(cmd
):
3206 """Disconnect cmd from parent fds, read only from stdout.
3208 creationflags
= 0x08000000 if WIN32
else 0 # CREATE_NO_WINDOW
3210 p
= Popen(cmd
, bufsize
=0, stdout
=PIPE
, stderr
=STDOUT
, stdin
=DEVNULL
,
3211 creationflags
=creationflags
)
3212 except OSError as ex
:
3213 if ex
.errno
== errno
.ENOENT
:
3214 raise RarCannotExec("Unrar not installed?") from None
3215 if ex
.errno
== errno
.EACCES
or ex
.errno
== errno
.EPERM
:
3216 raise RarCannotExec("Cannot execute unrar") from None
3221 def check_returncode(code
, out
, errmap
):
3222 """Raise exception according to unrar exit code.
3227 if code
> 0 and code
< len(errmap
):
3234 exc
= RarUnknownError
3238 msg
= "%s [%d]: %s" % (exc
.__doc
__, code
, out
)
3240 msg
= "%s [%d]" % (exc
.__doc
__, code
)
3245 def membuf_tempfile(memfile
):
3246 """Write in-memory file object to real file.
3250 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
3251 tmpf
= os
.fdopen(tmpfd
, "wb")
3254 shutil
.copyfileobj(memfile
, tmpf
, BSIZE
)
3256 except BaseException
:
3264 # Find working command-line tool
3268 def __init__(self
, setup
):
3272 cmdline
= self
.get_cmdline("check_cmd", None)
3274 p
= custom_popen(cmdline
)
3275 out
, _
= p
.communicate()
3276 return p
.returncode
== 0
3277 except RarCannotExec
:
3280 def open_cmdline(self
, pwd
, rarfn
, filefn
=None):
3281 cmdline
= self
.get_cmdline("open_cmd", pwd
)
3282 cmdline
.append(rarfn
)
3284 self
.add_file_arg(cmdline
, filefn
)
3287 def get_errmap(self
):
3288 return self
.setup
["errmap"]
3290 def get_cmdline(self
, key
, pwd
, nodash
=False):
3291 cmdline
= list(self
.setup
[key
])
3292 cmdline
[0] = globals()[cmdline
[0]]
3293 self
.add_password_arg(cmdline
, pwd
)
3295 cmdline
.append("--")
3298 def add_file_arg(self
, cmdline
, filename
):
3299 cmdline
.append(filename
)
3301 def add_password_arg(self
, cmdline
, pwd
):
3302 """Append password switch to commandline.
3305 if not isinstance(pwd
, str):
3306 pwd
= pwd
.decode("utf8")
3307 args
= self
.setup
["password"]
3309 tool
= self
.setup
["open_cmd"][0]
3310 raise RarCannotExec(f
"{tool} does not support passwords")
3311 elif isinstance(args
, str):
3312 cmdline
.append(args
+ pwd
)
3314 cmdline
.extend(args
)
3317 cmdline
.extend(self
.setup
["no_password"])
3321 "open_cmd": ("UNRAR_TOOL", "p", "-inul"),
3322 "check_cmd": ("UNRAR_TOOL", "-inul"),
3324 "no_password": ("-p-",),
3325 # map return code to exception class, codes from rar.txt
3327 RarWarning
, RarFatalError
, RarCRCError
, RarLockedArchiveError
, # 1..4
3328 RarWriteError
, RarOpenError
, RarUserError
, RarMemoryError
, # 5..8
3329 RarCreateError
, RarNoFilesError
, RarWrongPassword
] # 9..11
3332 # Problems with unar RAR backend:
3333 # - Does not support RAR2 locked files [fails to read]
3334 # - Does not support RAR5 Blake2sp hash [reading works]
3336 "open_cmd": ("UNAR_TOOL", "-q", "-o", "-"),
3337 "check_cmd": ("UNAR_TOOL", "-version"),
3338 "password": ("-p",),
3339 "no_password": ("-p", ""),
3343 # Problems with libarchive RAR backend:
3344 # - Does not support solid archives.
3345 # - Does not support password-protected archives.
3346 # - Does not support RARVM-based compression filters.
3348 "open_cmd": ("BSDTAR_TOOL", "-x", "--to-stdout", "-f"),
3349 "check_cmd": ("BSDTAR_TOOL", "--version"),
3355 CURRENT_SETUP
= None
3358 def tool_setup(unrar
=True, unar
=True, bsdtar
=True, force
=False):
3359 """Pick a tool, return cached ToolSetup.
3361 global CURRENT_SETUP
3363 CURRENT_SETUP
= None
3364 if CURRENT_SETUP
is not None:
3365 return CURRENT_SETUP
3368 lst
.append(UNRAR_CONFIG
)
3370 lst
.append(UNAR_CONFIG
)
3372 lst
.append(BSDTAR_CONFIG
)
3375 setup
= ToolSetup(conf
)
3377 CURRENT_SETUP
= setup
3379 if CURRENT_SETUP
is None:
3380 raise RarCannotExec("Cannot find working tool")
3381 return CURRENT_SETUP
3385 """Minimal command-line interface for rarfile module.
3388 p
= argparse
.ArgumentParser(description
=main
.__doc
__)
3389 g
= p
.add_mutually_exclusive_group(required
=True)
3390 g
.add_argument("-l", "--list", metavar
="<rarfile>",
3391 help="Show archive listing")
3392 g
.add_argument("-e", "--extract", nargs
=2,
3393 metavar
=("<rarfile>", "<output_dir>"),
3394 help="Extract archive into target dir")
3395 g
.add_argument("-t", "--test", metavar
="<rarfile>",
3396 help="Test if a archive is valid")
3397 cmd
= p
.parse_args(args
)
3400 with
RarFile(cmd
.list) as rf
:
3403 with
RarFile(cmd
.test
) as rf
:
3406 with
RarFile(cmd
.extract
[0]) as rf
:
3407 rf
.extractall(cmd
.extract
[1])
3410 if __name__
== "__main__":