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
, sha256
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 #: executable for p7zip/7z tool
116 #: executable for alternative 7z tool
117 SEVENZIP2_TOOL
= "7zz"
119 #: default fallback charset
120 DEFAULT_CHARSET
= "windows-1252"
122 #: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
123 TRY_ENCODINGS
= ("utf8", "utf-16le")
125 #: whether to speed up decompression by using tmp archive
128 #: limit the filesize for tmp archive usage
129 HACK_SIZE_LIMIT
= 20 * 1024 * 1024
131 #: set specific directory for mkstemp() used by hack dir usage
134 #: Separator for path name components. Always "/".
142 RAR_BLOCK_MARK
= 0x72 # r
143 RAR_BLOCK_MAIN
= 0x73 # s
144 RAR_BLOCK_FILE
= 0x74 # t
145 RAR_BLOCK_OLD_COMMENT
= 0x75 # u
146 RAR_BLOCK_OLD_EXTRA
= 0x76 # v
147 RAR_BLOCK_OLD_SUB
= 0x77 # w
148 RAR_BLOCK_OLD_RECOVERY
= 0x78 # x
149 RAR_BLOCK_OLD_AUTH
= 0x79 # y
150 RAR_BLOCK_SUB
= 0x7a # z
151 RAR_BLOCK_ENDARC
= 0x7b # {
153 # flags for RAR_BLOCK_MAIN
154 RAR_MAIN_VOLUME
= 0x0001
155 RAR_MAIN_COMMENT
= 0x0002
156 RAR_MAIN_LOCK
= 0x0004
157 RAR_MAIN_SOLID
= 0x0008
158 RAR_MAIN_NEWNUMBERING
= 0x0010
159 RAR_MAIN_AUTH
= 0x0020
160 RAR_MAIN_RECOVERY
= 0x0040
161 RAR_MAIN_PASSWORD
= 0x0080
162 RAR_MAIN_FIRSTVOLUME
= 0x0100
163 RAR_MAIN_ENCRYPTVER
= 0x0200
165 # flags for RAR_BLOCK_FILE
166 RAR_FILE_SPLIT_BEFORE
= 0x0001
167 RAR_FILE_SPLIT_AFTER
= 0x0002
168 RAR_FILE_PASSWORD
= 0x0004
169 RAR_FILE_COMMENT
= 0x0008
170 RAR_FILE_SOLID
= 0x0010
171 RAR_FILE_DICTMASK
= 0x00e0
172 RAR_FILE_DICT64
= 0x0000
173 RAR_FILE_DICT128
= 0x0020
174 RAR_FILE_DICT256
= 0x0040
175 RAR_FILE_DICT512
= 0x0060
176 RAR_FILE_DICT1024
= 0x0080
177 RAR_FILE_DICT2048
= 0x00a0
178 RAR_FILE_DICT4096
= 0x00c0
179 RAR_FILE_DIRECTORY
= 0x00e0
180 RAR_FILE_LARGE
= 0x0100
181 RAR_FILE_UNICODE
= 0x0200
182 RAR_FILE_SALT
= 0x0400
183 RAR_FILE_VERSION
= 0x0800
184 RAR_FILE_EXTTIME
= 0x1000
185 RAR_FILE_EXTFLAGS
= 0x2000
187 # flags for RAR_BLOCK_ENDARC
188 RAR_ENDARC_NEXT_VOLUME
= 0x0001
189 RAR_ENDARC_DATACRC
= 0x0002
190 RAR_ENDARC_REVSPACE
= 0x0004
191 RAR_ENDARC_VOLNR
= 0x0008
193 # flags common to all blocks
194 RAR_SKIP_IF_UNKNOWN
= 0x4000
195 RAR_LONG_BLOCK
= 0x8000
198 RAR_OS_MSDOS
= 0 #: MSDOS (only in RAR3)
199 RAR_OS_OS2
= 1 #: OS2 (only in RAR3)
200 RAR_OS_WIN32
= 2 #: Windows
201 RAR_OS_UNIX
= 3 #: UNIX
202 RAR_OS_MACOS
= 4 #: MacOS (only in RAR3)
203 RAR_OS_BEOS
= 5 #: BeOS (only in RAR3)
205 # Compression methods - "0".."5"
206 RAR_M0
= 0x30 #: No compression.
207 RAR_M1
= 0x31 #: Compression level `-m1` - Fastest compression.
208 RAR_M2
= 0x32 #: Compression level `-m2`.
209 RAR_M3
= 0x33 #: Compression level `-m3`.
210 RAR_M4
= 0x34 #: Compression level `-m4`.
211 RAR_M5
= 0x35 #: Compression level `-m5` - Maximum compression.
219 RAR5_BLOCK_SERVICE
= 3
220 RAR5_BLOCK_ENCRYPTION
= 4
221 RAR5_BLOCK_ENDARC
= 5
223 RAR5_BLOCK_FLAG_EXTRA_DATA
= 0x01
224 RAR5_BLOCK_FLAG_DATA_AREA
= 0x02
225 RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN
= 0x04
226 RAR5_BLOCK_FLAG_SPLIT_BEFORE
= 0x08
227 RAR5_BLOCK_FLAG_SPLIT_AFTER
= 0x10
228 RAR5_BLOCK_FLAG_DEPENDS_PREV
= 0x20
229 RAR5_BLOCK_FLAG_KEEP_WITH_PARENT
= 0x40
231 RAR5_MAIN_FLAG_ISVOL
= 0x01
232 RAR5_MAIN_FLAG_HAS_VOLNR
= 0x02
233 RAR5_MAIN_FLAG_SOLID
= 0x04
234 RAR5_MAIN_FLAG_RECOVERY
= 0x08
235 RAR5_MAIN_FLAG_LOCKED
= 0x10
237 RAR5_FILE_FLAG_ISDIR
= 0x01
238 RAR5_FILE_FLAG_HAS_MTIME
= 0x02
239 RAR5_FILE_FLAG_HAS_CRC32
= 0x04
240 RAR5_FILE_FLAG_UNKNOWN_SIZE
= 0x08
242 RAR5_COMPR_SOLID
= 0x40
244 RAR5_ENC_FLAG_HAS_CHECKVAL
= 0x01
246 RAR5_ENDARC_FLAG_NEXT_VOL
= 0x01
248 RAR5_XFILE_ENCRYPTION
= 1
251 RAR5_XFILE_VERSION
= 4
254 RAR5_XFILE_SERVICE
= 7
256 RAR5_XTIME_UNIXTIME
= 0x01
257 RAR5_XTIME_HAS_MTIME
= 0x02
258 RAR5_XTIME_HAS_CTIME
= 0x04
259 RAR5_XTIME_HAS_ATIME
= 0x08
260 RAR5_XTIME_UNIXTIME_NS
= 0x10
262 RAR5_XENC_CIPHER_AES256
= 0
264 RAR5_XENC_CHECKVAL
= 0x01
265 RAR5_XENC_TWEAKED
= 0x02
267 RAR5_XHASH_BLAKE2SP
= 0
269 RAR5_XREDIR_UNIX_SYMLINK
= 1
270 RAR5_XREDIR_WINDOWS_SYMLINK
= 2
271 RAR5_XREDIR_WINDOWS_JUNCTION
= 3
272 RAR5_XREDIR_HARD_LINK
= 4
273 RAR5_XREDIR_FILE_COPY
= 5
275 RAR5_XREDIR_ISDIR
= 0x01
277 RAR5_XOWNER_UNAME
= 0x01
278 RAR5_XOWNER_GNAME
= 0x02
279 RAR5_XOWNER_UID
= 0x04
280 RAR5_XOWNER_GID
= 0x08
285 DOS_MODE_ARCHIVE
= 0x20
287 DOS_MODE_SYSTEM
= 0x04
288 DOS_MODE_HIDDEN
= 0x02
289 DOS_MODE_READONLY
= 0x01
291 RAR5_PW_CHECK_SIZE
= 8
295 ## internal constants
298 RAR_ID
= b
"Rar!\x1a\x07\x00"
299 RAR5_ID
= b
"Rar!\x1a\x07\x01\x00"
301 WIN32
= sys
.platform
== "win32"
302 BSIZE
= 512 * 1024 if WIN32
else 64 * 1024
304 SFX_MAX_SIZE
= 2 * 1024 * 1024
308 _BAD_CHARS
= r
"""\x00-\x1F<>|"?*"""
309 RC_BAD_CHARS_UNIX
= re
.compile(r
"[%s]" % _BAD_CHARS
)
310 RC_BAD_CHARS_WIN32
= re
.compile(r
"[%s:^\\]" % _BAD_CHARS
)
315 def _find_sfx_header(xfile
):
318 steps
= (64, SFX_MAX_SIZE
)
320 with
XFile(xfile
) as fd
:
326 curdata
= buf
.getvalue()
329 pos
= curdata
.find(sig
, findpos
)
332 if curdata
[pos
:pos
+ len(RAR_ID
)] == RAR_ID
:
334 if curdata
[pos
:pos
+ len(RAR5_ID
)] == RAR5_ID
:
336 findpos
= pos
+ len(sig
)
345 def get_rar_version(xfile
):
346 """Check quickly whether file is rar archive.
348 with
XFile(xfile
) as fd
:
349 buf
= fd
.read(len(RAR5_ID
))
350 if buf
.startswith(RAR_ID
):
352 elif buf
.startswith(RAR5_ID
):
357 def is_rarfile(xfile
):
358 """Check quickly whether file is rar archive.
361 return get_rar_version(xfile
) > 0
363 # File not found or not accessible, ignore
367 def is_rarfile_sfx(xfile
):
368 """Check whether file is rar archive with support for SFX.
370 It will read 2M from file.
372 return _find_sfx_header(xfile
)[0] > 0
375 class Error(Exception):
376 """Base class for rarfile errors."""
379 class BadRarFile(Error
):
380 """Incorrect data in archive."""
383 class NotRarFile(Error
):
384 """The file is not RAR archive."""
387 class BadRarName(Error
):
388 """Cannot guess multipart name components."""
391 class NoRarEntry(Error
):
392 """File not found in RAR"""
395 class PasswordRequired(Error
):
396 """File requires password"""
399 class NeedFirstVolume(Error
):
400 """Need to start from first volume.
405 Volume number of current file or None if not known
407 def __init__(self
, msg
, volume
):
408 super().__init
__(msg
)
409 self
.current_volume
= volume
412 class NoCrypto(Error
):
413 """Cannot parse encrypted headers - no crypto available."""
416 class RarExecError(Error
):
417 """Problem reported by unrar/rar."""
420 class RarWarning(RarExecError
):
421 """Non-fatal error"""
424 class RarFatalError(RarExecError
):
428 class RarCRCError(RarExecError
):
429 """CRC error during unpacking"""
432 class RarLockedArchiveError(RarExecError
):
433 """Must not modify locked archive"""
436 class RarWriteError(RarExecError
):
440 class RarOpenError(RarExecError
):
444 class RarUserError(RarExecError
):
448 class RarMemoryError(RarExecError
):
452 class RarCreateError(RarExecError
):
456 class RarNoFilesError(RarExecError
):
457 """No files that match pattern were found"""
460 class RarUserBreak(RarExecError
):
464 class RarWrongPassword(RarExecError
):
465 """Incorrect password"""
468 class RarUnknownError(RarExecError
):
469 """Unknown exit code"""
472 class RarSignalExit(RarExecError
):
473 """Unrar exited with signal"""
476 class RarCannotExec(RarExecError
):
477 """Executable not found."""
480 class UnsupportedWarning(UserWarning):
481 """Archive uses feature that are unsupported by rarfile.
483 .. versionadded:: 4.0
488 r
"""An entry in rar archive.
490 Timestamps as :class:`~datetime.datetime` are without timezone in RAR3,
491 with UTC timezone in RAR5 archives.
496 File name with relative path.
497 Path separator is "/". Always unicode string.
500 File modification timestamp. As tuple of (year, month, day, hour, minute, second).
501 RAR5 allows archives where it is missing, it's None then.
504 Optional file comment field. Unicode string. (RAR3-only)
513 Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants.
516 Minimal Rar version needed for decompressing. As (major*10 + minor),
521 RAR5 does not have such field in archive, it's simply set to 50.
524 Host OS type, one of RAR_OS_* constants.
526 RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`,
527 :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`.
529 RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`.
532 File attributes. May be either dos-style or unix-style, depending on host_os.
535 File modification time. Same value as :attr:`date_time`
536 but as :class:`~datetime.datetime` object with extended precision.
539 Optional time field: creation time. As :class:`~datetime.datetime` object.
542 Optional time field: last access time. As :class:`~datetime.datetime` object.
545 Optional time field: archival time. As :class:`~datetime.datetime` object.
549 CRC-32 of uncompressed file, unsigned int.
554 Blake2SP hash over decompressed data. (RAR5-only)
557 Volume nr, starting from 0.
560 Volume file name, where file starts.
563 If not None, file is link of some sort. Contains tuple of (type, flags, target).
566 Type is one of constants:
568 :data:`RAR5_XREDIR_UNIX_SYMLINK`
570 :data:`RAR5_XREDIR_WINDOWS_SYMLINK`
572 :data:`RAR5_XREDIR_WINDOWS_JUNCTION`
574 :data:`RAR5_XREDIR_HARD_LINK`
576 :data:`RAR5_XREDIR_FILE_COPY`
577 Current file is copy of another archive entry.
579 Flags may contain bits:
581 :data:`RAR5_XREDIR_ISDIR`
582 Symlink points to directory.
585 # zipfile-compatible fields
594 # optional extended time fields, datetime() objects.
599 extract_version
= None
618 """Returns True if entry is a directory.
620 .. versionadded:: 4.0
624 def is_symlink(self
):
625 """Returns True if entry is a symlink.
627 .. versionadded:: 4.0
632 """Returns True if entry is a normal file.
634 .. versionadded:: 4.0
638 def needs_password(self
):
639 """Returns True if data is stored password-protected.
641 if self
.type == RAR_BLOCK_FILE
:
642 return (self
.flags
& RAR_FILE_PASSWORD
) > 0
646 """Returns True if entry is a directory.
654 """Parse RAR structure, provide access to files in 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 .. versionadded:: 4.0
678 #: File name, if available. Unicode string or None.
681 #: Archive comment. Unicode string or None.
684 def __init__(self
, file, mode
="r", charset
=None, info_callback
=None,
685 crc_check
=True, errors
="stop", part_only
=False):
686 if is_filelike(file):
687 self
.filename
= getattr(file, "name", None)
689 if isinstance(file, Path
):
694 self
._charset
= charset
or DEFAULT_CHARSET
695 self
._info
_callback
= info_callback
696 self
._crc
_check
= crc_check
697 self
._part
_only
= part_only
698 self
._password
= None
699 self
._file
_parser
= None
703 elif errors
== "strict":
706 raise ValueError("Invalid value for errors= parameter.")
709 raise NotImplementedError("RarFile supports only mode=r")
717 def __exit__(self
, typ
, value
, traceback
):
722 """Iterate over members."""
723 return iter(self
.infolist())
725 def setpassword(self
, pwd
):
726 """Sets the password to use when extracting.
729 if self
._file
_parser
:
730 if self
._file
_parser
.has_header_encryption():
731 self
._file
_parser
= None
732 if not self
._file
_parser
:
735 self
._file
_parser
.setpassword(self
._password
)
737 def needs_password(self
):
738 """Returns True if any archive entries require password for extraction.
740 return self
._file
_parser
.needs_password()
743 """Returns True if archive uses solid compression.
745 return self
._file
_parser
.is_solid()
748 """Return list of filenames in archive.
750 return [f
.filename
for f
in self
.infolist()]
753 """Return RarInfo objects for all files/directories in archive.
755 return self
._file
_parser
.infolist()
757 def volumelist(self
):
758 """Returns filenames of archive volumes.
760 In case of single-volume archive, the list contains
761 just the name of main archive file.
763 return self
._file
_parser
.volumelist()
765 def getinfo(self
, name
):
766 """Return RarInfo for file.
768 return self
._file
_parser
.getinfo(name
)
770 def getinfo_orig(self
, name
):
771 """Return RarInfo for file source.
773 RAR5: if name is hard-linked or copied file,
774 returns original entry with original filename.
776 .. versionadded:: 4.1
778 return self
._file
_parser
.getinfo_orig(name
)
780 def open(self
, name
, mode
="r", pwd
=None):
781 """Returns file-like object (:class:`RarExtFile`) from where the data can be read.
783 The object implements :class:`io.RawIOBase` interface, so it can
784 be further wrapped with :class:`io.BufferedReader`
785 and :class:`io.TextIOWrapper`.
787 On older Python where io module is not available, it implements
788 only .read(), .seek(), .tell() and .close() methods.
790 The object is seekable, although the seeking is fast only on
791 uncompressed files, on compressed files the seeking is implemented
792 by reading ahead and/or restarting the decompression.
797 file name or RarInfo instance.
801 password to use for extracting.
805 raise NotImplementedError("RarFile.open() supports only mode=r")
808 inf
= self
.getinfo(name
)
810 raise io
.UnsupportedOperation("Directory does not have any data: " + inf
.filename
)
813 if inf
.needs_password():
814 pwd
= pwd
or self
._password
816 raise PasswordRequired("File %s requires password" % inf
.filename
)
820 return self
._file
_parser
.open(inf
, pwd
)
822 def read(self
, name
, pwd
=None):
823 """Return uncompressed data for archive entry.
825 For longer files using :meth:`~RarFile.open` may be better idea.
830 filename or RarInfo instance
832 password to use for extracting.
835 with self
.open(name
, "r", pwd
) as f
:
839 """Release open resources."""
842 def printdir(self
, file=None):
843 """Print archive file list to stdout or given file.
847 for f
in self
.infolist():
848 print(f
.filename
, file=file)
850 def extract(self
, member
, path
=None, pwd
=None):
851 """Extract single file into current directory.
856 filename or :class:`RarInfo` instance
858 optional destination path
860 optional password to use
862 inf
= self
.getinfo(member
)
863 return self
._extract
_one
(inf
, path
, pwd
, True)
865 def extractall(self
, path
=None, members
=None, pwd
=None):
866 """Extract all files into current directory.
871 optional destination path
873 optional filename or :class:`RarInfo` instance list to extract
875 optional password to use
878 members
= self
.namelist()
883 inf
= self
.getinfo(m
)
884 dst
= self
._extract
_one
(inf
, path
, pwd
, not inf
.is_dir())
887 dirs
.append((dst
, inf
))
890 dirs
.sort(reverse
=True)
891 for dst
, inf
in dirs
:
892 self
._set
_attrs
(inf
, dst
)
894 def testrar(self
, pwd
=None):
895 """Read all files and test CRC.
897 for member
in self
.infolist():
899 with self
.open(member
, 'r', pwd
) as f
:
900 empty_read(f
, member
.file_size
, BSIZE
)
903 """Return error string if parsing failed or None if no problems.
905 if not self
._file
_parser
:
906 return "Not a RAR file"
907 return self
._file
_parser
.strerror()
914 """Run parser for file type
916 ver
, sfx_ofs
= _find_sfx_header(self
._rarfile
)
918 p3
= RAR3Parser(self
._rarfile
, self
._password
, self
._crc
_check
,
919 self
._charset
, self
._strict
, self
._info
_callback
,
920 sfx_ofs
, self
._part
_only
)
921 self
._file
_parser
= p3
# noqa
923 p5
= RAR5Parser(self
._rarfile
, self
._password
, self
._crc
_check
,
924 self
._charset
, self
._strict
, self
._info
_callback
,
925 sfx_ofs
, self
._part
_only
)
926 self
._file
_parser
= p5
# noqa
928 raise NotRarFile("Not a RAR file")
930 self
._file
_parser
.parse()
931 self
.comment
= self
._file
_parser
.comment
933 def _extract_one(self
, info
, path
, pwd
, set_attrs
):
934 fname
= sanitize_filename(
935 info
.filename
, os
.path
.sep
, WIN32
941 path
= os
.fspath(path
)
942 dstfn
= os
.path
.join(path
, fname
)
944 dirname
= os
.path
.dirname(dstfn
)
945 if dirname
and dirname
!= ".":
946 os
.makedirs(dirname
, exist_ok
=True)
949 return self
._make
_file
(info
, dstfn
, pwd
, set_attrs
)
951 return self
._make
_dir
(info
, dstfn
, pwd
, set_attrs
)
952 if info
.is_symlink():
953 return self
._make
_symlink
(info
, dstfn
, pwd
, set_attrs
)
956 def _create_helper(self
, name
, flags
, info
):
957 return os
.open(name
, flags
)
959 def _make_file(self
, info
, dstfn
, pwd
, set_attrs
):
960 def helper(name
, flags
):
961 return self
._create
_helper
(name
, flags
, info
)
962 with self
.open(info
, "r", pwd
) as src
:
963 with
open(dstfn
, "wb", opener
=helper
) as dst
:
964 shutil
.copyfileobj(src
, dst
)
966 self
._set
_attrs
(info
, dstfn
)
969 def _make_dir(self
, info
, dstfn
, pwd
, set_attrs
):
970 os
.makedirs(dstfn
, exist_ok
=True)
972 self
._set
_attrs
(info
, dstfn
)
975 def _make_symlink(self
, info
, dstfn
, pwd
, set_attrs
):
976 target_is_directory
= False
977 if info
.host_os
== RAR_OS_UNIX
:
978 link_name
= self
.read(info
, pwd
)
979 target_is_directory
= (info
.flags
& RAR_FILE_DIRECTORY
) == RAR_FILE_DIRECTORY
980 elif info
.file_redir
:
981 redir_type
, redir_flags
, link_name
= info
.file_redir
982 if redir_type
== RAR5_XREDIR_WINDOWS_JUNCTION
:
983 warnings
.warn(f
"Windows junction not supported - {info.filename}", UnsupportedWarning
)
985 target_is_directory
= (redir_type
& RAR5_XREDIR_ISDIR
) > 0
987 warnings
.warn(f
"Unsupported link type - {info.filename}", UnsupportedWarning
)
990 os
.symlink(link_name
, dstfn
, target_is_directory
=target_is_directory
)
993 def _set_attrs(self
, info
, dstfn
):
994 if info
.host_os
== RAR_OS_UNIX
:
995 os
.chmod(dstfn
, info
.mode
& 0o777)
996 elif info
.host_os
in (RAR_OS_WIN32
, RAR_OS_MSDOS
):
997 # only keep R/O attr, except for dirs on win32
998 if info
.mode
& DOS_MODE_READONLY
and (info
.is_file() or not WIN32
):
1000 new_mode
= st
.st_mode
& ~
0o222
1001 os
.chmod(dstfn
, new_mode
)
1004 mtime_ns
= to_nsecs(info
.mtime
)
1005 atime_ns
= to_nsecs(info
.atime
) if info
.atime
else mtime_ns
1006 os
.utime(dstfn
, ns
=(atime_ns
, mtime_ns
))
1010 # File format parsing
1014 """Shared parser parts."""
1017 _needs_password
= False
1024 def __init__(self
, rarfile
, password
, crc_check
, charset
, strict
,
1025 info_cb
, sfx_offset
, part_only
):
1026 self
._rarfile
= rarfile
1027 self
._password
= password
1028 self
._crc
_check
= crc_check
1029 self
._charset
= charset
1030 self
._strict
= strict
1031 self
._info
_callback
= info_cb
1032 self
._info
_list
= []
1035 self
._sfx
_offset
= sfx_offset
1036 self
._part
_only
= part_only
1039 """Returns True if archive uses solid compression.
1042 if self
._main
.flags
& RAR_MAIN_SOLID
:
1046 def has_header_encryption(self
):
1047 """Returns True if headers are encrypted
1049 if self
._hdrenc
_main
:
1052 if self
._main
.flags
& RAR_MAIN_PASSWORD
:
1056 def setpassword(self
, pwd
):
1057 """Set cached password."""
1058 self
._password
= pwd
1060 def volumelist(self
):
1062 return self
._vol
_list
1064 def needs_password(self
):
1065 """Is password required"""
1066 return self
._needs
_password
1070 return self
._parse
_error
1073 """List of RarInfo records.
1075 return self
._info
_list
1077 def getinfo(self
, member
):
1078 """Return RarInfo for filename
1080 if isinstance(member
, RarInfo
):
1081 fname
= member
.filename
1082 elif isinstance(member
, Path
):
1087 if fname
.endswith("/"):
1088 fname
= fname
.rstrip("/")
1091 return self
._info
_map
[fname
]
1093 raise NoRarEntry("No such file: %s" % fname
) from None
1095 def getinfo_orig(self
, member
):
1096 inf
= self
.getinfo(member
)
1098 redir_type
, redir_flags
, redir_name
= inf
.file_redir
1099 # cannot leave to unrar as it expects copied file to exist
1100 if redir_type
in (RAR5_XREDIR_FILE_COPY
, RAR5_XREDIR_HARD_LINK
):
1101 inf
= self
.getinfo(redir_name
)
1114 def _parse_real(self
):
1115 """Actually read file.
1117 fd
= XFile(self
._rarfile
)
1119 fd
.seek(self
._sfx
_offset
, 0)
1120 sig
= fd
.read(len(self
._expect
_sig
))
1121 if sig
!= self
._expect
_sig
:
1122 raise NotRarFile("Not a Rar archive")
1124 volume
= 0 # first vol (.rar) is 0
1127 volfile
= self
._rarfile
1128 self
._vol
_list
= [self
._rarfile
]
1129 raise_need_first_vol
= False
1132 h
= None # don"t read past ENDARC
1134 h
= self
._parse
_header
(fd
)
1136 if raise_need_first_vol
:
1137 # did not find ENDARC with VOLNR
1138 raise NeedFirstVolume("Need to start from first volume", None)
1139 if more_vols
and not self
._part
_only
:
1143 volfile
= self
._next
_volname
(volfile
)
1146 self
._set
_error
("Cannot open next volume: %s", volfile
)
1149 sig
= fd
.read(len(self
._expect
_sig
))
1150 if sig
!= self
._expect
_sig
:
1151 self
._set
_error
("Invalid volume sig: %s", volfile
)
1155 self
._vol
_list
.append(volfile
)
1157 self
._hdrenc
_main
= None
1161 h
.volume_file
= volfile
1163 if h
.type == RAR_BLOCK_MAIN
and not self
._main
:
1165 if volume
== 0 and (h
.flags
& RAR_MAIN_NEWNUMBERING
) and not self
._part
_only
:
1166 # RAR 2.x does not set FIRSTVOLUME,
1167 # so check it only if NEWNUMBERING is used
1168 if (h
.flags
& RAR_MAIN_FIRSTVOLUME
) == 0:
1169 if getattr(h
, "main_volume_number", None) is not None:
1170 # rar5 may have more info
1171 raise NeedFirstVolume(
1172 "Need to start from first volume (current: %r)"
1173 % (h
.main_volume_number
,),
1174 h
.main_volume_number
1176 # delay raise until we have volnr from ENDARC
1177 raise_need_first_vol
= True
1178 if h
.flags
& RAR_MAIN_PASSWORD
:
1179 self
._needs
_password
= True
1180 if not self
._password
:
1182 elif h
.type == RAR_BLOCK_ENDARC
:
1183 more_vols
= (h
.flags
& RAR_ENDARC_NEXT_VOLUME
) > 0
1185 if raise_need_first_vol
and (h
.flags
& RAR_ENDARC_VOLNR
) > 0:
1186 raise NeedFirstVolume(
1187 "Need to start from first volume (current: %r)"
1188 % (h
.endarc_volnr
,),
1191 elif h
.type == RAR_BLOCK_FILE
:
1192 # RAR 2.x does not write RAR_BLOCK_ENDARC
1193 if h
.flags
& RAR_FILE_SPLIT_AFTER
:
1195 # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
1196 if volume
== 0 and h
.flags
& RAR_FILE_SPLIT_BEFORE
:
1197 if not self
._part
_only
:
1198 raise_need_first_vol
= True
1200 if h
.needs_password():
1201 self
._needs
_password
= True
1204 self
.process_entry(fd
, h
)
1206 if self
._info
_callback
:
1207 self
._info
_callback
(h
)
1211 fd
.seek(h
.data_offset
+ h
.add_size
, 0)
1213 def process_entry(self
, fd
, item
):
1214 """Examine item, add into lookup cache."""
1215 raise NotImplementedError()
1217 def _decrypt_header(self
, fd
):
1218 raise NotImplementedError("_decrypt_header")
1220 def _parse_block_header(self
, fd
):
1221 raise NotImplementedError("_parse_block_header")
1223 def _open_hack(self
, inf
, pwd
):
1224 raise NotImplementedError("_open_hack")
1226 def _parse_header(self
, fd
):
1227 """Read single header
1230 # handle encrypted headers
1231 if (self
._main
and self
._main
.flags
& RAR_MAIN_PASSWORD
) or self
._hdrenc
_main
:
1232 if not self
._password
:
1234 fd
= self
._decrypt
_header
(fd
)
1236 # now read actual header
1237 return self
._parse
_block
_header
(fd
)
1238 except struct
.error
:
1239 self
._set
_error
("Broken header in RAR file")
1242 def _next_volname(self
, volfile
):
1243 """Given current vol name, construct next one
1245 if is_filelike(volfile
):
1246 raise IOError("Working on single FD")
1247 if self
._main
.flags
& RAR_MAIN_NEWNUMBERING
:
1248 return _next_newvol(volfile
)
1249 return _next_oldvol(volfile
)
1251 def _set_error(self
, msg
, *args
):
1254 self
._parse
_error
= msg
1256 raise BadRarFile(msg
)
1258 def open(self
, inf
, pwd
):
1259 """Return stream object for file data."""
1262 redir_type
, redir_flags
, redir_name
= inf
.file_redir
1263 # cannot leave to unrar as it expects copied file to exist
1264 if redir_type
in (RAR5_XREDIR_FILE_COPY
, RAR5_XREDIR_HARD_LINK
):
1265 inf
= self
.getinfo(redir_name
)
1267 raise BadRarFile("cannot find copied file")
1268 elif redir_type
in (
1269 RAR5_XREDIR_UNIX_SYMLINK
, RAR5_XREDIR_WINDOWS_SYMLINK
,
1270 RAR5_XREDIR_WINDOWS_JUNCTION
,
1272 return io
.BytesIO(redir_name
.encode("utf8"))
1273 if inf
.flags
& RAR_FILE_SPLIT_BEFORE
:
1274 raise NeedFirstVolume("Partial file, please start from first volume: " + inf
.filename
, None)
1276 # is temp write usable?
1280 elif self
._main
._must
_disable
_hack
():
1282 elif inf
._must
_disable
_hack
():
1284 elif is_filelike(self
._rarfile
):
1286 elif inf
.file_size
> HACK_SIZE_LIMIT
:
1288 elif not USE_EXTRACT_HACK
:
1292 if inf
.compress_type
== RAR_M0
and (inf
.flags
& RAR_FILE_PASSWORD
) == 0 and inf
.file_redir
is None:
1293 return self
._open
_clear
(inf
)
1295 return self
._open
_hack
(inf
, pwd
)
1296 elif is_filelike(self
._rarfile
):
1297 return self
._open
_unrar
_membuf
(self
._rarfile
, inf
, pwd
)
1299 return self
._open
_unrar
(self
._rarfile
, inf
, pwd
)
1301 def _open_clear(self
, inf
):
1303 return self
._open
_unrar
(self
._rarfile
, inf
)
1304 return DirectReader(self
, inf
)
1306 def _open_hack_core(self
, inf
, pwd
, prefix
, suffix
):
1308 size
= inf
.compress_size
+ inf
.header_size
1309 rf
= XFile(inf
.volume_file
, 0)
1310 rf
.seek(inf
.header_offset
)
1312 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
1313 tmpf
= os
.fdopen(tmpfd
, "wb")
1319 buf
= rf
.read(BSIZE
)
1323 raise BadRarFile("read failed: " + inf
.filename
)
1329 except BaseException
:
1335 return self
._open
_unrar
(tmpname
, inf
, pwd
, tmpname
)
1337 def _open_unrar_membuf(self
, memfile
, inf
, pwd
):
1338 """Write in-memory archive to temp file, needed for solid archives.
1340 tmpname
= membuf_tempfile(memfile
)
1341 return self
._open
_unrar
(tmpname
, inf
, pwd
, tmpname
, force_file
=True)
1343 def _open_unrar(self
, rarfile
, inf
, pwd
=None, tmpfile
=None, force_file
=False):
1344 """Extract using unrar
1346 setup
= tool_setup()
1348 # not giving filename avoids encoding related problems
1350 if not tmpfile
or force_file
:
1351 fn
= inf
.filename
.replace("/", os
.path
.sep
)
1353 # read from unrar pipe
1354 cmd
= setup
.open_cmdline(pwd
, rarfile
, fn
)
1355 return PipeReader(self
, inf
, cmd
, tmpfile
)
1362 class Rar3Info(RarInfo
):
1363 """RAR3 specific fields."""
1364 extract_version
= 15
1369 header_offset
= None
1375 # make sure some rar5 fields are always present
1377 blake2sp_hash
= None
1379 endarc_datacrc
= None
1382 def _must_disable_hack(self
):
1383 if self
.type == RAR_BLOCK_FILE
:
1384 if self
.flags
& RAR_FILE_PASSWORD
:
1386 elif self
.flags
& (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER
):
1388 elif self
.type == RAR_BLOCK_MAIN
:
1389 if self
.flags
& (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD
):
1394 """Returns True if entry is a directory."""
1395 if self
.type == RAR_BLOCK_FILE
and not self
.is_symlink():
1396 return (self
.flags
& RAR_FILE_DIRECTORY
) == RAR_FILE_DIRECTORY
1399 def is_symlink(self
):
1400 """Returns True if entry is a symlink."""
1402 self
.type == RAR_BLOCK_FILE
and
1403 self
.host_os
== RAR_OS_UNIX
and
1404 self
.mode
& 0xF000 == 0xA000
1408 """Returns True if entry is a normal file."""
1410 self
.type == RAR_BLOCK_FILE
and
1411 not (self
.is_dir() or self
.is_symlink())
1415 class RAR3Parser(CommonParser
):
1416 """Parse RAR3 file format.
1418 _expect_sig
= RAR_ID
1419 _last_aes_key
= (None, None, None) # (salt, key, iv)
1421 def _decrypt_header(self
, fd
):
1422 if not _have_crypto
:
1423 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1425 if self
._last
_aes
_key
[0] == salt
:
1426 key
, iv
= self
._last
_aes
_key
[1:]
1428 key
, iv
= rar3_s2k(self
._password
, salt
)
1429 self
._last
_aes
_key
= (salt
, key
, iv
)
1430 return HeaderDecrypt(fd
, key
, iv
)
1432 def _parse_block_header(self
, fd
):
1433 """Parse common block header
1436 h
.header_offset
= fd
.tell()
1438 # read and parse base header
1439 buf
= fd
.read(S_BLK_HDR
.size
)
1442 if len(buf
) < S_BLK_HDR
.size
:
1443 self
._set
_error
("Unexpected EOF when reading header")
1445 t
= S_BLK_HDR
.unpack_from(buf
)
1446 h
.header_crc
, h
.type, h
.flags
, h
.header_size
= t
1449 if h
.header_size
> S_BLK_HDR
.size
:
1450 hdata
= buf
+ fd
.read(h
.header_size
- S_BLK_HDR
.size
)
1453 h
.data_offset
= fd
.tell()
1456 if len(hdata
) != h
.header_size
:
1457 self
._set
_error
("Unexpected EOF when reading header")
1460 pos
= S_BLK_HDR
.size
1462 # block has data assiciated with it?
1463 if h
.flags
& RAR_LONG_BLOCK
:
1464 h
.add_size
, pos
= load_le32(hdata
, pos
)
1468 # parse interesting ones, decide header boundaries for crc
1469 if h
.type == RAR_BLOCK_MARK
:
1471 elif h
.type == RAR_BLOCK_MAIN
:
1473 if h
.flags
& RAR_MAIN_ENCRYPTVER
:
1476 if h
.flags
& RAR_MAIN_COMMENT
:
1477 self
._parse
_subblocks
(h
, hdata
, pos
)
1478 elif h
.type == RAR_BLOCK_FILE
:
1479 pos
= self
._parse
_file
_header
(h
, hdata
, pos
- 4)
1481 if h
.flags
& RAR_FILE_COMMENT
:
1482 pos
= self
._parse
_subblocks
(h
, hdata
, pos
)
1483 elif h
.type == RAR_BLOCK_SUB
:
1484 pos
= self
._parse
_file
_header
(h
, hdata
, pos
- 4)
1485 crc_pos
= h
.header_size
1486 elif h
.type == RAR_BLOCK_OLD_AUTH
:
1489 elif h
.type == RAR_BLOCK_OLD_EXTRA
:
1492 elif h
.type == RAR_BLOCK_ENDARC
:
1493 if h
.flags
& RAR_ENDARC_DATACRC
:
1494 h
.endarc_datacrc
, pos
= load_le32(hdata
, pos
)
1495 if h
.flags
& RAR_ENDARC_VOLNR
:
1496 h
.endarc_volnr
= S_SHORT
.unpack_from(hdata
, pos
)[0]
1498 crc_pos
= h
.header_size
1500 crc_pos
= h
.header_size
1503 if h
.type == RAR_BLOCK_OLD_SUB
:
1504 crcdat
= hdata
[2:] + fd
.read(h
.add_size
)
1506 crcdat
= hdata
[2:crc_pos
]
1508 calc_crc
= crc32(crcdat
) & 0xFFFF
1510 # return good header
1511 if h
.header_crc
== calc_crc
:
1514 # header parsing failed.
1515 self
._set
_error
("Header CRC error (%02x): exp=%x got=%x (xlen = %d)",
1516 h
.type, h
.header_crc
, calc_crc
, len(crcdat
))
1518 # instead panicing, send eof
1521 def _parse_file_header(self
, h
, hdata
, pos
):
1522 """Read file-specific header
1524 fld
= S_FILE_HDR
.unpack_from(hdata
, pos
)
1525 pos
+= S_FILE_HDR
.size
1527 h
.compress_size
= fld
[0]
1528 h
.file_size
= fld
[1]
1531 h
.date_time
= parse_dos_time(fld
[4])
1532 h
.mtime
= to_datetime(h
.date_time
)
1533 h
.extract_version
= fld
[5]
1534 h
.compress_type
= fld
[6]
1535 h
._name
_size
= name_size
= fld
[7]
1538 h
._md
_class
= CRC32Context
1539 h
._md
_expect
= h
.CRC
1541 if h
.flags
& RAR_FILE_LARGE
:
1542 h1
, pos
= load_le32(hdata
, pos
)
1543 h2
, pos
= load_le32(hdata
, pos
)
1544 h
.compress_size |
= h1
<< 32
1545 h
.file_size |
= h2
<< 32
1546 h
.add_size
= h
.compress_size
1548 name
, pos
= load_bytes(hdata
, name_size
, pos
)
1549 if h
.flags
& RAR_FILE_UNICODE
and b
"\0" in name
:
1550 # stored in custom encoding
1551 nul
= name
.find(b
"\0")
1552 h
.orig_filename
= name
[:nul
]
1553 u
= UnicodeFilename(h
.orig_filename
, name
[nul
+ 1:])
1554 h
.filename
= u
.decode()
1556 # if parsing failed fall back to simple name
1558 h
.filename
= self
._decode
(h
.orig_filename
)
1559 elif h
.flags
& RAR_FILE_UNICODE
:
1561 h
.orig_filename
= name
1562 h
.filename
= name
.decode("utf8", "replace")
1564 # stored in random encoding
1565 h
.orig_filename
= name
1566 h
.filename
= self
._decode
(name
)
1568 # change separator, set dir suffix
1569 h
.filename
= h
.filename
.replace("\\", "/").rstrip("/")
1571 h
.filename
= h
.filename
+ "/"
1573 if h
.flags
& RAR_FILE_SALT
:
1574 h
.salt
, pos
= load_bytes(hdata
, 8, pos
)
1578 # optional extended time stamps
1579 if h
.flags
& RAR_FILE_EXTTIME
:
1580 pos
= _parse_ext_time(h
, hdata
, pos
)
1582 h
.mtime
= h
.atime
= h
.ctime
= h
.arctime
= None
1586 def _parse_subblocks(self
, h
, hdata
, pos
):
1587 """Find old-style comment subblock
1589 while pos
< len(hdata
):
1590 # ordinary block header
1591 t
= S_BLK_HDR
.unpack_from(hdata
, pos
)
1592 ___scrc
, stype
, sflags
, slen
= t
1593 pos_next
= pos
+ slen
1594 pos
+= S_BLK_HDR
.size
1600 # followed by block-specific header
1601 if stype
== RAR_BLOCK_OLD_COMMENT
and pos
+ S_COMMENT_HDR
.size
<= pos_next
:
1602 declen
, ver
, meth
, crc
= S_COMMENT_HDR
.unpack_from(hdata
, pos
)
1603 pos
+= S_COMMENT_HDR
.size
1604 data
= hdata
[pos
: pos_next
]
1605 cmt
= rar3_decompress(ver
, meth
, data
, declen
, sflags
,
1606 crc
, self
._password
)
1607 if not self
._crc
_check
or (crc32(cmt
) & 0xFFFF == crc
):
1608 h
.comment
= self
._decode
_comment
(cmt
)
1613 def _read_comment_v3(self
, inf
, pwd
=None):
1616 with
XFile(inf
.volume_file
) as rf
:
1617 rf
.seek(inf
.data_offset
)
1618 data
= rf
.read(inf
.compress_size
)
1621 cmt
= rar3_decompress(inf
.extract_version
, inf
.compress_type
, data
,
1622 inf
.file_size
, inf
.flags
, inf
.CRC
, pwd
, inf
.salt
)
1630 return self
._decode
_comment
(cmt
)
1632 def _decode(self
, val
):
1633 for c
in TRY_ENCODINGS
:
1635 return val
.decode(c
)
1636 except UnicodeError:
1638 return val
.decode(self
._charset
, "replace")
1640 def _decode_comment(self
, val
):
1641 return self
._decode
(val
)
1643 def process_entry(self
, fd
, item
):
1644 if item
.type == RAR_BLOCK_FILE
:
1645 # use only first part
1646 if item
.flags
& RAR_FILE_VERSION
:
1647 pass # skip old versions
1648 elif (item
.flags
& RAR_FILE_SPLIT_BEFORE
) == 0:
1649 self
._info
_map
[item
.filename
.rstrip("/")] = item
1650 self
._info
_list
.append(item
)
1651 elif len(self
._info
_list
) > 0:
1652 # final crc is in last block
1653 old
= self
._info
_list
[-1]
1655 old
._md
_expect
= item
._md
_expect
1656 old
.compress_size
+= item
.compress_size
1658 # parse new-style comment
1659 if item
.type == RAR_BLOCK_SUB
and item
.filename
== "CMT":
1660 if item
.flags
& (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER
):
1662 elif item
.flags
& RAR_FILE_SOLID
:
1664 cmt
= self
._read
_comment
_v
3(item
, self
._password
)
1665 if len(self
._info
_list
) > 0:
1666 old
= self
._info
_list
[-1]
1670 cmt
= self
._read
_comment
_v
3(item
, self
._password
)
1673 if item
.type == RAR_BLOCK_MAIN
:
1674 if item
.flags
& RAR_MAIN_COMMENT
:
1675 self
.comment
= item
.comment
1676 if item
.flags
& RAR_MAIN_PASSWORD
:
1677 self
._needs
_password
= True
1679 # put file compressed data into temporary .rar archive, and run
1680 # unrar on that, thus avoiding unrar going over whole archive
1681 def _open_hack(self
, inf
, pwd
):
1682 # create main header: crc, type, flags, size, res1, res2
1683 prefix
= RAR_ID
+ S_BLK_HDR
.pack(0x90CF, 0x73, 0, 13) + b
"\0" * (2 + 4)
1684 return self
._open
_hack
_core
(inf
, pwd
, prefix
, b
"")
1691 class Rar5Info(RarInfo
):
1692 """Shared fields for RAR5 records.
1694 extract_version
= 50
1697 header_offset
= None
1704 block_extra_size
= 0
1707 volume_number
= None
1711 def _must_disable_hack(self
):
1715 class Rar5BaseFile(Rar5Info
):
1716 """Shared sturct for file & service record.
1720 file_encryption
= (0, 0, 0, b
"", b
"", b
"")
1721 file_compress_flags
= None
1725 blake2sp_hash
= None
1727 def _must_disable_hack(self
):
1728 if self
.flags
& RAR_FILE_PASSWORD
:
1730 if self
.block_flags
& (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER
):
1732 if self
.file_compress_flags
& RAR5_COMPR_SOLID
:
1739 class Rar5FileInfo(Rar5BaseFile
):
1740 """RAR5 file record.
1742 type = RAR_BLOCK_FILE
1744 def is_symlink(self
):
1745 """Returns True if entry is a symlink."""
1746 # pylint: disable=unsubscriptable-object
1748 self
.file_redir
is not None and
1749 self
.file_redir
[0] in (
1750 RAR5_XREDIR_UNIX_SYMLINK
,
1751 RAR5_XREDIR_WINDOWS_SYMLINK
,
1752 RAR5_XREDIR_WINDOWS_JUNCTION
,
1757 """Returns True if entry is a normal file."""
1758 return not (self
.is_dir() or self
.is_symlink())
1761 """Returns True if entry is a directory."""
1762 if not self
.file_redir
:
1763 if self
.file_flags
& RAR5_FILE_FLAG_ISDIR
:
1768 class Rar5ServiceInfo(Rar5BaseFile
):
1769 """RAR5 service record.
1771 type = RAR_BLOCK_SUB
1774 class Rar5MainInfo(Rar5Info
):
1775 """RAR5 archive main record.
1777 type = RAR_BLOCK_MAIN
1779 main_volume_number
= None
1781 def _must_disable_hack(self
):
1782 if self
.main_flags
& RAR5_MAIN_FLAG_SOLID
:
1787 class Rar5EncryptionInfo(Rar5Info
):
1788 """RAR5 archive header encryption record.
1790 type = RAR5_BLOCK_ENCRYPTION
1791 encryption_algo
= None
1792 encryption_flags
= None
1793 encryption_kdf_count
= None
1794 encryption_salt
= None
1795 encryption_check_value
= None
1797 def needs_password(self
):
1801 class Rar5EndArcInfo(Rar5Info
):
1802 """RAR5 end of archive record.
1804 type = RAR_BLOCK_ENDARC
1808 class RAR5Parser(CommonParser
):
1809 """Parse RAR5 format.
1811 _expect_sig
= RAR5_ID
1814 # AES encrypted headers
1815 _last_aes256_key
= (-1, None, None) # (kdf_count, salt, key)
1817 def _get_utf8_password(self
):
1818 pwd
= self
._password
1819 if isinstance(pwd
, str):
1820 return pwd
.encode("utf8")
1823 def _gen_key(self
, kdf_count
, salt
):
1824 if self
._last
_aes
256_key
[:2] == (kdf_count
, salt
):
1825 return self
._last
_aes
256_key
[2]
1827 raise BadRarFile("Too large kdf_count")
1828 pwd
= self
._get
_utf
8_password
()
1829 key
= pbkdf2_hmac("sha256", pwd
, salt
, 1 << kdf_count
)
1830 self
._last
_aes
256_key
= (kdf_count
, salt
, key
)
1833 def _decrypt_header(self
, fd
):
1834 if not _have_crypto
:
1835 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1836 h
= self
._hdrenc
_main
1837 key
= self
._gen
_key
(h
.encryption_kdf_count
, h
.encryption_salt
)
1839 return HeaderDecrypt(fd
, key
, iv
)
1841 def _parse_block_header(self
, fd
):
1842 """Parse common block header
1844 header_offset
= fd
.tell()
1847 start_bytes
= fd
.read(preload
)
1848 if len(start_bytes
) < preload
:
1849 self
._set
_error
("Unexpected EOF when reading header")
1851 while start_bytes
[-1] & 0x80:
1854 self
._set
_error
("Unexpected EOF when reading header")
1857 header_crc
, pos
= load_le32(start_bytes
, 0)
1858 hdrlen
, pos
= load_vint(start_bytes
, pos
)
1859 if hdrlen
> 2 * 1024 * 1024:
1861 header_size
= pos
+ hdrlen
1863 # read full header, check for EOF
1864 hdata
= start_bytes
+ fd
.read(header_size
- len(start_bytes
))
1865 if len(hdata
) != header_size
:
1866 self
._set
_error
("Unexpected EOF when reading header")
1868 data_offset
= fd
.tell()
1870 calc_crc
= crc32(memoryview(hdata
)[4:])
1871 if header_crc
!= calc_crc
:
1872 # header parsing failed.
1873 self
._set
_error
("Header CRC error: exp=%x got=%x (xlen = %d)",
1874 header_crc
, calc_crc
, len(hdata
))
1877 block_type
, pos
= load_vint(hdata
, pos
)
1879 if block_type
== RAR5_BLOCK_MAIN
:
1880 h
, pos
= self
._parse
_block
_common
(Rar5MainInfo(), hdata
)
1881 h
= self
._parse
_main
_block
(h
, hdata
, pos
)
1882 elif block_type
== RAR5_BLOCK_FILE
:
1883 h
, pos
= self
._parse
_block
_common
(Rar5FileInfo(), hdata
)
1884 h
= self
._parse
_file
_block
(h
, hdata
, pos
)
1885 elif block_type
== RAR5_BLOCK_SERVICE
:
1886 h
, pos
= self
._parse
_block
_common
(Rar5ServiceInfo(), hdata
)
1887 h
= self
._parse
_file
_block
(h
, hdata
, pos
)
1888 elif block_type
== RAR5_BLOCK_ENCRYPTION
:
1889 h
, pos
= self
._parse
_block
_common
(Rar5EncryptionInfo(), hdata
)
1890 h
= self
._parse
_encryption
_block
(h
, hdata
, pos
)
1891 elif block_type
== RAR5_BLOCK_ENDARC
:
1892 h
, pos
= self
._parse
_block
_common
(Rar5EndArcInfo(), hdata
)
1893 h
= self
._parse
_endarc
_block
(h
, hdata
, pos
)
1897 h
.header_offset
= header_offset
1898 h
.data_offset
= data_offset
1901 def _parse_block_common(self
, h
, hdata
):
1902 h
.header_crc
, pos
= load_le32(hdata
, 0)
1903 hdrlen
, pos
= load_vint(hdata
, pos
)
1904 h
.header_size
= hdrlen
+ pos
1905 h
.block_type
, pos
= load_vint(hdata
, pos
)
1906 h
.block_flags
, pos
= load_vint(hdata
, pos
)
1908 if h
.block_flags
& RAR5_BLOCK_FLAG_EXTRA_DATA
:
1909 h
.block_extra_size
, pos
= load_vint(hdata
, pos
)
1910 if h
.block_flags
& RAR5_BLOCK_FLAG_DATA_AREA
:
1911 h
.add_size
, pos
= load_vint(hdata
, pos
)
1913 h
.compress_size
= h
.add_size
1915 if h
.block_flags
& RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN
:
1916 h
.flags |
= RAR_SKIP_IF_UNKNOWN
1917 if h
.block_flags
& RAR5_BLOCK_FLAG_DATA_AREA
:
1918 h
.flags |
= RAR_LONG_BLOCK
1921 def _parse_main_block(self
, h
, hdata
, pos
):
1922 h
.main_flags
, pos
= load_vint(hdata
, pos
)
1923 if h
.main_flags
& RAR5_MAIN_FLAG_HAS_VOLNR
:
1924 h
.main_volume_number
, pos
= load_vint(hdata
, pos
)
1926 h
.flags |
= RAR_MAIN_NEWNUMBERING
1927 if h
.main_flags
& RAR5_MAIN_FLAG_SOLID
:
1928 h
.flags |
= RAR_MAIN_SOLID
1929 if h
.main_flags
& RAR5_MAIN_FLAG_ISVOL
:
1930 h
.flags |
= RAR_MAIN_VOLUME
1931 if h
.main_flags
& RAR5_MAIN_FLAG_RECOVERY
:
1932 h
.flags |
= RAR_MAIN_RECOVERY
1933 if self
._hdrenc
_main
:
1934 h
.flags |
= RAR_MAIN_PASSWORD
1935 if h
.main_flags
& RAR5_MAIN_FLAG_HAS_VOLNR
== 0:
1936 h
.flags |
= RAR_MAIN_FIRSTVOLUME
1940 def _parse_file_block(self
, h
, hdata
, pos
):
1941 h
.file_flags
, pos
= load_vint(hdata
, pos
)
1942 h
.file_size
, pos
= load_vint(hdata
, pos
)
1943 h
.mode
, pos
= load_vint(hdata
, pos
)
1945 if h
.file_flags
& RAR5_FILE_FLAG_HAS_MTIME
:
1946 h
.mtime
, pos
= load_unixtime(hdata
, pos
)
1947 h
.date_time
= h
.mtime
.timetuple()[:6]
1948 if h
.file_flags
& RAR5_FILE_FLAG_HAS_CRC32
:
1949 h
.CRC
, pos
= load_le32(hdata
, pos
)
1950 h
._md
_class
= CRC32Context
1951 h
._md
_expect
= h
.CRC
1953 h
.file_compress_flags
, pos
= load_vint(hdata
, pos
)
1954 h
.file_host_os
, pos
= load_vint(hdata
, pos
)
1955 h
.orig_filename
, pos
= load_vstr(hdata
, pos
)
1956 h
.filename
= h
.orig_filename
.decode("utf8", "replace").rstrip("/")
1958 # use compatible values
1959 if h
.file_host_os
== RAR5_OS_WINDOWS
:
1960 h
.host_os
= RAR_OS_WIN32
1962 h
.host_os
= RAR_OS_UNIX
1963 h
.compress_type
= RAR_M0
+ ((h
.file_compress_flags
>> 7) & 7)
1965 if h
.block_extra_size
:
1966 # allow 1 byte of garbage
1967 while pos
< len(hdata
) - 1:
1968 xsize
, pos
= load_vint(hdata
, pos
)
1969 xdata
, pos
= load_bytes(hdata
, xsize
, pos
)
1970 self
._process
_file
_extra
(h
, xdata
)
1972 if h
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_BEFORE
:
1973 h
.flags |
= RAR_FILE_SPLIT_BEFORE
1974 if h
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_AFTER
:
1975 h
.flags |
= RAR_FILE_SPLIT_AFTER
1976 if h
.file_flags
& RAR5_FILE_FLAG_ISDIR
:
1977 h
.flags |
= RAR_FILE_DIRECTORY
1978 if h
.file_compress_flags
& RAR5_COMPR_SOLID
:
1979 h
.flags |
= RAR_FILE_SOLID
1982 h
.filename
= h
.filename
+ "/"
1985 def _parse_endarc_block(self
, h
, hdata
, pos
):
1986 h
.endarc_flags
, pos
= load_vint(hdata
, pos
)
1987 if h
.endarc_flags
& RAR5_ENDARC_FLAG_NEXT_VOL
:
1988 h
.flags |
= RAR_ENDARC_NEXT_VOLUME
1991 def _check_password(self
, check_value
, kdf_count_shift
, salt
):
1992 if len(check_value
) != RAR5_PW_CHECK_SIZE
+ RAR5_PW_SUM_SIZE
:
1995 hdr_check
= check_value
[:RAR5_PW_CHECK_SIZE
]
1996 hdr_sum
= check_value
[RAR5_PW_CHECK_SIZE
:]
1997 sum_hash
= sha256(hdr_check
).digest()
1998 if sum_hash
[:RAR5_PW_SUM_SIZE
] != hdr_sum
:
2001 kdf_count
= (1 << kdf_count_shift
) + 32
2002 pwd
= self
._get
_utf
8_password
()
2003 pwd_hash
= pbkdf2_hmac("sha256", pwd
, salt
, kdf_count
)
2005 pwd_check
= bytearray(RAR5_PW_CHECK_SIZE
)
2006 len_mask
= RAR5_PW_CHECK_SIZE
- 1
2007 for i
, v
in enumerate(pwd_hash
):
2008 pwd_check
[i
& len_mask
] ^
= v
2010 if pwd_check
!= hdr_check
:
2011 raise RarWrongPassword()
2013 def _parse_encryption_block(self
, h
, hdata
, pos
):
2014 h
.encryption_algo
, pos
= load_vint(hdata
, pos
)
2015 h
.encryption_flags
, pos
= load_vint(hdata
, pos
)
2016 h
.encryption_kdf_count
, pos
= load_byte(hdata
, pos
)
2017 h
.encryption_salt
, pos
= load_bytes(hdata
, 16, pos
)
2018 if h
.encryption_flags
& RAR5_ENC_FLAG_HAS_CHECKVAL
:
2019 h
.encryption_check_value
, pos
= load_bytes(hdata
, 12, pos
)
2020 if h
.encryption_algo
!= RAR5_XENC_CIPHER_AES256
:
2021 raise BadRarFile("Unsupported header encryption cipher")
2022 if h
.encryption_check_value
and self
._password
:
2023 self
._check
_password
(h
.encryption_check_value
, h
.encryption_kdf_count
, h
.encryption_salt
)
2024 self
._hdrenc
_main
= h
2027 def _process_file_extra(self
, h
, xdata
):
2028 xtype
, pos
= load_vint(xdata
, 0)
2029 if xtype
== RAR5_XFILE_TIME
:
2030 self
._parse
_file
_xtime
(h
, xdata
, pos
)
2031 elif xtype
== RAR5_XFILE_ENCRYPTION
:
2032 self
._parse
_file
_encryption
(h
, xdata
, pos
)
2033 elif xtype
== RAR5_XFILE_HASH
:
2034 self
._parse
_file
_hash
(h
, xdata
, pos
)
2035 elif xtype
== RAR5_XFILE_VERSION
:
2036 self
._parse
_file
_version
(h
, xdata
, pos
)
2037 elif xtype
== RAR5_XFILE_REDIR
:
2038 self
._parse
_file
_redir
(h
, xdata
, pos
)
2039 elif xtype
== RAR5_XFILE_OWNER
:
2040 self
._parse
_file
_owner
(h
, xdata
, pos
)
2041 elif xtype
== RAR5_XFILE_SERVICE
:
2046 # extra block for file time record
2047 def _parse_file_xtime(self
, h
, xdata
, pos
):
2048 tflags
, pos
= load_vint(xdata
, pos
)
2050 ldr
= load_windowstime
2051 if tflags
& RAR5_XTIME_UNIXTIME
:
2054 if tflags
& RAR5_XTIME_HAS_MTIME
:
2055 h
.mtime
, pos
= ldr(xdata
, pos
)
2056 h
.date_time
= h
.mtime
.timetuple()[:6]
2057 if tflags
& RAR5_XTIME_HAS_CTIME
:
2058 h
.ctime
, pos
= ldr(xdata
, pos
)
2059 if tflags
& RAR5_XTIME_HAS_ATIME
:
2060 h
.atime
, pos
= ldr(xdata
, pos
)
2062 if tflags
& RAR5_XTIME_UNIXTIME_NS
:
2063 if tflags
& RAR5_XTIME_HAS_MTIME
:
2064 nsec
, pos
= load_le32(xdata
, pos
)
2065 h
.mtime
= to_nsdatetime(h
.mtime
, nsec
)
2066 if tflags
& RAR5_XTIME_HAS_CTIME
:
2067 nsec
, pos
= load_le32(xdata
, pos
)
2068 h
.ctime
= to_nsdatetime(h
.ctime
, nsec
)
2069 if tflags
& RAR5_XTIME_HAS_ATIME
:
2070 nsec
, pos
= load_le32(xdata
, pos
)
2071 h
.atime
= to_nsdatetime(h
.atime
, nsec
)
2073 # just remember encryption info
2074 def _parse_file_encryption(self
, h
, xdata
, pos
):
2075 algo
, pos
= load_vint(xdata
, pos
)
2076 flags
, pos
= load_vint(xdata
, pos
)
2077 kdf_count
, pos
= load_byte(xdata
, pos
)
2078 salt
, pos
= load_bytes(xdata
, 16, pos
)
2079 iv
, pos
= load_bytes(xdata
, 16, pos
)
2081 if flags
& RAR5_XENC_CHECKVAL
:
2082 checkval
, pos
= load_bytes(xdata
, 12, pos
)
2083 if flags
& RAR5_XENC_TWEAKED
:
2085 h
._md
_class
= NoHashContext
2087 h
.file_encryption
= (algo
, flags
, kdf_count
, salt
, iv
, checkval
)
2088 h
.flags |
= RAR_FILE_PASSWORD
2090 def _parse_file_hash(self
, h
, xdata
, pos
):
2091 hash_type
, pos
= load_vint(xdata
, pos
)
2092 if hash_type
== RAR5_XHASH_BLAKE2SP
:
2093 h
.blake2sp_hash
, pos
= load_bytes(xdata
, 32, pos
)
2094 if (h
.file_encryption
[1] & RAR5_XENC_TWEAKED
) == 0:
2095 h
._md
_class
= Blake2SP
2096 h
._md
_expect
= h
.blake2sp_hash
2098 def _parse_file_version(self
, h
, xdata
, pos
):
2099 flags
, pos
= load_vint(xdata
, pos
)
2100 version
, pos
= load_vint(xdata
, pos
)
2101 h
.file_version
= (flags
, version
)
2103 def _parse_file_redir(self
, h
, xdata
, pos
):
2104 redir_type
, pos
= load_vint(xdata
, pos
)
2105 redir_flags
, pos
= load_vint(xdata
, pos
)
2106 redir_name
, pos
= load_vstr(xdata
, pos
)
2107 redir_name
= redir_name
.decode("utf8", "replace")
2108 h
.file_redir
= (redir_type
, redir_flags
, redir_name
)
2110 def _parse_file_owner(self
, h
, xdata
, pos
):
2111 user_name
= group_name
= user_id
= group_id
= None
2113 flags
, pos
= load_vint(xdata
, pos
)
2114 if flags
& RAR5_XOWNER_UNAME
:
2115 user_name
, pos
= load_vstr(xdata
, pos
)
2116 if flags
& RAR5_XOWNER_GNAME
:
2117 group_name
, pos
= load_vstr(xdata
, pos
)
2118 if flags
& RAR5_XOWNER_UID
:
2119 user_id
, pos
= load_vint(xdata
, pos
)
2120 if flags
& RAR5_XOWNER_GID
:
2121 group_id
, pos
= load_vint(xdata
, pos
)
2123 h
.file_owner
= (user_name
, group_name
, user_id
, group_id
)
2125 def process_entry(self
, fd
, item
):
2126 if item
.block_type
== RAR5_BLOCK_FILE
:
2127 if item
.file_version
:
2128 pass # skip old versions
2129 elif (item
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_BEFORE
) == 0:
2130 # use only first part
2131 self
._info
_map
[item
.filename
.rstrip("/")] = item
2132 self
._info
_list
.append(item
)
2133 elif len(self
._info
_list
) > 0:
2134 # final crc is in last block
2135 old
= self
._info
_list
[-1]
2137 old
._md
_expect
= item
._md
_expect
2138 old
.blake2sp_hash
= item
.blake2sp_hash
2139 old
.compress_size
+= item
.compress_size
2140 elif item
.block_type
== RAR5_BLOCK_SERVICE
:
2141 if item
.filename
== "CMT":
2142 self
._load
_comment
(fd
, item
)
2144 def _load_comment(self
, fd
, item
):
2145 if item
.block_flags
& (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER
):
2147 if item
.compress_type
!= RAR_M0
:
2150 if item
.flags
& RAR_FILE_PASSWORD
:
2151 algo
, ___flags
, kdf_count
, salt
, iv
, ___checkval
= item
.file_encryption
2152 if algo
!= RAR5_XENC_CIPHER_AES256
:
2154 key
= self
._gen
_key
(kdf_count
, salt
)
2155 f
= HeaderDecrypt(fd
, key
, iv
)
2156 cmt
= f
.read(item
.file_size
)
2159 with self
._open
_clear
(item
) as cmtstream
:
2160 cmt
= cmtstream
.read()
2162 # rar bug? - appends zero to comment
2163 cmt
= cmt
.split(b
"\0", 1)[0]
2164 self
.comment
= cmt
.decode("utf8")
2167 def _open_hack(self
, inf
, pwd
):
2168 # len, type, blk_flags, flags
2169 main_hdr
= b
"\x03\x01\x00\x00"
2170 endarc_hdr
= b
"\x03\x05\x00\x00"
2171 main_hdr
= S_LONG
.pack(crc32(main_hdr
)) + main_hdr
2172 endarc_hdr
= S_LONG
.pack(crc32(endarc_hdr
)) + endarc_hdr
2173 return self
._open
_hack
_core
(inf
, pwd
, RAR5_ID
+ main_hdr
, endarc_hdr
)
2180 class UnicodeFilename
:
2181 """Handle RAR3 unicode filename decompression.
2183 def __init__(self
, name
, encdata
):
2184 self
.std_name
= bytearray(name
)
2185 self
.encdata
= bytearray(encdata
)
2186 self
.pos
= self
.encpos
= 0
2187 self
.buf
= bytearray()
2191 """Copy encoded byte."""
2193 c
= self
.encdata
[self
.encpos
]
2201 """Copy byte from 8-bit representation."""
2203 return self
.std_name
[self
.pos
]
2208 def put(self
, lo
, hi
):
2209 """Copy 16-bit value to result."""
2215 """Decompress compressed UTF16 value."""
2216 hi
= self
.enc_byte()
2218 while self
.encpos
< len(self
.encdata
):
2220 flags
= self
.enc_byte()
2223 t
= (flags
>> flagbits
) & 3
2225 self
.put(self
.enc_byte(), 0)
2227 self
.put(self
.enc_byte(), hi
)
2229 self
.put(self
.enc_byte(), self
.enc_byte())
2234 for _
in range((n
& 0x7f) + 2):
2235 lo
= (self
.std_byte() + c
) & 0xFF
2238 for _
in range(n
+ 2):
2239 self
.put(self
.std_byte(), 0)
2240 return self
.buf
.decode("utf-16le", "replace")
2243 class RarExtFile(io
.RawIOBase
):
2244 """Base class for file-like object that :meth:`RarFile.open` returns.
2246 Provides public methods and common crc checking.
2249 - no short reads - .read() and .readinfo() read as much as requested.
2250 - no internal buffer, use io.BufferedReader for that.
2252 name
= None #: Filename of the archive entry
2262 def _open_extfile(self
, parser
, inf
):
2263 self
.name
= inf
.filename
2264 self
._parser
= parser
2270 md_class
= NoHashContext
2272 md_class
= self
._inf
._md
_class
or NoHashContext
2273 self
._md
_context
= md_class()
2275 self
._remain
= self
._inf
.file_size
2277 def read(self
, n
=-1):
2278 """Read all or specified amount of data from archive entry."""
2281 if n
is None or n
< 0:
2283 elif n
> self
._remain
:
2292 data
= self
._read
(n
)
2296 self
._md
_context
.update(data
)
2297 self
._remain
-= len(data
)
2299 data
= b
"".join(buf
)
2301 raise BadRarFile("Failed the read enough data: req=%d got=%d" % (orig
, len(data
)))
2304 if not data
or self
._remain
== 0:
2310 """Check final CRC."""
2311 final
= self
._md
_context
.digest()
2312 exp
= self
._inf
._md
_expect
2317 if self
._returncode
:
2318 check_returncode(self
._returncode
, "", tool_setup().get_errmap())
2319 if self
._remain
!= 0:
2320 raise BadRarFile("Failed the read enough data")
2322 raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % (
2323 self
._inf
.filename
, exp
, final
))
2325 def _read(self
, cnt
):
2326 """Actual read that gets sanitized cnt."""
2327 raise NotImplementedError("_read")
2330 """Close open resources."""
2339 """Hook delete to make sure tempfile is removed."""
2342 def readinto(self
, buf
):
2343 """Zero-copy read directly into buffer.
2347 raise NotImplementedError("readinto")
2350 """Return current reading position in uncompressed data."""
2351 return self
._inf
.file_size
- self
._remain
2353 def seek(self
, offset
, whence
=0):
2356 On uncompressed files, the seeking works by actual
2357 seeks so it's fast. On compresses files its slow
2358 - forward seeking happends by reading ahead,
2359 backwards by re-opening and decompressing from the start.
2362 # disable crc check when seeking
2363 if not self
._seeking
:
2364 self
._md
_context
= NoHashContext()
2365 self
._seeking
= True
2367 fsize
= self
._inf
.file_size
2368 cur_ofs
= self
.tell()
2370 if whence
== 0: # seek from beginning of file
2372 elif whence
== 1: # seek from current position
2373 new_ofs
= cur_ofs
+ offset
2374 elif whence
== 2: # seek from end of file
2375 new_ofs
= fsize
+ offset
2377 raise ValueError("Invalid value for whence")
2382 elif new_ofs
> fsize
:
2385 # do the actual seek
2386 if new_ofs
>= cur_ofs
:
2387 self
._skip
(new_ofs
- cur_ofs
)
2390 self
._open
_extfile
(self
._parser
, self
._inf
)
2394 def _skip(self
, cnt
):
2395 """Read and discard data"""
2396 empty_read(self
, cnt
, BSIZE
)
2405 Writing is not supported.
2412 Seeking is supported, although it's slow on compressed files.
2417 """Read all remaining data"""
2418 # avoid RawIOBase default impl
2422 class PipeReader(RarExtFile
):
2423 """Read data from pipe, handle tempfile cleanup."""
2425 def __init__(self
, parser
, inf
, cmd
, tempfile
=None):
2429 self
._tempfile
= tempfile
2430 self
._open
_extfile
(parser
, inf
)
2432 def _close_proc(self
):
2435 for f
in (self
._proc
.stdout
, self
._proc
.stderr
, self
._proc
.stdin
):
2439 self
._returncode
= self
._proc
.returncode
2442 def _open_extfile(self
, parser
, inf
):
2443 super()._open
_extfile
(parser
, inf
)
2448 # launch new process
2449 self
._returncode
= 0
2450 self
._proc
= custom_popen(self
._cmd
)
2451 self
._fd
= self
._proc
.stdout
2453 def _read(self
, cnt
):
2454 """Read from pipe."""
2456 # normal read is usually enough
2457 data
= self
._fd
.read(cnt
)
2458 if len(data
) == cnt
or not data
:
2461 # short read, try looping
2465 data
= self
._fd
.read(cnt
)
2470 return b
"".join(buf
)
2473 """Close open resources."""
2480 os
.unlink(self
._tempfile
)
2483 self
._tempfile
= None
2485 def readinto(self
, buf
):
2486 """Zero-copy read directly into buffer."""
2488 if cnt
> self
._remain
:
2490 vbuf
= memoryview(buf
)
2493 res
= self
._fd
.readinto(vbuf
[got
: cnt
])
2496 self
._md
_context
.update(vbuf
[got
: got
+ res
])
2502 class DirectReader(RarExtFile
):
2503 """Read uncompressed data directly from archive.
2509 def __init__(self
, parser
, inf
):
2511 self
._open
_extfile
(parser
, inf
)
2513 def _open_extfile(self
, parser
, inf
):
2514 super()._open
_extfile
(parser
, inf
)
2516 self
._volfile
= self
._inf
.volume_file
2517 self
._fd
= XFile(self
._volfile
, 0)
2518 self
._fd
.seek(self
._inf
.header_offset
, 0)
2519 self
._cur
= self
._parser
._parse
_header
(self
._fd
)
2520 self
._cur
_avail
= self
._cur
.add_size
2522 def _skip(self
, cnt
):
2523 """RAR Seek, skipping through rar files to get to correct position
2528 if self
._cur
_avail
== 0:
2529 if not self
._open
_next
():
2532 # fd is in read pos, do the read
2533 if cnt
> self
._cur
_avail
:
2534 cnt
-= self
._cur
_avail
2535 self
._remain
-= self
._cur
_avail
2538 self
._fd
.seek(cnt
, 1)
2539 self
._cur
_avail
-= cnt
2543 def _read(self
, cnt
):
2544 """Read from potentially multi-volume archive."""
2546 pos
= self
._fd
.tell()
2547 need
= self
._cur
.data_offset
+ self
._cur
.add_size
- self
._cur
_avail
2549 self
._fd
.seek(need
, 0)
2554 if self
._cur
_avail
== 0:
2555 if not self
._open
_next
():
2558 # fd is in read pos, do the read
2559 if cnt
> self
._cur
_avail
:
2560 data
= self
._fd
.read(self
._cur
_avail
)
2562 data
= self
._fd
.read(cnt
)
2568 self
._cur
_avail
-= len(data
)
2573 return b
"".join(buf
)
2575 def _open_next(self
):
2576 """Proceed to next volume."""
2578 # is the file split over archives?
2579 if (self
._cur
.flags
& RAR_FILE_SPLIT_AFTER
) == 0:
2587 self
._volfile
= self
._parser
._next
_volname
(self
._volfile
)
2588 fd
= open(self
._volfile
, "rb", 0)
2590 sig
= fd
.read(len(self
._parser
._expect
_sig
))
2591 if sig
!= self
._parser
._expect
_sig
:
2592 raise BadRarFile("Invalid signature")
2594 # loop until first file header
2596 cur
= self
._parser
._parse
_header
(fd
)
2598 raise BadRarFile("Unexpected EOF")
2599 if cur
.type in (RAR_BLOCK_MARK
, RAR_BLOCK_MAIN
):
2601 fd
.seek(cur
.add_size
, 1)
2603 if cur
.orig_filename
!= self
._inf
.orig_filename
:
2604 raise BadRarFile("Did not found file entry")
2606 self
._cur
_avail
= cur
.add_size
2609 def readinto(self
, buf
):
2610 """Zero-copy read directly into buffer."""
2612 vbuf
= memoryview(buf
)
2613 while got
< len(buf
):
2615 if self
._cur
_avail
== 0:
2616 if not self
._open
_next
():
2619 # length for next read
2620 cnt
= len(buf
) - got
2621 if cnt
> self
._cur
_avail
:
2622 cnt
= self
._cur
_avail
2624 # read into temp view
2625 res
= self
._fd
.readinto(vbuf
[got
: got
+ cnt
])
2628 self
._md
_context
.update(vbuf
[got
: got
+ res
])
2629 self
._cur
_avail
-= res
2635 class HeaderDecrypt
:
2636 """File-like object that decrypts from another file"""
2637 def __init__(self
, f
, key
, iv
):
2639 self
.ciph
= AES_CBC_Decrypt(key
, iv
)
2643 """Current file pos - works only on block boundaries."""
2644 return self
.f
.tell()
2646 def read(self
, cnt
=None):
2647 """Read and decrypt."""
2649 raise BadRarFile("Bad count to header decrypt - wrong password?")
2652 if cnt
<= len(self
.buf
):
2653 res
= self
.buf
[:cnt
]
2654 self
.buf
= self
.buf
[cnt
:]
2663 enc
= self
.f
.read(blklen
)
2664 if len(enc
) < blklen
:
2666 dec
= self
.ciph
.decrypt(enc
)
2672 self
.buf
= dec
[cnt
:]
2679 """Input may be filename or file object.
2681 __slots__
= ("_fd", "_need_close")
2683 def __init__(self
, xfile
, bufsize
=1024):
2684 if is_filelike(xfile
):
2685 self
._need
_close
= False
2689 self
._need
_close
= True
2690 self
._fd
= open(xfile
, "rb", bufsize
)
2692 def read(self
, n
=None):
2693 """Read from file."""
2694 return self
._fd
.read(n
)
2697 """Return file pos."""
2698 return self
._fd
.tell()
2700 def seek(self
, ofs
, whence
=0):
2701 """Move file pos."""
2702 return self
._fd
.seek(ofs
, whence
)
2704 def readinto(self
, buf
):
2705 """Read into buffer."""
2706 return self
._fd
.readinto(buf
)
2709 """Close file object."""
2710 if self
._need
_close
:
2713 def __enter__(self
):
2716 def __exit__(self
, typ
, val
, tb
):
2720 class NoHashContext
:
2721 """No-op hash function."""
2722 def __init__(self
, data
=None):
2724 def update(self
, data
):
2728 def hexdigest(self
):
2729 """Hexadecimal digest."""
2733 """Hash context that uses CRC32."""
2734 __slots__
= ["_crc"]
2736 def __init__(self
, data
=None):
2741 def update(self
, data
):
2743 self
._crc
= crc32(data
, self
._crc
)
2749 def hexdigest(self
):
2750 """Hexadecimal digest."""
2751 return "%08x" % self
.digest()
2755 """Blake2sp hash context.
2757 __slots__
= ["_thread", "_buf", "_cur", "_digest"]
2762 def __init__(self
, data
=None):
2768 for i
in range(self
.parallelism
):
2769 ctx
= self
._blake
2s
(i
, 0, i
== (self
.parallelism
- 1))
2770 self
._thread
.append(ctx
)
2775 def _blake2s(self
, ofs
, depth
, is_last
):
2776 return blake2s(node_offset
=ofs
, node_depth
=depth
, last_node
=is_last
,
2777 depth
=2, inner_size
=32, fanout
=self
.parallelism
)
2779 def _add_block(self
, blk
):
2780 self
._thread
[self
._cur
].update(blk
)
2781 self
._cur
= (self
._cur
+ 1) % self
.parallelism
2783 def update(self
, data
):
2786 view
= memoryview(data
)
2787 bs
= self
.block_size
2789 need
= bs
- len(self
._buf
)
2790 if len(view
) < need
:
2791 self
._buf
+= view
.tobytes()
2793 self
._add
_block
(self
._buf
+ view
[:need
].tobytes())
2795 while len(view
) >= bs
:
2796 self
._add
_block
(view
[:bs
])
2798 self
._buf
= view
.tobytes()
2801 """Return final digest value.
2803 if self
._digest
is None:
2805 self
._add
_block
(self
._buf
)
2807 ctx
= self
._blake
2s
(0, 1, True)
2808 for t
in self
._thread
:
2809 ctx
.update(t
.digest())
2810 self
._digest
= ctx
.digest()
2813 def hexdigest(self
):
2814 """Hexadecimal digest."""
2815 return hexlify(self
.digest()).decode("ascii")
2819 """Emulate buggy SHA1 from RAR3.
2824 _BLK_BE
= struct
.Struct(b
">16L")
2825 _BLK_LE
= struct
.Struct(b
"<16L")
2827 __slots__
= ("_nbytes", "_md", "_rarbug")
2829 def __init__(self
, data
=b
"", rarbug
=False):
2832 self
._rarbug
= rarbug
2835 def update(self
, data
):
2836 """Process more data."""
2837 self
._md
.update(data
)
2838 bufpos
= self
._nbytes
& 63
2839 self
._nbytes
+= len(data
)
2841 if self
._rarbug
and len(data
) > 64:
2842 dpos
= self
.block_size
- bufpos
2843 while dpos
+ self
.block_size
<= len(data
):
2844 self
._corrupt
(data
, dpos
)
2845 dpos
+= self
.block_size
2848 """Return final state."""
2849 return self
._md
.digest()
2851 def hexdigest(self
):
2852 """Return final state as hex string."""
2853 return self
._md
.hexdigest()
2855 def _corrupt(self
, data
, dpos
):
2856 """Corruption from SHA1 core."""
2857 ws
= list(self
._BLK
_BE
.unpack_from(data
, dpos
))
2858 for t
in range(16, 80):
2859 tmp
= ws
[(t
- 3) & 15] ^ ws
[(t
- 8) & 15] ^ ws
[(t
- 14) & 15] ^ ws
[(t
- 16) & 15]
2860 ws
[t
& 15] = ((tmp
<< 1) |
(tmp
>> (32 - 1))) & 0xFFFFFFFF
2861 self
._BLK
_LE
.pack_into(data
, dpos
, *ws
)
2865 ## Utility functions
2868 S_LONG
= Struct("<L")
2869 S_SHORT
= Struct("<H")
2870 S_BYTE
= Struct("<B")
2872 S_BLK_HDR
= Struct("<HBHH")
2873 S_FILE_HDR
= Struct("<LLBLLBBHL")
2874 S_COMMENT_HDR
= Struct("<HBBH")
2877 def load_vint(buf
, pos
):
2878 """Load RAR5 variable-size int."""
2879 limit
= min(pos
+ 11, len(buf
))
2883 res
+= ((b
& 0x7F) << ofs
)
2888 raise BadRarFile("cannot load vint")
2891 def load_byte(buf
, pos
):
2892 """Load single byte"""
2895 raise BadRarFile("cannot load byte")
2896 return S_BYTE
.unpack_from(buf
, pos
)[0], end
2899 def load_le32(buf
, pos
):
2900 """Load little-endian 32-bit integer"""
2903 raise BadRarFile("cannot load le32")
2904 return S_LONG
.unpack_from(buf
, pos
)[0], end
2907 def load_bytes(buf
, num
, pos
):
2908 """Load sequence of bytes"""
2911 raise BadRarFile("cannot load bytes")
2912 return buf
[pos
: end
], end
2915 def load_vstr(buf
, pos
):
2916 """Load bytes prefixed by vint length"""
2917 slen
, pos
= load_vint(buf
, pos
)
2918 return load_bytes(buf
, slen
, pos
)
2921 def load_dostime(buf
, pos
):
2922 """Load LE32 dos timestamp"""
2923 stamp
, pos
= load_le32(buf
, pos
)
2924 tup
= parse_dos_time(stamp
)
2925 return to_datetime(tup
), pos
2928 def load_unixtime(buf
, pos
):
2929 """Load LE32 unix timestamp"""
2930 secs
, pos
= load_le32(buf
, pos
)
2931 dt
= datetime
.fromtimestamp(secs
, timezone
.utc
)
2935 def load_windowstime(buf
, pos
):
2936 """Load LE64 windows timestamp"""
2937 # unix epoch (1970) in seconds from windows epoch (1601)
2938 unix_epoch
= 11644473600
2939 val1
, pos
= load_le32(buf
, pos
)
2940 val2
, pos
= load_le32(buf
, pos
)
2941 secs
, n1secs
= divmod((val2
<< 32) | val1
, 10000000)
2942 dt
= datetime
.fromtimestamp(secs
- unix_epoch
, timezone
.utc
)
2943 dt
= to_nsdatetime(dt
, n1secs
* 100)
2951 _rc_num
= re
.compile('^[0-9]+$')
2954 def _next_newvol(volfile
):
2955 """New-style next volume
2957 name
, ext
= os
.path
.splitext(volfile
)
2958 if ext
.lower() in ("", ".exe", ".sfx"):
2959 volfile
= name
+ ".rar"
2960 i
= len(volfile
) - 1
2962 if "0" <= volfile
[i
] <= "9":
2963 return _inc_volname(volfile
, i
, False)
2964 if volfile
[i
] in ("/", os
.sep
):
2967 raise BadRarName("Cannot construct volume name: " + volfile
)
2971 def _next_oldvol(volfile
):
2972 """Old-style next volume
2974 name
, ext
= os
.path
.splitext(volfile
)
2975 if ext
.lower() in ("", ".exe", ".sfx"):
2978 if _rc_num
.match(sfx
):
2979 ext
= _inc_volname(ext
, len(ext
) - 1, True)
2982 ext
= ext
[:2] + "00"
2986 def _inc_volname(volfile
, i
, inc_chars
):
2987 """increase digits with carry, otherwise just increment char
2996 elif "0" <= fn
[i
] < "9" or inc_chars
:
2997 fn
[i
] = chr(ord(fn
[i
]) + 1)
3000 fn
.insert(i
+ 1, "1")
3005 def _parse_ext_time(h
, data
, pos
):
3006 """Parse all RAR3 extended time fields
3008 # flags and rest of data can be missing
3010 if pos
+ 2 <= len(data
):
3011 flags
= S_SHORT
.unpack_from(data
, pos
)[0]
3014 mtime
, pos
= _parse_xtime(flags
>> 3 * 4, data
, pos
, h
.mtime
)
3015 h
.ctime
, pos
= _parse_xtime(flags
>> 2 * 4, data
, pos
)
3016 h
.atime
, pos
= _parse_xtime(flags
>> 1 * 4, data
, pos
)
3017 h
.arctime
, pos
= _parse_xtime(flags
>> 0 * 4, data
, pos
)
3020 h
.date_time
= mtime
.timetuple()[:6]
3024 def _parse_xtime(flag
, data
, pos
, basetime
=None):
3025 """Parse one RAR3 extended time field
3030 basetime
, pos
= load_dostime(data
, pos
)
3032 # load second fractions of 100ns units
3035 for _
in range(cnt
):
3036 b
, pos
= load_byte(data
, pos
)
3037 rem
= (b
<< 16) |
(rem
>> 8)
3039 # dostime has room for 30 seconds only, correct if needed
3040 if flag
& 4 and basetime
.second
< 59:
3041 basetime
= basetime
.replace(second
=basetime
.second
+ 1)
3043 res
= to_nsdatetime(basetime
, rem
* 100)
3047 def is_filelike(obj
):
3048 """Filename or file object?
3050 if isinstance(obj
, (bytes
, str, Path
)):
3053 for a
in ("read", "tell", "seek"):
3054 res
= res
and hasattr(obj
, a
)
3056 raise ValueError("Invalid object passed as file")
3060 def rar3_s2k(pwd
, salt
):
3061 """String-to-key hash for RAR3.
3063 if not isinstance(pwd
, str):
3064 pwd
= pwd
.decode("utf8")
3065 seed
= bytearray(pwd
.encode("utf-16le") + salt
)
3066 h
= Rar3Sha1(rarbug
=True)
3069 for j
in range(0x4000):
3070 cnt
= S_LONG
.pack(i
* 0x4000 + j
)
3074 iv
+= h
.digest()[19:20]
3075 key_be
= h
.digest()[:16]
3076 key_le
= pack("<LLLL", *unpack(">LLLL", key_be
))
3080 def rar3_decompress(vers
, meth
, data
, declen
=0, flags
=0, crc
=0, pwd
=None, salt
=None):
3081 """Decompress blob of compressed data.
3083 Used for data with non-standard header - eg. comments.
3085 # already uncompressed?
3086 if meth
== RAR_M0
and (flags
& RAR_FILE_PASSWORD
) == 0:
3089 # take only necessary flags
3090 flags
= flags
& (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK
)
3091 flags |
= RAR_LONG_BLOCK
3095 date
= ((2010 - 1980) << 25) + (12 << 21) + (31 << 16)
3096 mode
= DOS_MODE_ARCHIVE
3097 fhdr
= S_FILE_HDR
.pack(len(data
), declen
, RAR_OS_MSDOS
, crc
,
3098 date
, vers
, meth
, len(fname
), mode
)
3104 hlen
= S_BLK_HDR
.size
+ len(fhdr
)
3105 hdr
= S_BLK_HDR
.pack(0, RAR_BLOCK_FILE
, flags
, hlen
) + fhdr
3106 hcrc
= crc32(hdr
[2:]) & 0xFFFF
3107 hdr
= S_BLK_HDR
.pack(hcrc
, RAR_BLOCK_FILE
, flags
, hlen
) + fhdr
3109 # archive main header
3110 mh
= S_BLK_HDR
.pack(0x90CF, RAR_BLOCK_MAIN
, 0, 13) + b
"\0" * (2 + 4)
3112 # decompress via temp rar
3113 setup
= tool_setup()
3114 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
3115 tmpf
= os
.fdopen(tmpfd
, "wb")
3117 tmpf
.write(RAR_ID
+ mh
+ hdr
+ data
)
3120 curpwd
= (flags
& RAR_FILE_PASSWORD
) and pwd
or None
3121 cmd
= setup
.open_cmdline(curpwd
, tmpname
)
3122 p
= custom_popen(cmd
)
3123 return p
.communicate()[0]
3129 def sanitize_filename(fname
, pathsep
, is_win32
):
3130 """Simulate unrar sanitization.
3133 if len(fname
) > 1 and fname
[1] == ":":
3135 rc
= RC_BAD_CHARS_WIN32
3137 rc
= RC_BAD_CHARS_UNIX
3138 if rc
.search(fname
):
3139 fname
= rc
.sub("_", fname
)
3142 for seg
in fname
.split("/"):
3143 if seg
in ("", ".", ".."):
3145 if is_win32
and seg
[-1] in (" ", "."):
3146 seg
= seg
[:-1] + "_"
3148 return pathsep
.join(parts
)
3151 def empty_read(src
, size
, blklen
):
3152 """Read and drop fixed amount of data.
3156 res
= src
.read(blklen
)
3158 res
= src
.read(size
)
3160 raise BadRarFile("cannot load data")
3165 """Convert 6-part time tuple into datetime object.
3168 year
, mon
, day
, h
, m
, s
= t
3170 # assume the values are valid
3172 return datetime(year
, mon
, day
, h
, m
, s
)
3176 # sanitize invalid values
3177 mday
= (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
3178 mon
= max(1, min(mon
, 12))
3179 day
= max(1, min(day
, mday
[mon
]))
3183 return datetime(year
, mon
, day
, h
, m
, s
)
3186 def parse_dos_time(stamp
):
3187 """Parse standard 32-bit DOS timestamp.
3189 sec
, stamp
= stamp
& 0x1F, stamp
>> 5
3190 mn
, stamp
= stamp
& 0x3F, stamp
>> 6
3191 hr
, stamp
= stamp
& 0x1F, stamp
>> 5
3192 day
, stamp
= stamp
& 0x1F, stamp
>> 5
3193 mon
, stamp
= stamp
& 0x0F, stamp
>> 4
3194 yr
= (stamp
& 0x7F) + 1980
3195 return (yr
, mon
, day
, hr
, mn
, sec
* 2)
3198 # pylint: disable=arguments-differ,signature-differs
3199 class nsdatetime(datetime
):
3200 """Datetime that carries nanoseconds.
3202 Arithmetic not supported, will lose nanoseconds.
3204 .. versionadded:: 4.0
3206 __slots__
= ("nanosecond",)
3207 nanosecond
: int #: Number of nanoseconds, 0 <= nanosecond < 999999999
3209 def __new__(cls
, year
, month
=None, day
=None, hour
=0, minute
=0, second
=0,
3210 microsecond
=0, tzinfo
=None, *, fold
=0, nanosecond
=0):
3211 usec
, mod
= divmod(nanosecond
, 1000) if nanosecond
else (microsecond
, 0)
3213 return datetime(year
, month
, day
, hour
, minute
, second
, usec
, tzinfo
, fold
=fold
)
3214 self
= super().__new
__(cls
, year
, month
, day
, hour
, minute
, second
, usec
, tzinfo
, fold
=fold
)
3215 self
.nanosecond
= nanosecond
3218 def isoformat(self
, sep
="T", timespec
="auto"):
3219 """Formats with nanosecond precision by default.
3221 if timespec
== "auto":
3222 pre
, post
= super().isoformat(sep
, "microseconds").split(".", 1)
3223 return f
"{pre}.{self.nanosecond:09d}{post[6:]}"
3224 return super().isoformat(sep
, timespec
)
3226 def astimezone(self
, tz
=None):
3227 """Convert to new timezone.
3229 tmp
= super().astimezone(tz
)
3230 return self
.__class
__(tmp
.year
, tmp
.month
, tmp
.day
, tmp
.hour
, tmp
.minute
, tmp
.second
,
3231 nanosecond
=self
.nanosecond
, tzinfo
=tmp
.tzinfo
, fold
=tmp
.fold
)
3233 def replace(self
, year
=None, month
=None, day
=None, hour
=None, minute
=None, second
=None,
3234 microsecond
=None, tzinfo
=None, *, fold
=None, nanosecond
=None):
3235 """Return new timestamp with specified fields replaced.
3237 return self
.__class
__(
3238 self
.year
if year
is None else year
,
3239 self
.month
if month
is None else month
,
3240 self
.day
if day
is None else day
,
3241 self
.hour
if hour
is None else hour
,
3242 self
.minute
if minute
is None else minute
,
3243 self
.second
if second
is None else second
,
3244 nanosecond
=((self
.nanosecond
if microsecond
is None else microsecond
* 1000)
3245 if nanosecond
is None else nanosecond
),
3246 tzinfo
=self
.tzinfo
if tzinfo
is None else tzinfo
,
3247 fold
=self
.fold
if fold
is None else fold
)
3250 return hash((super().__hash
__(), self
.nanosecond
)) if self
.nanosecond
else super().__hash
__()
3252 def __eq__(self
, other
):
3253 return super().__eq
__(other
) and self
.nanosecond
== (
3254 other
.nanosecond
if isinstance(other
, nsdatetime
) else other
.microsecond
* 1000)
3256 def __gt__(self
, other
):
3257 return super().__gt
__(other
) or (super().__eq
__(other
) and self
.nanosecond
> (
3258 other
.nanosecond
if isinstance(other
, nsdatetime
) else other
.microsecond
* 1000))
3260 def __lt__(self
, other
):
3261 return not (self
> other
or self
== other
)
3263 def __ge__(self
, other
):
3264 return not self
< other
3266 def __le__(self
, other
):
3267 return not self
> other
3269 def __ne__(self
, other
):
3270 return not self
== other
3273 def to_nsdatetime(dt
, nsec
):
3274 """Apply nanoseconds to datetime.
3278 return nsdatetime(dt
.year
, dt
.month
, dt
.day
, dt
.hour
, dt
.minute
, dt
.second
,
3279 tzinfo
=dt
.tzinfo
, fold
=dt
.fold
, nanosecond
=nsec
)
3283 """Convert datatime instance to nanoseconds.
3285 secs
= int(dt
.timestamp())
3286 nsecs
= dt
.nanosecond
if isinstance(dt
, nsdatetime
) else dt
.microsecond
* 1000
3287 return secs
* 1000000000 + nsecs
3290 def custom_popen(cmd
):
3291 """Disconnect cmd from parent fds, read only from stdout.
3293 creationflags
= 0x08000000 if WIN32
else 0 # CREATE_NO_WINDOW
3295 p
= Popen(cmd
, bufsize
=0, stdout
=PIPE
, stderr
=STDOUT
, stdin
=DEVNULL
,
3296 creationflags
=creationflags
)
3297 except OSError as ex
:
3298 if ex
.errno
== errno
.ENOENT
:
3299 raise RarCannotExec("Unrar not installed?") from None
3300 if ex
.errno
== errno
.EACCES
or ex
.errno
== errno
.EPERM
:
3301 raise RarCannotExec("Cannot execute unrar") from None
3306 def check_returncode(code
, out
, errmap
):
3307 """Raise exception according to unrar exit code.
3312 if code
> 0 and code
< len(errmap
):
3319 exc
= RarUnknownError
3323 msg
= "%s [%d]: %s" % (exc
.__doc
__, code
, out
)
3325 msg
= "%s [%d]" % (exc
.__doc
__, code
)
3330 def membuf_tempfile(memfile
):
3331 """Write in-memory file object to real file.
3335 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
3336 tmpf
= os
.fdopen(tmpfd
, "wb")
3339 shutil
.copyfileobj(memfile
, tmpf
, BSIZE
)
3341 except BaseException
:
3349 # Find working command-line tool
3353 def __init__(self
, setup
):
3357 cmdline
= self
.get_cmdline("check_cmd", None)
3359 p
= custom_popen(cmdline
)
3360 out
, _
= p
.communicate()
3361 return p
.returncode
== 0
3362 except RarCannotExec
:
3365 def open_cmdline(self
, pwd
, rarfn
, filefn
=None):
3366 cmdline
= self
.get_cmdline("open_cmd", pwd
)
3367 cmdline
.append(rarfn
)
3369 self
.add_file_arg(cmdline
, filefn
)
3372 def get_errmap(self
):
3373 return self
.setup
["errmap"]
3375 def get_cmdline(self
, key
, pwd
, nodash
=False):
3376 cmdline
= list(self
.setup
[key
])
3377 cmdline
[0] = globals()[cmdline
[0]]
3378 if key
== "check_cmd":
3380 self
.add_password_arg(cmdline
, pwd
)
3382 cmdline
.append("--")
3385 def add_file_arg(self
, cmdline
, filename
):
3386 cmdline
.append(filename
)
3388 def add_password_arg(self
, cmdline
, pwd
):
3389 """Append password switch to commandline.
3392 if not isinstance(pwd
, str):
3393 pwd
= pwd
.decode("utf8")
3394 args
= self
.setup
["password"]
3396 tool
= self
.setup
["open_cmd"][0]
3397 raise RarCannotExec(f
"{tool} does not support passwords")
3398 elif isinstance(args
, str):
3399 cmdline
.append(args
+ pwd
)
3401 cmdline
.extend(args
)
3404 cmdline
.extend(self
.setup
["no_password"])
3408 "open_cmd": ("UNRAR_TOOL", "p", "-inul"),
3409 "check_cmd": ("UNRAR_TOOL", "-inul", "-?"),
3411 "no_password": ("-p-",),
3412 # map return code to exception class, codes from rar.txt
3414 RarWarning
, RarFatalError
, RarCRCError
, RarLockedArchiveError
, # 1..4
3415 RarWriteError
, RarOpenError
, RarUserError
, RarMemoryError
, # 5..8
3416 RarCreateError
, RarNoFilesError
, RarWrongPassword
] # 9..11
3419 # Problems with unar RAR backend:
3420 # - Does not support RAR2 locked files [fails to read]
3421 # - Does not support RAR5 Blake2sp hash [reading works]
3423 "open_cmd": ("UNAR_TOOL", "-q", "-o", "-"),
3424 "check_cmd": ("UNAR_TOOL", "-version"),
3425 "password": ("-p",),
3426 "no_password": ("-p", ""),
3430 # Problems with libarchive RAR backend:
3431 # - Does not support solid archives.
3432 # - Does not support password-protected archives.
3433 # - Does not support RARVM-based compression filters.
3435 "open_cmd": ("BSDTAR_TOOL", "-x", "--to-stdout", "-f"),
3436 "check_cmd": ("BSDTAR_TOOL", "--version"),
3443 "open_cmd": ("SEVENZIP_TOOL", "e", "-so", "-bb0"),
3444 "check_cmd": ("SEVENZIP_TOOL", "i"),
3446 "no_password": ("-p",),
3448 RarWarning
, RarFatalError
, None, None, # 1..4
3449 None, None, RarUserError
, RarMemoryError
] # 5..8
3452 SEVENZIP2_CONFIG
= {
3453 "open_cmd": ("SEVENZIP2_TOOL", "e", "-so", "-bb0"),
3454 "check_cmd": ("SEVENZIP2_TOOL", "i"),
3456 "no_password": ("-p",),
3458 RarWarning
, RarFatalError
, None, None, # 1..4
3459 None, None, RarUserError
, RarMemoryError
] # 5..8
3462 CURRENT_SETUP
= None
3465 def tool_setup(unrar
=True, unar
=True, bsdtar
=True, sevenzip
=True, sevenzip2
=True, force
=False):
3466 """Pick a tool, return cached ToolSetup.
3468 global CURRENT_SETUP
3470 CURRENT_SETUP
= None
3471 if CURRENT_SETUP
is not None:
3472 return CURRENT_SETUP
3475 lst
.append(UNRAR_CONFIG
)
3477 lst
.append(UNAR_CONFIG
)
3479 lst
.append(SEVENZIP_CONFIG
)
3481 lst
.append(SEVENZIP2_CONFIG
)
3483 lst
.append(BSDTAR_CONFIG
)
3486 setup
= ToolSetup(conf
)
3488 CURRENT_SETUP
= setup
3490 if CURRENT_SETUP
is None:
3491 raise RarCannotExec("Cannot find working tool")
3492 return CURRENT_SETUP
3496 """Minimal command-line interface for rarfile module.
3499 p
= argparse
.ArgumentParser(description
=main
.__doc
__)
3500 g
= p
.add_mutually_exclusive_group(required
=True)
3501 g
.add_argument("-l", "--list", metavar
="<rarfile>",
3502 help="Show archive listing")
3503 g
.add_argument("-e", "--extract", nargs
=2,
3504 metavar
=("<rarfile>", "<output_dir>"),
3505 help="Extract archive into target dir")
3506 g
.add_argument("-t", "--test", metavar
="<rarfile>",
3507 help="Test if a archive is valid")
3508 cmd
= p
.parse_args(args
)
3511 with
RarFile(cmd
.list) as rf
:
3514 with
RarFile(cmd
.test
) as rf
:
3517 with
RarFile(cmd
.extract
[0]) as rf
:
3518 rf
.extractall(cmd
.extract
[1])
3521 if __name__
== "__main__":