3 # Copyright (c) 2005-2019 Marko Kreen <markokr@gmail.com>
5 # Permission to use, copy, modify, and/or distribute this software for any
6 # purpose with or without fee is hereby granted, provided that the above
7 # copyright notice and this permission notice appear in all copies.
9 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 r
"""RAR archive reader.
19 This is Python module for Rar archive reading. The interface
20 is made as :mod:`zipfile`-like as possible.
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.
54 ## Imports and compat - support various crypto options
62 from struct
import pack
, unpack
, Struct
63 from binascii
import crc32
as rar_crc32
, hexlify
64 from tempfile
import mkstemp
65 from subprocess
import Popen
, PIPE
, STDOUT
66 from io
import RawIOBase
, BytesIO
67 from hashlib
import sha1
, sha256
, blake2s
69 from datetime
import datetime
, timedelta
, timezone
70 from pathlib
import Path
72 # only needed for encrypted headers
75 from cryptography
.hazmat
.primitives
.ciphers
import algorithms
, modes
, Cipher
76 from cryptography
.hazmat
.backends
import default_backend
77 from cryptography
.hazmat
.primitives
import hashes
78 from cryptography
.hazmat
.primitives
.kdf
import pbkdf2
80 class AES_CBC_Decrypt(object):
82 def __init__(self
, key
, iv
):
83 ciph
= Cipher(algorithms
.AES(key
), modes
.CBC(iv
), default_backend())
84 self
.decrypt
= ciph
.decryptor().update
86 def pbkdf2_sha256(password
, salt
, iters
):
87 """PBKDF2 with HMAC-SHA256"""
88 ctx
= pbkdf2
.PBKDF2HMAC(hashes
.SHA256(), 32, salt
, iters
, default_backend())
89 return ctx
.derive(password
)
92 from Crypto
.Cipher
import AES
93 from Crypto
.Protocol
import KDF
95 class AES_CBC_Decrypt(object):
97 def __init__(self
, key
, iv
):
98 self
.decrypt
= AES
.new(key
, AES
.MODE_CBC
, iv
).decrypt
100 def pbkdf2_sha256(password
, salt
, iters
):
101 """PBKDF2 with HMAC-SHA256"""
102 return KDF
.PBKDF2(password
, salt
, 32, iters
, hmac_sha256
)
110 """Return hex string."""
111 return hexlify(data
).decode("ascii")
116 # export only interesting items
117 __all__
= ["is_rarfile", "is_rarfile_sfx", "RarInfo", "RarFile", "RarExtFile"]
120 ## Module configuration. Can be tuned after importing.
124 #: executable for unrar tool
127 #: executable for unar tool
131 #: executable for bsdtar tool
132 BSDTAR_TOOL
= "bsdtar"
134 #: default fallback charset
135 DEFAULT_CHARSET
= "windows-1252"
137 #: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
138 TRY_ENCODINGS
= ("utf8", "utf-16le")
140 #: whether to speed up decompression by using tmp archive
143 #: limit the filesize for tmp archive usage
144 HACK_SIZE_LIMIT
= 20 * 1024 * 1024
146 #: set specific directory for mkstemp() used by hack dir usage
149 #: Separator for path name components. RAR internally uses "\\".
150 #: Use "/" to be similar with zipfile.
158 RAR_BLOCK_MARK
= 0x72 # r
159 RAR_BLOCK_MAIN
= 0x73 # s
160 RAR_BLOCK_FILE
= 0x74 # t
161 RAR_BLOCK_OLD_COMMENT
= 0x75 # u
162 RAR_BLOCK_OLD_EXTRA
= 0x76 # v
163 RAR_BLOCK_OLD_SUB
= 0x77 # w
164 RAR_BLOCK_OLD_RECOVERY
= 0x78 # x
165 RAR_BLOCK_OLD_AUTH
= 0x79 # y
166 RAR_BLOCK_SUB
= 0x7a # z
167 RAR_BLOCK_ENDARC
= 0x7b # {
169 # flags for RAR_BLOCK_MAIN
170 RAR_MAIN_VOLUME
= 0x0001
171 RAR_MAIN_COMMENT
= 0x0002
172 RAR_MAIN_LOCK
= 0x0004
173 RAR_MAIN_SOLID
= 0x0008
174 RAR_MAIN_NEWNUMBERING
= 0x0010
175 RAR_MAIN_AUTH
= 0x0020
176 RAR_MAIN_RECOVERY
= 0x0040
177 RAR_MAIN_PASSWORD
= 0x0080
178 RAR_MAIN_FIRSTVOLUME
= 0x0100
179 RAR_MAIN_ENCRYPTVER
= 0x0200
181 # flags for RAR_BLOCK_FILE
182 RAR_FILE_SPLIT_BEFORE
= 0x0001
183 RAR_FILE_SPLIT_AFTER
= 0x0002
184 RAR_FILE_PASSWORD
= 0x0004
185 RAR_FILE_COMMENT
= 0x0008
186 RAR_FILE_SOLID
= 0x0010
187 RAR_FILE_DICTMASK
= 0x00e0
188 RAR_FILE_DICT64
= 0x0000
189 RAR_FILE_DICT128
= 0x0020
190 RAR_FILE_DICT256
= 0x0040
191 RAR_FILE_DICT512
= 0x0060
192 RAR_FILE_DICT1024
= 0x0080
193 RAR_FILE_DICT2048
= 0x00a0
194 RAR_FILE_DICT4096
= 0x00c0
195 RAR_FILE_DIRECTORY
= 0x00e0
196 RAR_FILE_LARGE
= 0x0100
197 RAR_FILE_UNICODE
= 0x0200
198 RAR_FILE_SALT
= 0x0400
199 RAR_FILE_VERSION
= 0x0800
200 RAR_FILE_EXTTIME
= 0x1000
201 RAR_FILE_EXTFLAGS
= 0x2000
203 # flags for RAR_BLOCK_ENDARC
204 RAR_ENDARC_NEXT_VOLUME
= 0x0001
205 RAR_ENDARC_DATACRC
= 0x0002
206 RAR_ENDARC_REVSPACE
= 0x0004
207 RAR_ENDARC_VOLNR
= 0x0008
209 # flags common to all blocks
210 RAR_SKIP_IF_UNKNOWN
= 0x4000
211 RAR_LONG_BLOCK
= 0x8000
221 # Compression methods - "0".."5"
235 RAR5_BLOCK_SERVICE
= 3
236 RAR5_BLOCK_ENCRYPTION
= 4
237 RAR5_BLOCK_ENDARC
= 5
239 RAR5_BLOCK_FLAG_EXTRA_DATA
= 0x01
240 RAR5_BLOCK_FLAG_DATA_AREA
= 0x02
241 RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN
= 0x04
242 RAR5_BLOCK_FLAG_SPLIT_BEFORE
= 0x08
243 RAR5_BLOCK_FLAG_SPLIT_AFTER
= 0x10
244 RAR5_BLOCK_FLAG_DEPENDS_PREV
= 0x20
245 RAR5_BLOCK_FLAG_KEEP_WITH_PARENT
= 0x40
247 RAR5_MAIN_FLAG_ISVOL
= 0x01
248 RAR5_MAIN_FLAG_HAS_VOLNR
= 0x02
249 RAR5_MAIN_FLAG_SOLID
= 0x04
250 RAR5_MAIN_FLAG_RECOVERY
= 0x08
251 RAR5_MAIN_FLAG_LOCKED
= 0x10
253 RAR5_FILE_FLAG_ISDIR
= 0x01
254 RAR5_FILE_FLAG_HAS_MTIME
= 0x02
255 RAR5_FILE_FLAG_HAS_CRC32
= 0x04
256 RAR5_FILE_FLAG_UNKNOWN_SIZE
= 0x08
258 RAR5_COMPR_SOLID
= 0x40
260 RAR5_ENC_FLAG_HAS_CHECKVAL
= 0x01
262 RAR5_ENDARC_FLAG_NEXT_VOL
= 0x01
264 RAR5_XFILE_ENCRYPTION
= 1
267 RAR5_XFILE_VERSION
= 4
270 RAR5_XFILE_SERVICE
= 7
272 RAR5_XTIME_UNIXTIME
= 0x01
273 RAR5_XTIME_HAS_MTIME
= 0x02
274 RAR5_XTIME_HAS_CTIME
= 0x04
275 RAR5_XTIME_HAS_ATIME
= 0x08
277 RAR5_XENC_CIPHER_AES256
= 0
279 RAR5_XENC_CHECKVAL
= 0x01
280 RAR5_XENC_TWEAKED
= 0x02
282 RAR5_XHASH_BLAKE2SP
= 0
284 RAR5_XREDIR_UNIX_SYMLINK
= 1
285 RAR5_XREDIR_WINDOWS_SYMLINK
= 2
286 RAR5_XREDIR_WINDOWS_JUNCTION
= 3
287 RAR5_XREDIR_HARD_LINK
= 4
288 RAR5_XREDIR_FILE_COPY
= 5
290 RAR5_XREDIR_ISDIR
= 0x01
292 RAR5_XOWNER_UNAME
= 0x01
293 RAR5_XOWNER_GNAME
= 0x02
294 RAR5_XOWNER_UID
= 0x04
295 RAR5_XOWNER_GID
= 0x08
301 ## internal constants
304 RAR_ID
= b
"Rar!\x1a\x07\x00"
305 RAR5_ID
= b
"Rar!\x1a\x07\x01\x00"
308 UTC
= timezone(timedelta(0), "UTC")
311 SFX_MAX_SIZE
= 2 * 1024 * 1024
315 def _get_rar_version(xfile
):
316 """Check quickly whether file is rar archive.
318 with
XFile(xfile
) as fd
:
319 buf
= fd
.read(len(RAR5_ID
))
320 if buf
.startswith(RAR_ID
):
322 elif buf
.startswith(RAR5_ID
):
326 def _find_sfx_header(xfile
):
329 steps
= (64, SFX_MAX_SIZE
)
331 with
XFile(xfile
) as fd
:
337 curdata
= buf
.getvalue()
340 pos
= curdata
.find(sig
, findpos
)
343 if curdata
[pos
:pos
+len(RAR_ID
)] == RAR_ID
:
345 if curdata
[pos
:pos
+len(RAR5_ID
)] == RAR5_ID
:
347 findpos
= pos
+ len(sig
)
354 def is_rarfile(xfile
):
355 """Check quickly whether file is rar archive.
357 return _get_rar_version(xfile
) > 0
359 def is_rarfile_sfx(xfile
):
360 """Check whether file is rar archive with support for SFX.
362 It will read 2M from file.
364 return _find_sfx_header(xfile
)[0] > 0
366 class Error(Exception):
367 """Base class for rarfile errors."""
369 class BadRarFile(Error
):
370 """Incorrect data in archive."""
372 class NotRarFile(Error
):
373 """The file is not RAR archive."""
375 class BadRarName(Error
):
376 """Cannot guess multipart name components."""
378 class NoRarEntry(Error
):
379 """File not found in RAR"""
381 class PasswordRequired(Error
):
382 """File requires password"""
384 class NeedFirstVolume(Error
):
385 """Need to start from first volume."""
387 class NoCrypto(Error
):
388 """Cannot parse encrypted headers - no crypto available."""
390 class RarExecError(Error
):
391 """Problem reported by unrar/rar."""
393 class RarWarning(RarExecError
):
394 """Non-fatal error"""
396 class RarFatalError(RarExecError
):
399 class RarCRCError(RarExecError
):
400 """CRC error during unpacking"""
402 class RarLockedArchiveError(RarExecError
):
403 """Must not modify locked archive"""
405 class RarWriteError(RarExecError
):
408 class RarOpenError(RarExecError
):
411 class RarUserError(RarExecError
):
414 class RarMemoryError(RarExecError
):
417 class RarCreateError(RarExecError
):
420 class RarNoFilesError(RarExecError
):
421 """No files that match pattern were found"""
423 class RarUserBreak(RarExecError
):
426 class RarWrongPassword(RarExecError
):
427 """Incorrect password"""
429 class RarUnknownError(RarExecError
):
430 """Unknown exit code"""
432 class RarSignalExit(RarExecError
):
433 """Unrar exited with signal"""
435 class RarCannotExec(RarExecError
):
436 """Executable not found."""
439 class RarInfo(object):
440 r
"""An entry in rar archive.
442 RAR3 extended timestamps are :class:`datetime.datetime` objects without timezone.
443 RAR5 extended timestamps are :class:`datetime.datetime` objects with UTC timezone.
448 File name with relative path.
449 Path separator is "/". Always unicode string.
452 File modification timestamp. As tuple of (year, month, day, hour, minute, second).
453 RAR5 allows archives where it is missing, it's None then.
462 Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants.
465 Minimal Rar version needed for decompressing. As (major*10 + minor),
470 RAR5 does not have such field in archive, it's simply set to 50.
473 Host OS type, one of RAR_OS_* constants.
475 RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`,
476 :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`.
478 RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`.
481 File attributes. May be either dos-style or unix-style, depending on host_os.
484 File modification time. Same value as :attr:`date_time`
485 but as :class:`datetime.datetime` object with extended precision.
488 Optional time field: creation time. As :class:`datetime.datetime` object.
491 Optional time field: last access time. As :class:`datetime.datetime` object.
494 Optional time field: archival time. As :class:`datetime.datetime` object.
498 CRC-32 of uncompressed file, unsigned int.
503 Blake2SP hash over decompressed data. (RAR5-only)
506 Optional file comment field. Unicode string. (RAR3-only)
509 If not None, file is link of some sort. Contains tuple of (type, flags, target).
512 Type is one of constants:
514 :data:`RAR5_XREDIR_UNIX_SYMLINK`
515 unix symlink to target.
516 :data:`RAR5_XREDIR_WINDOWS_SYMLINK`
517 windows symlink to target.
518 :data:`RAR5_XREDIR_WINDOWS_JUNCTION`
520 :data:`RAR5_XREDIR_HARD_LINK`
522 :data:`RAR5_XREDIR_FILE_COPY`
523 current file is copy of another archive entry.
525 Flags may contain :data:`RAR5_XREDIR_ISDIR` bit.
528 Volume nr, starting from 0.
531 Volume file name, where file starts.
535 # zipfile-compatible fields
545 # optional extended time fields, datetime() objects.
550 extract_version
= None
568 """Returns True if entry is a directory.
570 if self
.type == RAR_BLOCK_FILE
:
571 return (self
.flags
& RAR_FILE_DIRECTORY
) == RAR_FILE_DIRECTORY
574 def needs_password(self
):
575 """Returns True if data is stored password-protected.
577 if self
.type == RAR_BLOCK_FILE
:
578 return (self
.flags
& RAR_FILE_PASSWORD
) > 0
582 class RarFile(object):
583 """Parse RAR structure, provide access to files in archive.
586 #: Archive comment. Unicode string or None.
589 def __init__(self
, rarfile
, mode
="r", charset
=None, info_callback
=None,
590 crc_check
=True, errors
="stop"):
591 """Open and parse a RAR archive.
598 only "r" is supported.
600 fallback charset to use, if filenames are not already Unicode-enabled.
602 debug callback, gets to see all archive entries.
604 set to False to disable CRC checks
606 Either "stop" to quietly stop parsing on errors,
607 or "strict" to raise errors. Default is "stop".
609 if isinstance(rarfile
, Path
):
610 self
._rarfile
= str(rarfile
)
612 self
._rarfile
= rarfile
614 self
._charset
= charset
or DEFAULT_CHARSET
615 self
._info
_callback
= info_callback
616 self
._crc
_check
= crc_check
617 self
._password
= None
618 self
._file
_parser
= None
622 elif errors
== "strict":
625 raise ValueError("Invalid value for errors= parameter.")
628 raise NotImplementedError("RarFile supports only mode=r")
636 def __exit__(self
, typ
, value
, traceback
):
640 def setpassword(self
, password
):
641 """Sets the password to use when extracting.
643 self
._password
= password
644 if self
._file
_parser
:
645 if self
._file
_parser
.has_header_encryption():
646 self
._file
_parser
= None
647 if not self
._file
_parser
:
650 self
._file
_parser
.setpassword(self
._password
)
652 def needs_password(self
):
653 """Returns True if any archive entries require password for extraction.
655 return self
._file
_parser
.needs_password()
658 """Return list of filenames in archive.
660 return [f
.filename
for f
in self
.infolist()]
663 """Return RarInfo objects for all files/directories in archive.
665 return self
._file
_parser
.infolist()
667 def volumelist(self
):
668 """Returns filenames of archive volumes.
670 In case of single-volume archive, the list contains
671 just the name of main archive file.
673 return self
._file
_parser
.volumelist()
675 def getinfo(self
, fname
):
676 """Return RarInfo for file.
678 return self
._file
_parser
.getinfo(fname
)
680 def open(self
, fname
, mode
="r", psw
=None):
681 """Returns file-like object (:class:`RarExtFile`) from where the data can be read.
683 The object implements :class:`io.RawIOBase` interface, so it can
684 be further wrapped with :class:`io.BufferedReader`
685 and :class:`io.TextIOWrapper`.
687 On older Python where io module is not available, it implements
688 only .read(), .seek(), .tell() and .close() methods.
690 The object is seekable, although the seeking is fast only on
691 uncompressed files, on compressed files the seeking is implemented
692 by reading ahead and/or restarting the decompression.
697 file name or RarInfo instance.
701 password to use for extracting.
705 raise NotImplementedError("RarFile.open() supports only mode=r")
708 inf
= self
.getinfo(fname
)
710 raise TypeError("Directory does not have any data: " + inf
.filename
)
713 if inf
.needs_password():
714 psw
= psw
or self
._password
716 raise PasswordRequired("File %s requires password" % inf
.filename
)
720 return self
._file
_parser
.open(inf
, psw
)
722 def read(self
, fname
, psw
=None):
723 """Return uncompressed data for archive entry.
725 For longer files using :meth:`RarFile.open` may be better idea.
730 filename or RarInfo instance
732 password to use for extracting.
735 with self
.open(fname
, "r", psw
) as f
:
739 """Release open resources."""
743 """Print archive file list to stdout."""
744 for f
in self
.infolist():
747 def extract(self
, member
, path
=None, pwd
=None):
748 """Extract single file into current directory.
753 filename or :class:`RarInfo` instance
755 optional destination path
757 optional password to use
759 if isinstance(member
, RarInfo
):
760 fname
= member
.filename
761 elif isinstance(member
, Path
):
765 self
._extract
([fname
], path
, pwd
)
767 def extractall(self
, path
=None, members
=None, pwd
=None):
768 """Extract all files into current directory.
773 optional destination path
775 optional filename or :class:`RarInfo` instance list to extract
777 optional password to use
780 if members
is not None:
782 if isinstance(m
, RarInfo
):
783 fnlist
.append(m
.filename
)
786 self
._extract
(fnlist
, path
, pwd
)
789 """Let "unrar" test the archive.
792 with
XTempFile(self
._rarfile
) as rarfile
:
793 cmd
= setup
.test_cmdline(self
._password
, rarfile
)
794 p
= custom_popen(cmd
)
795 output
= p
.communicate()[0]
796 check_returncode(p
, output
)
799 """Return error string if parsing failed or None if no problems.
801 if not self
._file
_parser
:
802 return "Not a RAR file"
803 return self
._file
_parser
.strerror()
810 ver
, sfx_ofs
= _find_sfx_header(self
._rarfile
)
812 p3
= RAR3Parser(self
._rarfile
, self
._password
, self
._crc
_check
,
813 self
._charset
, self
._strict
, self
._info
_callback
,
815 self
._file
_parser
= p3
# noqa
817 p5
= RAR5Parser(self
._rarfile
, self
._password
, self
._crc
_check
,
818 self
._charset
, self
._strict
, self
._info
_callback
,
820 self
._file
_parser
= p5
# noqa
822 raise BadRarFile("Not a RAR file")
824 self
._file
_parser
.parse()
825 self
.comment
= self
._file
_parser
.comment
827 # call unrar to extract a file
828 def _extract(self
, fnlist
, path
=None, psw
=None):
831 if os
.sep
!= PATH_SEP
:
832 fnlist
= [fn
.replace(PATH_SEP
, os
.sep
) for fn
in fnlist
]
834 if path
and isinstance(path
, Path
):
837 psw
= psw
or self
._password
840 with
XTempFile(self
._rarfile
) as rarfn
:
841 cmd
= setup
.extract_cmdline(psw
, rarfn
, fnlist
, path
)
844 p
= custom_popen(cmd
)
845 output
= p
.communicate()[0]
846 check_returncode(p
, output
)
849 # File format parsing
852 class CommonParser(object):
853 """Shared parser parts."""
856 _needs_password
= False
863 def __init__(self
, rarfile
, password
, crc_check
, charset
, strict
, info_cb
, sfx_offset
):
864 self
._rarfile
= rarfile
865 self
._password
= password
866 self
._crc
_check
= crc_check
867 self
._charset
= charset
868 self
._strict
= strict
869 self
._info
_callback
= info_cb
873 self
._sfx
_offset
= sfx_offset
875 def has_header_encryption(self
):
876 """Returns True if headers are encrypted
878 if self
._hdrenc
_main
:
881 if self
._main
.flags
& RAR_MAIN_PASSWORD
:
885 def setpassword(self
, psw
):
886 """Set cached password."""
889 def volumelist(self
):
891 return self
._vol
_list
893 def needs_password(self
):
894 """Is password required"""
895 return self
._needs
_password
899 return self
._parse
_error
902 """List of RarInfo records.
904 return self
._info
_list
906 def getinfo(self
, member
):
907 """Return RarInfo for filename
909 if isinstance(member
, RarInfo
):
910 fname
= member
.filename
911 elif isinstance(member
, Path
):
916 # accept both ways here
918 fname2
= fname
.replace("\\", "/")
920 fname2
= fname
.replace("/", "\\")
923 return self
._info
_map
[fname
]
926 return self
._info
_map
[fname2
]
928 raise NoRarEntry("No such file: %s" % fname
)
941 def _parse_real(self
):
942 fd
= XFile(self
._rarfile
)
944 fd
.seek(self
._sfx
_offset
, 0)
945 sig
= fd
.read(len(self
._expect
_sig
))
946 if sig
!= self
._expect
_sig
:
947 if isinstance(self
._rarfile
, str):
948 raise NotRarFile("Not a Rar archive: {}".format(self
._rarfile
))
949 raise NotRarFile("Not a Rar archive")
951 volume
= 0 # first vol (.rar) is 0
954 volfile
= self
._rarfile
955 self
._vol
_list
= [self
._rarfile
]
958 h
= None # don"t read past ENDARC
960 h
= self
._parse
_header
(fd
)
966 volfile
= self
._next
_volname
(volfile
)
969 self
._set
_error
("Cannot open next volume: %s", volfile
)
972 sig
= fd
.read(len(self
._expect
_sig
))
973 if sig
!= self
._expect
_sig
:
974 self
._set
_error
("Invalid volume sig: %s", volfile
)
978 self
._vol
_list
.append(volfile
)
983 h
.volume_file
= volfile
985 if h
.type == RAR_BLOCK_MAIN
and not self
._main
:
987 if volume
== 0 and (h
.flags
& RAR_MAIN_NEWNUMBERING
):
988 # RAR 2.x does not set FIRSTVOLUME,
989 # so check it only if NEWNUMBERING is used
990 if (h
.flags
& RAR_MAIN_FIRSTVOLUME
) == 0:
991 if getattr(h
, "main_volume_number", None) is not None:
992 # rar5 may have more info
993 raise NeedFirstVolume(
994 "Need to start from first volume (current: %r)"
995 % (h
.main_volume_number
,)
997 raise NeedFirstVolume("Need to start from first volume")
998 if h
.flags
& RAR_MAIN_PASSWORD
:
999 self
._needs
_password
= True
1000 if not self
._password
:
1002 elif h
.type == RAR_BLOCK_ENDARC
:
1003 more_vols
= (h
.flags
& RAR_ENDARC_NEXT_VOLUME
) > 0
1005 elif h
.type == RAR_BLOCK_FILE
:
1006 # RAR 2.x does not write RAR_BLOCK_ENDARC
1007 if h
.flags
& RAR_FILE_SPLIT_AFTER
:
1009 # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
1010 if volume
== 0 and h
.flags
& RAR_FILE_SPLIT_BEFORE
:
1011 raise NeedFirstVolume("Need to start from first volume")
1013 if h
.needs_password():
1014 self
._needs
_password
= True
1017 self
.process_entry(fd
, h
)
1019 if self
._info
_callback
:
1020 self
._info
_callback
(h
)
1024 fd
.seek(h
.data_offset
+ h
.add_size
, 0)
1026 def process_entry(self
, fd
, item
):
1027 """Examine item, add into lookup cache."""
1028 raise NotImplementedError()
1030 def _decrypt_header(self
, fd
):
1031 raise NotImplementedError("_decrypt_header")
1033 def _parse_block_header(self
, fd
):
1034 raise NotImplementedError("_parse_block_header")
1036 def _open_hack(self
, inf
, psw
):
1037 raise NotImplementedError("_open_hack")
1039 # read single header
1040 def _parse_header(self
, fd
):
1042 # handle encrypted headers
1043 if (self
._main
and self
._main
.flags
& RAR_MAIN_PASSWORD
) or self
._hdrenc
_main
:
1044 if not self
._password
:
1046 fd
= self
._decrypt
_header
(fd
)
1048 # now read actual header
1049 return self
._parse
_block
_header
(fd
)
1050 except struct
.error
:
1051 self
._set
_error
("Broken header in RAR file")
1054 # given current vol name, construct next one
1055 def _next_volname(self
, volfile
):
1056 if is_filelike(volfile
):
1057 raise IOError("Working on single FD")
1058 if self
._main
.flags
& RAR_MAIN_NEWNUMBERING
:
1059 return _next_newvol(volfile
)
1060 return _next_oldvol(volfile
)
1062 def _set_error(self
, msg
, *args
):
1065 self
._parse
_error
= msg
1067 raise BadRarFile(msg
)
1069 def open(self
, inf
, psw
):
1070 """Return stream object for file data."""
1073 # cannot leave to unrar as it expects copied file to exist
1074 if inf
.file_redir
[0] in (RAR5_XREDIR_FILE_COPY
, RAR5_XREDIR_HARD_LINK
):
1075 inf
= self
.getinfo(inf
.file_redir
[2])
1077 raise BadRarFile("cannot find copied file")
1079 if inf
.flags
& RAR_FILE_SPLIT_BEFORE
:
1080 raise NeedFirstVolume("Partial file, please start from first volume: " + inf
.filename
)
1082 # is temp write usable?
1086 elif self
._main
._must
_disable
_hack
():
1088 elif inf
._must
_disable
_hack
():
1090 elif is_filelike(self
._rarfile
):
1092 elif inf
.file_size
> HACK_SIZE_LIMIT
:
1094 elif not USE_EXTRACT_HACK
:
1098 if inf
.compress_type
== RAR_M0
and (inf
.flags
& RAR_FILE_PASSWORD
) == 0 and inf
.file_redir
is None:
1099 return self
._open
_clear
(inf
)
1101 return self
._open
_hack
(inf
, psw
)
1102 elif is_filelike(self
._rarfile
):
1103 return self
._open
_unrar
_membuf
(self
._rarfile
, inf
, psw
)
1105 return self
._open
_unrar
(self
._rarfile
, inf
, psw
)
1107 def _open_clear(self
, inf
):
1108 return DirectReader(self
, inf
)
1110 def _open_hack_core(self
, inf
, psw
, prefix
, suffix
):
1112 size
= inf
.compress_size
+ inf
.header_size
1113 rf
= XFile(inf
.volume_file
, 0)
1114 rf
.seek(inf
.header_offset
)
1116 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
1117 tmpf
= os
.fdopen(tmpfd
, "wb")
1123 buf
= rf
.read(BSIZE
)
1127 raise BadRarFile("read failed: " + inf
.filename
)
1139 return self
._open
_unrar
(tmpname
, inf
, psw
, tmpname
)
1141 # write in-memory archive to temp file - needed for solid archives
1142 def _open_unrar_membuf(self
, memfile
, inf
, psw
):
1143 tmpname
= membuf_tempfile(memfile
)
1144 return self
._open
_unrar
(tmpname
, inf
, psw
, tmpname
, force_file
=True)
1146 # extract using unrar
1147 def _open_unrar(self
, rarfile
, inf
, psw
=None, tmpfile
=None, force_file
=False):
1148 setup
= tool_setup()
1150 # not giving filename avoids encoding related problems
1152 if not tmpfile
or force_file
:
1154 if PATH_SEP
!= os
.sep
:
1155 fn
= fn
.replace(PATH_SEP
, os
.sep
)
1157 # read from unrar pipe
1158 cmd
= setup
.open_cmdline(psw
, rarfile
, fn
)
1159 return PipeReader(self
, inf
, cmd
, tmpfile
)
1165 class Rar3Info(RarInfo
):
1166 """RAR3 specific fields."""
1167 extract_version
= 15
1172 header_offset
= None
1177 # make sure some rar5 fields are always present
1179 blake2sp_hash
= None
1181 def _must_disable_hack(self
):
1182 if self
.type == RAR_BLOCK_FILE
:
1183 if self
.flags
& RAR_FILE_PASSWORD
:
1185 elif self
.flags
& (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER
):
1187 elif self
.type == RAR_BLOCK_MAIN
:
1188 if self
.flags
& (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD
):
1193 class RAR3Parser(CommonParser
):
1194 """Parse RAR3 file format.
1196 _expect_sig
= RAR_ID
1197 _last_aes_key
= (None, None, None) # (salt, key, iv)
1199 def _decrypt_header(self
, fd
):
1200 if not _have_crypto
:
1201 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1203 if self
._last
_aes
_key
[0] == salt
:
1204 key
, iv
= self
._last
_aes
_key
[1:]
1206 key
, iv
= rar3_s2k(self
._password
, salt
)
1207 self
._last
_aes
_key
= (salt
, key
, iv
)
1208 return HeaderDecrypt(fd
, key
, iv
)
1211 def _parse_block_header(self
, fd
):
1213 h
.header_offset
= fd
.tell()
1215 # read and parse base header
1216 buf
= fd
.read(S_BLK_HDR
.size
)
1219 t
= S_BLK_HDR
.unpack_from(buf
)
1220 h
.header_crc
, h
.type, h
.flags
, h
.header_size
= t
1223 if h
.header_size
> S_BLK_HDR
.size
:
1224 hdata
= buf
+ fd
.read(h
.header_size
- S_BLK_HDR
.size
)
1227 h
.data_offset
= fd
.tell()
1230 if len(hdata
) != h
.header_size
:
1231 self
._set
_error
("Unexpected EOF when reading header")
1234 pos
= S_BLK_HDR
.size
1236 # block has data assiciated with it?
1237 if h
.flags
& RAR_LONG_BLOCK
:
1238 h
.add_size
, pos
= load_le32(hdata
, pos
)
1242 # parse interesting ones, decide header boundaries for crc
1243 if h
.type == RAR_BLOCK_MARK
:
1245 elif h
.type == RAR_BLOCK_MAIN
:
1247 if h
.flags
& RAR_MAIN_ENCRYPTVER
:
1250 if h
.flags
& RAR_MAIN_COMMENT
:
1251 self
._parse
_subblocks
(h
, hdata
, pos
)
1252 elif h
.type == RAR_BLOCK_FILE
:
1253 pos
= self
._parse
_file
_header
(h
, hdata
, pos
- 4)
1255 if h
.flags
& RAR_FILE_COMMENT
:
1256 pos
= self
._parse
_subblocks
(h
, hdata
, pos
)
1257 elif h
.type == RAR_BLOCK_SUB
:
1258 pos
= self
._parse
_file
_header
(h
, hdata
, pos
- 4)
1259 crc_pos
= h
.header_size
1260 elif h
.type == RAR_BLOCK_OLD_AUTH
:
1263 elif h
.type == RAR_BLOCK_OLD_EXTRA
:
1267 crc_pos
= h
.header_size
1270 if h
.type == RAR_BLOCK_OLD_SUB
:
1271 crcdat
= hdata
[2:] + fd
.read(h
.add_size
)
1273 crcdat
= hdata
[2:crc_pos
]
1275 calc_crc
= rar_crc32(crcdat
) & 0xFFFF
1277 # return good header
1278 if h
.header_crc
== calc_crc
:
1281 # header parsing failed.
1282 self
._set
_error
("Header CRC error (%02x): exp=%x got=%x (xlen = %d)",
1283 h
.type, h
.header_crc
, calc_crc
, len(crcdat
))
1285 # instead panicing, send eof
1288 # read file-specific header
1289 def _parse_file_header(self
, h
, hdata
, pos
):
1290 fld
= S_FILE_HDR
.unpack_from(hdata
, pos
)
1291 pos
+= S_FILE_HDR
.size
1293 h
.compress_size
= fld
[0]
1294 h
.file_size
= fld
[1]
1297 h
.date_time
= parse_dos_time(fld
[4])
1298 h
.mtime
= to_datetime(h
.date_time
)
1299 h
.extract_version
= fld
[5]
1300 h
.compress_type
= fld
[6]
1304 h
._md
_class
= CRC32Context
1305 h
._md
_expect
= h
.CRC
1307 if h
.flags
& RAR_FILE_LARGE
:
1308 h1
, pos
= load_le32(hdata
, pos
)
1309 h2
, pos
= load_le32(hdata
, pos
)
1310 h
.compress_size |
= h1
<< 32
1311 h
.file_size |
= h2
<< 32
1312 h
.add_size
= h
.compress_size
1314 name
, pos
= load_bytes(hdata
, name_size
, pos
)
1315 if h
.flags
& RAR_FILE_UNICODE
:
1316 nul
= name
.find(ZERO
)
1317 h
.orig_filename
= name
[:nul
]
1318 u
= UnicodeFilename(h
.orig_filename
, name
[nul
+ 1:])
1319 h
.filename
= u
.decode()
1321 # if parsing failed fall back to simple name
1323 h
.filename
= self
._decode
(h
.orig_filename
)
1325 h
.orig_filename
= name
1326 h
.filename
= self
._decode
(name
)
1328 # change separator, if requested
1329 if PATH_SEP
!= "\\":
1330 h
.filename
= h
.filename
.replace("\\", PATH_SEP
)
1332 if h
.flags
& RAR_FILE_SALT
:
1333 h
.salt
, pos
= load_bytes(hdata
, 8, pos
)
1337 # optional extended time stamps
1338 if h
.flags
& RAR_FILE_EXTTIME
:
1339 pos
= _parse_ext_time(h
, hdata
, pos
)
1341 h
.mtime
= h
.atime
= h
.ctime
= h
.arctime
= None
1345 # find old-style comment subblock
1346 def _parse_subblocks(self
, h
, hdata
, pos
):
1347 while pos
< len(hdata
):
1348 # ordinary block header
1349 t
= S_BLK_HDR
.unpack_from(hdata
, pos
)
1350 ___scrc
, stype
, sflags
, slen
= t
1351 pos_next
= pos
+ slen
1352 pos
+= S_BLK_HDR
.size
1358 # followed by block-specific header
1359 if stype
== RAR_BLOCK_OLD_COMMENT
and pos
+ S_COMMENT_HDR
.size
<= pos_next
:
1360 declen
, ver
, meth
, crc
= S_COMMENT_HDR
.unpack_from(hdata
, pos
)
1361 pos
+= S_COMMENT_HDR
.size
1362 data
= hdata
[pos
: pos_next
]
1363 cmt
= rar3_decompress(ver
, meth
, data
, declen
, sflags
,
1364 crc
, self
._password
)
1365 if not self
._crc
_check
:
1366 h
.comment
= self
._decode
_comment
(cmt
)
1367 elif rar_crc32(cmt
) & 0xFFFF == crc
:
1368 h
.comment
= self
._decode
_comment
(cmt
)
1373 def _read_comment_v3(self
, inf
, psw
=None):
1376 with
XFile(inf
.volume_file
) as rf
:
1377 rf
.seek(inf
.data_offset
)
1378 data
= rf
.read(inf
.compress_size
)
1381 cmt
= rar3_decompress(inf
.extract_version
, inf
.compress_type
, data
,
1382 inf
.file_size
, inf
.flags
, inf
.CRC
, psw
, inf
.salt
)
1386 crc
= rar_crc32(cmt
)
1390 return self
._decode
_comment
(cmt
)
1392 def _decode(self
, val
):
1393 for c
in TRY_ENCODINGS
:
1395 return val
.decode(c
)
1396 except UnicodeError:
1398 return val
.decode(self
._charset
, "replace")
1400 def _decode_comment(self
, val
):
1401 return self
._decode
(val
)
1403 def process_entry(self
, fd
, item
):
1404 if item
.type == RAR_BLOCK_FILE
:
1405 # use only first part
1406 if (item
.flags
& RAR_FILE_SPLIT_BEFORE
) == 0:
1407 self
._info
_map
[item
.filename
] = item
1408 self
._info
_list
.append(item
)
1409 elif len(self
._info
_list
) > 0:
1410 # final crc is in last block
1411 old
= self
._info
_list
[-1]
1413 old
._md
_expect
= item
._md
_expect
1414 old
.compress_size
+= item
.compress_size
1416 # parse new-style comment
1417 if item
.type == RAR_BLOCK_SUB
and item
.filename
== "CMT":
1418 if item
.flags
& (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER
):
1420 elif item
.flags
& RAR_FILE_SOLID
:
1422 cmt
= self
._read
_comment
_v
3(item
, self
._password
)
1423 if len(self
._info
_list
) > 0:
1424 old
= self
._info
_list
[-1]
1428 cmt
= self
._read
_comment
_v
3(item
, self
._password
)
1431 if item
.type == RAR_BLOCK_MAIN
:
1432 if item
.flags
& RAR_MAIN_COMMENT
:
1433 self
.comment
= item
.comment
1434 if item
.flags
& RAR_MAIN_PASSWORD
:
1435 self
._needs
_password
= True
1437 # put file compressed data into temporary .rar archive, and run
1438 # unrar on that, thus avoiding unrar going over whole archive
1439 def _open_hack(self
, inf
, psw
):
1440 # create main header: crc, type, flags, size, res1, res2
1441 prefix
= RAR_ID
+ S_BLK_HDR
.pack(0x90CF, 0x73, 0, 13) + ZERO
* (2 + 4)
1442 return self
._open
_hack
_core
(inf
, psw
, prefix
, EMPTY
)
1448 class Rar5Info(RarInfo
):
1449 """Shared fields for RAR5 records.
1451 extract_version
= 50
1454 header_offset
= None
1461 block_extra_size
= 0
1464 volume_number
= None
1468 def _must_disable_hack(self
):
1472 class Rar5BaseFile(Rar5Info
):
1473 """Shared sturct for file & service record.
1477 file_encryption
= (0, 0, 0, EMPTY
, EMPTY
, EMPTY
)
1478 file_compress_flags
= None
1482 blake2sp_hash
= None
1484 def _must_disable_hack(self
):
1485 if self
.flags
& RAR_FILE_PASSWORD
:
1487 if self
.block_flags
& (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER
):
1489 if self
.file_compress_flags
& RAR5_COMPR_SOLID
:
1496 class Rar5FileInfo(Rar5BaseFile
):
1497 """RAR5 file record.
1499 type = RAR_BLOCK_FILE
1502 class Rar5ServiceInfo(Rar5BaseFile
):
1503 """RAR5 service record.
1505 type = RAR_BLOCK_SUB
1508 class Rar5MainInfo(Rar5Info
):
1509 """RAR5 archive main record.
1511 type = RAR_BLOCK_MAIN
1513 main_volume_number
= None
1515 def _must_disable_hack(self
):
1516 if self
.main_flags
& RAR5_MAIN_FLAG_SOLID
:
1521 class Rar5EncryptionInfo(Rar5Info
):
1522 """RAR5 archive header encryption record.
1524 type = RAR5_BLOCK_ENCRYPTION
1525 encryption_algo
= None
1526 encryption_flags
= None
1527 encryption_kdf_count
= None
1528 encryption_salt
= None
1529 encryption_check_value
= None
1531 def needs_password(self
):
1535 class Rar5EndArcInfo(Rar5Info
):
1536 """RAR5 end of archive record.
1538 type = RAR_BLOCK_ENDARC
1542 class RAR5Parser(CommonParser
):
1543 """Parse RAR5 format.
1545 _expect_sig
= RAR5_ID
1548 # AES encrypted headers
1549 _last_aes256_key
= (-1, None, None) # (kdf_count, salt, key)
1551 def _gen_key(self
, kdf_count
, salt
):
1552 if self
._last
_aes
256_key
[:2] == (kdf_count
, salt
):
1553 return self
._last
_aes
256_key
[2]
1555 raise BadRarFile("Too large kdf_count")
1556 psw
= self
._password
1557 if isinstance(psw
, str):
1558 psw
= psw
.encode("utf8")
1559 key
= pbkdf2_sha256(psw
, salt
, 1 << kdf_count
)
1560 self
._last
_aes
256_key
= (kdf_count
, salt
, key
)
1563 def _decrypt_header(self
, fd
):
1564 if not _have_crypto
:
1565 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1566 h
= self
._hdrenc
_main
1567 key
= self
._gen
_key
(h
.encryption_kdf_count
, h
.encryption_salt
)
1569 return HeaderDecrypt(fd
, key
, iv
)
1572 def _parse_block_header(self
, fd
):
1573 header_offset
= fd
.tell()
1576 start_bytes
= fd
.read(preload
)
1577 header_crc
, pos
= load_le32(start_bytes
, 0)
1578 hdrlen
, pos
= load_vint(start_bytes
, pos
)
1579 if hdrlen
> 2 * 1024 * 1024:
1581 header_size
= pos
+ hdrlen
1583 # read full header, check for EOF
1584 hdata
= start_bytes
+ fd
.read(header_size
- len(start_bytes
))
1585 if len(hdata
) != header_size
:
1586 self
._set
_error
("Unexpected EOF when reading header")
1588 data_offset
= fd
.tell()
1590 calc_crc
= rar_crc32(memoryview(hdata
)[4:])
1591 if header_crc
!= calc_crc
:
1592 # header parsing failed.
1593 self
._set
_error
("Header CRC error: exp=%x got=%x (xlen = %d)",
1594 header_crc
, calc_crc
, len(hdata
))
1597 block_type
, pos
= load_vint(hdata
, pos
)
1599 if block_type
== RAR5_BLOCK_MAIN
:
1600 h
, pos
= self
._parse
_block
_common
(Rar5MainInfo(), hdata
)
1601 h
= self
._parse
_main
_block
(h
, hdata
, pos
)
1602 elif block_type
== RAR5_BLOCK_FILE
:
1603 h
, pos
= self
._parse
_block
_common
(Rar5FileInfo(), hdata
)
1604 h
= self
._parse
_file
_block
(h
, hdata
, pos
)
1605 elif block_type
== RAR5_BLOCK_SERVICE
:
1606 h
, pos
= self
._parse
_block
_common
(Rar5ServiceInfo(), hdata
)
1607 h
= self
._parse
_file
_block
(h
, hdata
, pos
)
1608 elif block_type
== RAR5_BLOCK_ENCRYPTION
:
1609 h
, pos
= self
._parse
_block
_common
(Rar5EncryptionInfo(), hdata
)
1610 h
= self
._parse
_encryption
_block
(h
, hdata
, pos
)
1611 elif block_type
== RAR5_BLOCK_ENDARC
:
1612 h
, pos
= self
._parse
_block
_common
(Rar5EndArcInfo(), hdata
)
1613 h
= self
._parse
_endarc
_block
(h
, hdata
, pos
)
1617 h
.header_offset
= header_offset
1618 h
.data_offset
= data_offset
1621 def _parse_block_common(self
, h
, hdata
):
1622 h
.header_crc
, pos
= load_le32(hdata
, 0)
1623 hdrlen
, pos
= load_vint(hdata
, pos
)
1624 h
.header_size
= hdrlen
+ pos
1625 h
.block_type
, pos
= load_vint(hdata
, pos
)
1626 h
.block_flags
, pos
= load_vint(hdata
, pos
)
1628 if h
.block_flags
& RAR5_BLOCK_FLAG_EXTRA_DATA
:
1629 h
.block_extra_size
, pos
= load_vint(hdata
, pos
)
1630 if h
.block_flags
& RAR5_BLOCK_FLAG_DATA_AREA
:
1631 h
.add_size
, pos
= load_vint(hdata
, pos
)
1633 h
.compress_size
= h
.add_size
1635 if h
.block_flags
& RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN
:
1636 h
.flags |
= RAR_SKIP_IF_UNKNOWN
1637 if h
.block_flags
& RAR5_BLOCK_FLAG_DATA_AREA
:
1638 h
.flags |
= RAR_LONG_BLOCK
1641 def _parse_main_block(self
, h
, hdata
, pos
):
1642 h
.main_flags
, pos
= load_vint(hdata
, pos
)
1643 if h
.main_flags
& RAR5_MAIN_FLAG_HAS_VOLNR
:
1644 h
.main_volume_number
, pos
= load_vint(hdata
, pos
)
1646 h
.flags |
= RAR_MAIN_NEWNUMBERING
1647 if h
.main_flags
& RAR5_MAIN_FLAG_SOLID
:
1648 h
.flags |
= RAR_MAIN_SOLID
1649 if h
.main_flags
& RAR5_MAIN_FLAG_ISVOL
:
1650 h
.flags |
= RAR_MAIN_VOLUME
1651 if h
.main_flags
& RAR5_MAIN_FLAG_RECOVERY
:
1652 h
.flags |
= RAR_MAIN_RECOVERY
1653 if self
._hdrenc
_main
:
1654 h
.flags |
= RAR_MAIN_PASSWORD
1655 if h
.main_flags
& RAR5_MAIN_FLAG_HAS_VOLNR
== 0:
1656 h
.flags |
= RAR_MAIN_FIRSTVOLUME
1660 def _parse_file_block(self
, h
, hdata
, pos
):
1661 h
.file_flags
, pos
= load_vint(hdata
, pos
)
1662 h
.file_size
, pos
= load_vint(hdata
, pos
)
1663 h
.mode
, pos
= load_vint(hdata
, pos
)
1665 if h
.file_flags
& RAR5_FILE_FLAG_HAS_MTIME
:
1666 h
.mtime
, pos
= load_unixtime(hdata
, pos
)
1667 h
.date_time
= h
.mtime
.timetuple()[:6]
1668 if h
.file_flags
& RAR5_FILE_FLAG_HAS_CRC32
:
1669 h
.CRC
, pos
= load_le32(hdata
, pos
)
1670 h
._md
_class
= CRC32Context
1671 h
._md
_expect
= h
.CRC
1673 h
.file_compress_flags
, pos
= load_vint(hdata
, pos
)
1674 h
.file_host_os
, pos
= load_vint(hdata
, pos
)
1675 h
.orig_filename
, pos
= load_vstr(hdata
, pos
)
1676 h
.filename
= h
.orig_filename
.decode("utf8", "replace")
1678 # use compatible values
1679 if h
.file_host_os
== RAR5_OS_WINDOWS
:
1680 h
.host_os
= RAR_OS_WIN32
1682 h
.host_os
= RAR_OS_UNIX
1683 h
.compress_type
= RAR_M0
+ ((h
.file_compress_flags
>> 7) & 7)
1685 if h
.block_extra_size
:
1686 # allow 1 byte of garbage
1687 while pos
< len(hdata
) - 1:
1688 xsize
, pos
= load_vint(hdata
, pos
)
1689 xdata
, pos
= load_bytes(hdata
, xsize
, pos
)
1690 self
._process
_file
_extra
(h
, xdata
)
1692 if h
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_BEFORE
:
1693 h
.flags |
= RAR_FILE_SPLIT_BEFORE
1694 if h
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_AFTER
:
1695 h
.flags |
= RAR_FILE_SPLIT_AFTER
1696 if h
.file_flags
& RAR5_FILE_FLAG_ISDIR
:
1697 h
.flags |
= RAR_FILE_DIRECTORY
1698 if h
.file_compress_flags
& RAR5_COMPR_SOLID
:
1699 h
.flags |
= RAR_FILE_SOLID
1703 def _parse_endarc_block(self
, h
, hdata
, pos
):
1704 h
.endarc_flags
, pos
= load_vint(hdata
, pos
)
1705 if h
.endarc_flags
& RAR5_ENDARC_FLAG_NEXT_VOL
:
1706 h
.flags |
= RAR_ENDARC_NEXT_VOLUME
1709 def _parse_encryption_block(self
, h
, hdata
, pos
):
1710 h
.encryption_algo
, pos
= load_vint(hdata
, pos
)
1711 h
.encryption_flags
, pos
= load_vint(hdata
, pos
)
1712 h
.encryption_kdf_count
, pos
= load_byte(hdata
, pos
)
1713 h
.encryption_salt
, pos
= load_bytes(hdata
, 16, pos
)
1714 if h
.encryption_flags
& RAR5_ENC_FLAG_HAS_CHECKVAL
:
1715 h
.encryption_check_value
= load_bytes(hdata
, 12, pos
)
1716 if h
.encryption_algo
!= RAR5_XENC_CIPHER_AES256
:
1717 raise BadRarFile("Unsupported header encryption cipher")
1718 self
._hdrenc
_main
= h
1722 def _process_file_extra(self
, h
, xdata
):
1723 xtype
, pos
= load_vint(xdata
, 0)
1724 if xtype
== RAR5_XFILE_TIME
:
1725 self
._parse
_file
_xtime
(h
, xdata
, pos
)
1726 elif xtype
== RAR5_XFILE_ENCRYPTION
:
1727 self
._parse
_file
_encryption
(h
, xdata
, pos
)
1728 elif xtype
== RAR5_XFILE_HASH
:
1729 self
._parse
_file
_hash
(h
, xdata
, pos
)
1730 elif xtype
== RAR5_XFILE_VERSION
:
1731 self
._parse
_file
_version
(h
, xdata
, pos
)
1732 elif xtype
== RAR5_XFILE_REDIR
:
1733 self
._parse
_file
_redir
(h
, xdata
, pos
)
1734 elif xtype
== RAR5_XFILE_OWNER
:
1735 self
._parse
_file
_owner
(h
, xdata
, pos
)
1736 elif xtype
== RAR5_XFILE_SERVICE
:
1741 # extra block for file time record
1742 def _parse_file_xtime(self
, h
, xdata
, pos
):
1743 tflags
, pos
= load_vint(xdata
, pos
)
1744 ldr
= load_windowstime
1745 if tflags
& RAR5_XTIME_UNIXTIME
:
1747 if tflags
& RAR5_XTIME_HAS_MTIME
:
1748 h
.mtime
, pos
= ldr(xdata
, pos
)
1749 h
.date_time
= h
.mtime
.timetuple()[:6]
1750 if tflags
& RAR5_XTIME_HAS_CTIME
:
1751 h
.ctime
, pos
= ldr(xdata
, pos
)
1752 if tflags
& RAR5_XTIME_HAS_ATIME
:
1753 h
.atime
, pos
= ldr(xdata
, pos
)
1755 # just remember encryption info
1756 def _parse_file_encryption(self
, h
, xdata
, pos
):
1757 algo
, pos
= load_vint(xdata
, pos
)
1758 flags
, pos
= load_vint(xdata
, pos
)
1759 kdf_count
, pos
= load_byte(xdata
, pos
)
1760 salt
, pos
= load_bytes(xdata
, 16, pos
)
1761 iv
, pos
= load_bytes(xdata
, 16, pos
)
1763 if flags
& RAR5_XENC_CHECKVAL
:
1764 checkval
, pos
= load_bytes(xdata
, 12, pos
)
1765 if flags
& RAR5_XENC_TWEAKED
:
1767 h
._md
_class
= NoHashContext
1769 h
.file_encryption
= (algo
, flags
, kdf_count
, salt
, iv
, checkval
)
1770 h
.flags |
= RAR_FILE_PASSWORD
1772 def _parse_file_hash(self
, h
, xdata
, pos
):
1773 hash_type
, pos
= load_vint(xdata
, pos
)
1774 if hash_type
== RAR5_XHASH_BLAKE2SP
:
1775 h
.blake2sp_hash
, pos
= load_bytes(xdata
, 32, pos
)
1776 if (h
.file_encryption
[1] & RAR5_XENC_TWEAKED
) == 0:
1777 h
._md
_class
= Blake2SP
1778 h
._md
_expect
= h
.blake2sp_hash
1780 def _parse_file_version(self
, h
, xdata
, pos
):
1781 flags
, pos
= load_vint(xdata
, pos
)
1782 version
, pos
= load_vint(xdata
, pos
)
1783 h
.file_version
= (flags
, version
)
1785 def _parse_file_redir(self
, h
, xdata
, pos
):
1786 redir_type
, pos
= load_vint(xdata
, pos
)
1787 redir_flags
, pos
= load_vint(xdata
, pos
)
1788 redir_name
, pos
= load_vstr(xdata
, pos
)
1789 redir_name
= redir_name
.decode("utf8", "replace")
1790 h
.file_redir
= (redir_type
, redir_flags
, redir_name
)
1792 def _parse_file_owner(self
, h
, xdata
, pos
):
1793 user_name
= group_name
= user_id
= group_id
= None
1795 flags
, pos
= load_vint(xdata
, pos
)
1796 if flags
& RAR5_XOWNER_UNAME
:
1797 user_name
, pos
= load_vstr(xdata
, pos
)
1798 if flags
& RAR5_XOWNER_GNAME
:
1799 group_name
, pos
= load_vstr(xdata
, pos
)
1800 if flags
& RAR5_XOWNER_UID
:
1801 user_id
, pos
= load_vint(xdata
, pos
)
1802 if flags
& RAR5_XOWNER_GID
:
1803 group_id
, pos
= load_vint(xdata
, pos
)
1805 h
.file_owner
= (user_name
, group_name
, user_id
, group_id
)
1807 def process_entry(self
, fd
, item
):
1808 if item
.block_type
== RAR5_BLOCK_FILE
:
1809 # use only first part
1810 if (item
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_BEFORE
) == 0:
1811 self
._info
_map
[item
.filename
] = item
1812 self
._info
_list
.append(item
)
1813 elif len(self
._info
_list
) > 0:
1814 # final crc is in last block
1815 old
= self
._info
_list
[-1]
1817 old
._md
_expect
= item
._md
_expect
1818 old
.blake2sp_hash
= item
.blake2sp_hash
1819 old
.compress_size
+= item
.compress_size
1820 elif item
.block_type
== RAR5_BLOCK_SERVICE
:
1821 if item
.filename
== "CMT":
1822 self
._load
_comment
(fd
, item
)
1824 def _load_comment(self
, fd
, item
):
1825 if item
.block_flags
& (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER
):
1827 if item
.compress_type
!= RAR_M0
:
1830 if item
.flags
& RAR_FILE_PASSWORD
:
1831 algo
, ___flags
, kdf_count
, salt
, iv
, ___checkval
= item
.file_encryption
1832 if algo
!= RAR5_XENC_CIPHER_AES256
:
1834 key
= self
._gen
_key
(kdf_count
, salt
)
1835 f
= HeaderDecrypt(fd
, key
, iv
)
1836 cmt
= f
.read(item
.file_size
)
1839 with self
._open
_clear
(item
) as cmtstream
:
1840 cmt
= cmtstream
.read()
1842 # rar bug? - appends zero to comment
1843 cmt
= cmt
.split(ZERO
, 1)[0]
1844 self
.comment
= cmt
.decode("utf8")
1847 def _open_hack(self
, inf
, psw
):
1848 # len, type, blk_flags, flags
1849 main_hdr
= b
"\x03\x01\x00\x00"
1850 endarc_hdr
= b
"\x03\x05\x00\x00"
1851 main_hdr
= S_LONG
.pack(rar_crc32(main_hdr
)) + main_hdr
1852 endarc_hdr
= S_LONG
.pack(rar_crc32(endarc_hdr
)) + endarc_hdr
1853 return self
._open
_hack
_core
(inf
, psw
, RAR5_ID
+ main_hdr
, endarc_hdr
)
1859 class UnicodeFilename(object):
1860 """Handle RAR3 unicode filename decompression.
1862 def __init__(self
, name
, encdata
):
1863 self
.std_name
= bytearray(name
)
1864 self
.encdata
= bytearray(encdata
)
1865 self
.pos
= self
.encpos
= 0
1866 self
.buf
= bytearray()
1870 """Copy encoded byte."""
1872 c
= self
.encdata
[self
.encpos
]
1880 """Copy byte from 8-bit representation."""
1882 return self
.std_name
[self
.pos
]
1887 def put(self
, lo
, hi
):
1888 """Copy 16-bit value to result."""
1894 """Decompress compressed UTF16 value."""
1895 hi
= self
.enc_byte()
1897 while self
.encpos
< len(self
.encdata
):
1899 flags
= self
.enc_byte()
1902 t
= (flags
>> flagbits
) & 3
1904 self
.put(self
.enc_byte(), 0)
1906 self
.put(self
.enc_byte(), hi
)
1908 self
.put(self
.enc_byte(), self
.enc_byte())
1913 for _
in range((n
& 0x7f) + 2):
1914 lo
= (self
.std_byte() + c
) & 0xFF
1917 for _
in range(n
+ 2):
1918 self
.put(self
.std_byte(), 0)
1919 return self
.buf
.decode("utf-16le", "replace")
1922 class RarExtFile(RawIOBase
):
1923 """Base class for file-like object that :meth:`RarFile.open` returns.
1925 Provides public methods and common crc checking.
1928 - no short reads - .read() and .readinfo() read as much as requested.
1929 - no internal buffer, use io.BufferedReader for that.
1932 #: Filename of the archive entry
1935 def __init__(self
, parser
, inf
):
1936 """Open archive entry.
1938 super(RarExtFile
, self
).__init
__()
1940 # standard io.* properties
1941 self
.name
= inf
.filename
1944 self
._parser
= parser
1948 self
._returncode
= 0
1950 self
._md
_context
= None
1957 md_class
= self
._inf
._md
_class
or NoHashContext
1958 self
._md
_context
= md_class()
1960 self
._remain
= self
._inf
.file_size
1962 def read(self
, cnt
=None):
1963 """Read all or specified amount of data from archive entry."""
1966 if cnt
is None or cnt
< 0:
1968 elif cnt
> self
._remain
:
1977 data
= self
._read
(cnt
)
1981 self
._md
_context
.update(data
)
1982 self
._remain
-= len(data
)
1984 data
= EMPTY
.join(buf
)
1986 raise BadRarFile("Failed the read enough data: req=%d got=%d" % (orig
, len(data
)))
1989 if not data
or self
._remain
== 0:
1995 """Check final CRC."""
1996 final
= self
._md
_context
.digest()
1997 exp
= self
._inf
._md
_expect
2002 if self
._returncode
:
2003 check_returncode(self
, "")
2004 if self
._remain
!= 0:
2005 raise BadRarFile("Failed the read enough data")
2007 raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % (
2008 self
._inf
.filename
, exp
, final
))
2010 def _read(self
, cnt
):
2011 """Actual read that gets sanitized cnt."""
2012 raise NotImplementedError("_read")
2015 """Close open resources."""
2017 super(RarExtFile
, self
).close()
2024 """Hook delete to make sure tempfile is removed."""
2027 def readinto(self
, buf
):
2028 """Zero-copy read directly into buffer.
2032 raise NotImplementedError("readinto")
2035 """Return current reading position in uncompressed data."""
2036 return self
._inf
.file_size
- self
._remain
2038 def seek(self
, ofs
, whence
=0):
2041 On uncompressed files, the seeking works by actual
2042 seeks so it's fast. On compresses files its slow
2043 - forward seeking happends by reading ahead,
2044 backwards by re-opening and decompressing from the start.
2047 # disable crc check when seeking
2048 self
._md
_context
= NoHashContext()
2050 fsize
= self
._inf
.file_size
2051 cur_ofs
= self
.tell()
2053 if whence
== 0: # seek from beginning of file
2055 elif whence
== 1: # seek from current position
2056 new_ofs
= cur_ofs
+ ofs
2057 elif whence
== 2: # seek from end of file
2058 new_ofs
= fsize
+ ofs
2060 raise ValueError("Invalid value for whence")
2065 elif new_ofs
> fsize
:
2068 # do the actual seek
2069 if new_ofs
>= cur_ofs
:
2070 self
._skip
(new_ofs
- cur_ofs
)
2077 def _skip(self
, cnt
):
2078 """Read and discard data"""
2081 buf
= self
.read(8192)
2083 buf
= self
.read(cnt
)
2095 Writing is not supported.
2102 Seeking is supported, although it's slow on compressed files.
2107 """Read all remaining data"""
2108 # avoid RawIOBase default impl
2112 class PipeReader(RarExtFile
):
2113 """Read data from pipe, handle tempfile cleanup."""
2115 def __init__(self
, rf
, inf
, cmd
, tempfile
=None):
2118 self
._tempfile
= tempfile
2119 super(PipeReader
, self
).__init
__(rf
, inf
)
2121 def _close_proc(self
):
2124 if self
._proc
.stdout
:
2125 self
._proc
.stdout
.close()
2126 if self
._proc
.stdin
:
2127 self
._proc
.stdin
.close()
2128 if self
._proc
.stderr
:
2129 self
._proc
.stderr
.close()
2131 self
._returncode
= self
._proc
.returncode
2135 super(PipeReader
, self
)._open
()
2140 # launch new process
2141 self
._returncode
= 0
2142 self
._proc
= custom_popen(self
._cmd
)
2143 self
._fd
= self
._proc
.stdout
2145 # avoid situation where unrar waits on stdin
2146 if self
._proc
.stdin
:
2147 self
._proc
.stdin
.close()
2149 def _read(self
, cnt
):
2150 """Read from pipe."""
2152 # normal read is usually enough
2153 data
= self
._fd
.read(cnt
)
2154 if len(data
) == cnt
or not data
:
2157 # short read, try looping
2161 data
= self
._fd
.read(cnt
)
2166 return EMPTY
.join(buf
)
2169 """Close open resources."""
2172 super(PipeReader
, self
).close()
2176 os
.unlink(self
._tempfile
)
2179 self
._tempfile
= None
2181 def readinto(self
, buf
):
2182 """Zero-copy read directly into buffer."""
2184 if cnt
> self
._remain
:
2186 vbuf
= memoryview(buf
)
2189 res
= self
._fd
.readinto(vbuf
[got
: cnt
])
2192 self
._md
_context
.update(vbuf
[got
: got
+ res
])
2198 class DirectReader(RarExtFile
):
2199 """Read uncompressed data directly from archive.
2206 super(DirectReader
, self
)._open
()
2208 self
._volfile
= self
._inf
.volume_file
2209 self
._fd
= XFile(self
._volfile
, 0)
2210 self
._fd
.seek(self
._inf
.header_offset
, 0)
2211 self
._cur
= self
._parser
._parse
_header
(self
._fd
)
2212 self
._cur
_avail
= self
._cur
.add_size
2214 def _skip(self
, cnt
):
2215 """RAR Seek, skipping through rar files to get to correct position
2220 if self
._cur
_avail
== 0:
2221 if not self
._open
_next
():
2224 # fd is in read pos, do the read
2225 if cnt
> self
._cur
_avail
:
2226 cnt
-= self
._cur
_avail
2227 self
._remain
-= self
._cur
_avail
2230 self
._fd
.seek(cnt
, 1)
2231 self
._cur
_avail
-= cnt
2235 def _read(self
, cnt
):
2236 """Read from potentially multi-volume archive."""
2241 if self
._cur
_avail
== 0:
2242 if not self
._open
_next
():
2245 # fd is in read pos, do the read
2246 if cnt
> self
._cur
_avail
:
2247 data
= self
._fd
.read(self
._cur
_avail
)
2249 data
= self
._fd
.read(cnt
)
2255 self
._cur
_avail
-= len(data
)
2260 return EMPTY
.join(buf
)
2262 def _open_next(self
):
2263 """Proceed to next volume."""
2265 # is the file split over archives?
2266 if (self
._cur
.flags
& RAR_FILE_SPLIT_AFTER
) == 0:
2274 self
._volfile
= self
._parser
._next
_volname
(self
._volfile
)
2275 fd
= open(self
._volfile
, "rb", 0)
2277 sig
= fd
.read(len(self
._parser
._expect
_sig
))
2278 if sig
!= self
._parser
._expect
_sig
:
2279 raise BadRarFile("Invalid signature")
2281 # loop until first file header
2283 cur
= self
._parser
._parse
_header
(fd
)
2285 raise BadRarFile("Unexpected EOF")
2286 if cur
.type in (RAR_BLOCK_MARK
, RAR_BLOCK_MAIN
):
2288 fd
.seek(cur
.add_size
, 1)
2290 if cur
.orig_filename
!= self
._inf
.orig_filename
:
2291 raise BadRarFile("Did not found file entry")
2293 self
._cur
_avail
= cur
.add_size
2296 def readinto(self
, buf
):
2297 """Zero-copy read directly into buffer."""
2299 vbuf
= memoryview(buf
)
2300 while got
< len(buf
):
2302 if self
._cur
_avail
== 0:
2303 if not self
._open
_next
():
2306 # length for next read
2307 cnt
= len(buf
) - got
2308 if cnt
> self
._cur
_avail
:
2309 cnt
= self
._cur
_avail
2311 # read into temp view
2312 res
= self
._fd
.readinto(vbuf
[got
: got
+ cnt
])
2315 self
._md
_context
.update(vbuf
[got
: got
+ res
])
2316 self
._cur
_avail
-= res
2322 class HeaderDecrypt(object):
2323 """File-like object that decrypts from another file"""
2324 def __init__(self
, f
, key
, iv
):
2326 self
.ciph
= AES_CBC_Decrypt(key
, iv
)
2330 """Current file pos - works only on block boundaries."""
2331 return self
.f
.tell()
2333 def read(self
, cnt
=None):
2334 """Read and decrypt."""
2336 raise BadRarFile("Bad count to header decrypt - wrong password?")
2339 if cnt
<= len(self
.buf
):
2340 res
= self
.buf
[:cnt
]
2341 self
.buf
= self
.buf
[cnt
:]
2350 enc
= self
.f
.read(blklen
)
2351 if len(enc
) < blklen
:
2353 dec
= self
.ciph
.decrypt(enc
)
2359 self
.buf
= dec
[cnt
:]
2365 # handle (filename|filelike) object
2366 class XFile(object):
2367 """Input may be filename or file object.
2369 __slots__
= ("_fd", "_need_close")
2371 def __init__(self
, xfile
, bufsize
=1024):
2372 if is_filelike(xfile
):
2373 self
._need
_close
= False
2377 self
._need
_close
= True
2378 self
._fd
= open(xfile
, "rb", bufsize
)
2380 def read(self
, n
=None):
2381 """Read from file."""
2382 return self
._fd
.read(n
)
2385 """Return file pos."""
2386 return self
._fd
.tell()
2388 def seek(self
, ofs
, whence
=0):
2389 """Move file pos."""
2390 return self
._fd
.seek(ofs
, whence
)
2392 def readinto(self
, dst
):
2393 """Read into buffer."""
2394 return self
._fd
.readinto(dst
)
2397 """Close file object."""
2398 if self
._need
_close
:
2401 def __enter__(self
):
2404 def __exit__(self
, typ
, val
, tb
):
2408 class NoHashContext(object):
2409 """No-op hash function."""
2410 def __init__(self
, data
=None):
2412 def update(self
, data
):
2416 def hexdigest(self
):
2417 """Hexadecimal digest."""
2420 class CRC32Context(object):
2421 """Hash context that uses CRC32."""
2422 __slots__
= ["_crc"]
2424 def __init__(self
, data
=None):
2429 def update(self
, data
):
2431 self
._crc
= rar_crc32(data
, self
._crc
)
2437 def hexdigest(self
):
2438 """Hexadecimal digest."""
2439 return "%08x" % self
.digest()
2442 class Blake2SP(object):
2443 """Blake2sp hash context.
2445 __slots__
= ["_thread", "_buf", "_cur", "_digest"]
2450 def __init__(self
, data
=None):
2456 for i
in range(self
.parallelism
):
2457 ctx
= self
._blake
2s
(i
, 0, i
== (self
.parallelism
- 1))
2458 self
._thread
.append(ctx
)
2463 def _blake2s(self
, ofs
, depth
, is_last
):
2464 return blake2s(node_offset
=ofs
, node_depth
=depth
, last_node
=is_last
,
2465 depth
=2, inner_size
=32, fanout
=self
.parallelism
)
2467 def _add_block(self
, blk
):
2468 self
._thread
[self
._cur
].update(blk
)
2469 self
._cur
= (self
._cur
+ 1) % self
.parallelism
2471 def update(self
, data
):
2474 view
= memoryview(data
)
2475 bs
= self
.block_size
2477 need
= bs
- len(self
._buf
)
2478 if len(view
) < need
:
2479 self
._buf
+= view
.tobytes()
2481 self
._add
_block
(self
._buf
+ view
[:need
].tobytes())
2483 while len(view
) >= bs
:
2484 self
._add
_block
(view
[:bs
])
2486 self
._buf
= view
.tobytes()
2489 """Return final digest value.
2491 if self
._digest
is None:
2493 self
._add
_block
(self
._buf
)
2495 ctx
= self
._blake
2s
(0, 1, True)
2496 for t
in self
._thread
:
2497 ctx
.update(t
.digest())
2498 self
._digest
= ctx
.digest()
2501 def hexdigest(self
):
2502 """Hexadecimal digest."""
2503 return tohex(self
.digest())
2506 class Rar3Sha1(object):
2507 """Bug-compat for SHA1
2512 _BLK_BE
= struct
.Struct(b
">16L")
2513 _BLK_LE
= struct
.Struct(b
"<16L")
2515 __slots__
= ("_nbytes", "_md", "_rarbug")
2517 def __init__(self
, data
=b
"", rarbug
=False):
2520 self
._rarbug
= rarbug
2523 def update(self
, data
):
2524 """Process more data."""
2525 self
._md
.update(data
)
2526 bufpos
= self
._nbytes
& 63
2527 self
._nbytes
+= len(data
)
2529 if self
._rarbug
and len(data
) > 64:
2530 dpos
= self
.block_size
- bufpos
2531 while dpos
+ self
.block_size
<= len(data
):
2532 self
._corrupt
(data
, dpos
)
2533 dpos
+= self
.block_size
2536 """Return final state."""
2537 return self
._md
.digest()
2539 def hexdigest(self
):
2540 """Return final state as hex string."""
2541 return self
._md
.hexdigest()
2543 def _corrupt(self
, data
, dpos
):
2544 """Corruption from SHA1 core."""
2545 ws
= list(self
._BLK
_BE
.unpack_from(data
, dpos
))
2546 for t
in range(16, 80):
2547 tmp
= ws
[(t
- 3) & 15] ^ ws
[(t
- 8) & 15] ^ ws
[(t
- 14) & 15] ^ ws
[(t
- 16) & 15]
2548 ws
[t
& 15] = ((tmp
<< 1) |
(tmp
>> (32 - 1))) & 0xFFFFFFFF
2549 self
._BLK
_LE
.pack_into(data
, dpos
, *ws
)
2553 ## Utility functions
2556 S_LONG
= Struct("<L")
2557 S_SHORT
= Struct("<H")
2558 S_BYTE
= Struct("<B")
2560 S_BLK_HDR
= Struct("<HBHH")
2561 S_FILE_HDR
= Struct("<LLBLLBBHL")
2562 S_COMMENT_HDR
= Struct("<HBBH")
2564 def load_vint(buf
, pos
):
2565 """Load variable-size int."""
2566 limit
= min(pos
+ 11, len(buf
))
2570 res
+= ((b
& 0x7F) << ofs
)
2575 raise BadRarFile("cannot load vint")
2577 def load_byte(buf
, pos
):
2578 """Load single byte"""
2581 raise BadRarFile("cannot load byte")
2582 return S_BYTE
.unpack_from(buf
, pos
)[0], end
2584 def load_le32(buf
, pos
):
2585 """Load little-endian 32-bit integer"""
2588 raise BadRarFile("cannot load le32")
2589 return S_LONG
.unpack_from(buf
, pos
)[0], pos
+ 4
2591 def load_bytes(buf
, num
, pos
):
2592 """Load sequence of bytes"""
2595 raise BadRarFile("cannot load bytes")
2596 return buf
[pos
: end
], end
2598 def load_vstr(buf
, pos
):
2599 """Load bytes prefixed by vint length"""
2600 slen
, pos
= load_vint(buf
, pos
)
2601 return load_bytes(buf
, slen
, pos
)
2603 def load_dostime(buf
, pos
):
2604 """Load LE32 dos timestamp"""
2605 stamp
, pos
= load_le32(buf
, pos
)
2606 tup
= parse_dos_time(stamp
)
2607 return to_datetime(tup
), pos
2609 def load_unixtime(buf
, pos
):
2610 """Load LE32 unix timestamp"""
2611 secs
, pos
= load_le32(buf
, pos
)
2612 dt
= datetime
.fromtimestamp(secs
, UTC
)
2615 def load_windowstime(buf
, pos
):
2616 """Load LE64 windows timestamp"""
2617 # unix epoch (1970) in seconds from windows epoch (1601)
2618 unix_epoch
= 11644473600
2619 val1
, pos
= load_le32(buf
, pos
)
2620 val2
, pos
= load_le32(buf
, pos
)
2621 secs
, n1secs
= divmod((val2
<< 32) | val1
, 10000000)
2622 dt
= datetime
.fromtimestamp(secs
- unix_epoch
, UTC
)
2623 dt
= dt
.replace(microsecond
=n1secs
// 10)
2626 # new-style next volume
2627 def _next_newvol(volfile
):
2628 i
= len(volfile
) - 1
2630 if volfile
[i
] >= "0" and volfile
[i
] <= "9":
2631 return _inc_volname(volfile
, i
)
2633 raise BadRarName("Cannot construct volume name: " + volfile
)
2635 # old-style next volume
2636 def _next_oldvol(volfile
):
2638 if volfile
[-4:].lower() == ".rar":
2639 return volfile
[:-2] + "00"
2640 return _inc_volname(volfile
, len(volfile
) - 1)
2642 # increase digits with carry, otherwise just increment char
2643 def _inc_volname(volfile
, i
):
2647 fn
[i
] = chr(ord(fn
[i
]) + 1)
2653 # rar3 extended time fields
2654 def _parse_ext_time(h
, data
, pos
):
2655 # flags and rest of data can be missing
2657 if pos
+ 2 <= len(data
):
2658 flags
= S_SHORT
.unpack_from(data
, pos
)[0]
2661 mtime
, pos
= _parse_xtime(flags
>> 3 * 4, data
, pos
, h
.mtime
)
2662 h
.ctime
, pos
= _parse_xtime(flags
>> 2 * 4, data
, pos
)
2663 h
.atime
, pos
= _parse_xtime(flags
>> 1 * 4, data
, pos
)
2664 h
.arctime
, pos
= _parse_xtime(flags
>> 0 * 4, data
, pos
)
2667 h
.date_time
= mtime
.timetuple()[:6]
2670 # rar3 one extended time field
2671 def _parse_xtime(flag
, data
, pos
, basetime
=None):
2675 basetime
, pos
= load_dostime(data
, pos
)
2677 # load second fractions
2680 for _
in range(cnt
):
2681 b
, pos
= load_byte(data
, pos
)
2682 rem
= (b
<< 16) |
(rem
>> 8)
2684 # convert 100ns units to microseconds
2689 # dostime has room for 30 seconds only, correct if needed
2690 if flag
& 4 and basetime
.second
< 59:
2691 res
= basetime
.replace(microsecond
=usec
, second
=basetime
.second
+ 1)
2693 res
= basetime
.replace(microsecond
=usec
)
2696 def is_filelike(obj
):
2697 """Filename or file object?
2699 filename_types
= (bytes
, str, Path
)
2701 if isinstance(obj
, filename_types
):
2704 for a
in ("read", "tell", "seek"):
2705 res
= res
and hasattr(obj
, a
)
2707 raise ValueError("Invalid object passed as file")
2710 def rar3_s2k(psw
, salt
):
2711 """String-to-key hash for RAR3.
2713 if not isinstance(psw
, str):
2714 psw
= psw
.decode("utf8")
2715 seed
= bytearray(psw
.encode("utf-16le") + salt
)
2716 h
= Rar3Sha1(rarbug
=True)
2719 for j
in range(0x4000):
2720 cnt
= S_LONG
.pack(i
* 0x4000 + j
)
2724 iv
+= h
.digest()[19:20]
2725 key_be
= h
.digest()[:16]
2726 key_le
= pack("<LLLL", *unpack(">LLLL", key_be
))
2729 def rar3_decompress(vers
, meth
, data
, declen
=0, flags
=0, crc
=0, psw
=None, salt
=None):
2730 """Decompress blob of compressed data.
2732 Used for data with non-standard header - eg. comments.
2734 # already uncompressed?
2735 if meth
== RAR_M0
and (flags
& RAR_FILE_PASSWORD
) == 0:
2738 # take only necessary flags
2739 flags
= flags
& (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK
)
2740 flags |
= RAR_LONG_BLOCK
2744 date
= ((2010 - 1980) << 25) + (12 << 21) + (31 << 16)
2746 fhdr
= S_FILE_HDR
.pack(len(data
), declen
, RAR_OS_MSDOS
, crc
,
2747 date
, vers
, meth
, len(fname
), mode
)
2749 if flags
& RAR_FILE_SALT
:
2755 hlen
= S_BLK_HDR
.size
+ len(fhdr
)
2756 hdr
= S_BLK_HDR
.pack(0, RAR_BLOCK_FILE
, flags
, hlen
) + fhdr
2757 hcrc
= rar_crc32(hdr
[2:]) & 0xFFFF
2758 hdr
= S_BLK_HDR
.pack(hcrc
, RAR_BLOCK_FILE
, flags
, hlen
) + fhdr
2760 # archive main header
2761 mh
= S_BLK_HDR
.pack(0x90CF, RAR_BLOCK_MAIN
, 0, 13) + ZERO
* (2 + 4)
2763 # decompress via temp rar
2764 setup
= tool_setup()
2765 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
2766 tmpf
= os
.fdopen(tmpfd
, "wb")
2768 tmpf
.write(RAR_ID
+ mh
+ hdr
+ data
)
2771 curpsw
= (flags
& RAR_FILE_PASSWORD
) and psw
or None
2772 cmd
= setup
.open_cmdline(curpsw
, tmpname
)
2773 p
= custom_popen(cmd
)
2774 return p
.communicate()[0]
2780 """Convert 6-part time tuple into datetime object.
2786 year
, mon
, day
, h
, m
, s
= t
2788 # assume the values are valid
2790 return datetime(year
, mon
, day
, h
, m
, s
)
2794 # sanitize invalid values
2795 mday
= (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
2810 if mon
== 2 and day
== 29:
2812 return datetime(year
, mon
, day
, h
, m
, s
)
2815 return datetime(year
, mon
, day
, h
, m
, s
)
2817 def parse_dos_time(stamp
):
2818 """Parse standard 32-bit DOS timestamp.
2820 sec
, stamp
= stamp
& 0x1F, stamp
>> 5
2821 mn
, stamp
= stamp
& 0x3F, stamp
>> 6
2822 hr
, stamp
= stamp
& 0x1F, stamp
>> 5
2823 day
, stamp
= stamp
& 0x1F, stamp
>> 5
2824 mon
, stamp
= stamp
& 0x0F, stamp
>> 4
2825 yr
= (stamp
& 0x7F) + 1980
2826 return (yr
, mon
, day
, hr
, mn
, sec
* 2)
2828 def custom_popen(cmd
):
2829 """Disconnect cmd from parent fds, read only from stdout.
2833 if sys
.platform
== "win32":
2834 creationflags
= 0x08000000 # CREATE_NO_WINDOW
2838 p
= Popen(cmd
, bufsize
=0, stdout
=PIPE
, stdin
=PIPE
, stderr
=STDOUT
,
2839 creationflags
=creationflags
)
2840 except OSError as ex
:
2841 if ex
.errno
== errno
.ENOENT
:
2842 raise RarCannotExec("Unrar not installed?")
2843 if ex
.errno
== errno
.EACCES
or ex
.errno
== errno
.EPERM
:
2844 raise RarCannotExec("Cannot execute unrar")
2848 def check_returncode(p
, out
):
2849 """Raise exception according to unrar exit code.
2855 errmap
= tool_setup().get_errmap()
2856 if code
> 0 and code
< len(errmap
):
2863 exc
= RarUnknownError
2867 msg
= "%s [%d]: %s" % (exc
.__doc
__, p
.returncode
, out
)
2869 msg
= "%s [%d]" % (exc
.__doc
__, p
.returncode
)
2873 def hmac_sha256(key
, data
):
2875 return HMAC(key
, data
, sha256
).digest()
2877 def membuf_tempfile(memfile
):
2878 """Write in-memory file object to real file."""
2881 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
2882 tmpf
= os
.fdopen(tmpfd
, "wb")
2886 buf
= memfile
.read(BSIZE
)
2897 class XTempFile(object):
2898 """Real file for archive.
2900 __slots__
= ("_tmpfile", "_filename")
2902 def __init__(self
, rarfile
):
2903 if is_filelike(rarfile
):
2904 self
._tmpfile
= membuf_tempfile(rarfile
)
2905 self
._filename
= self
._tmpfile
2907 self
._tmpfile
= None
2908 self
._filename
= rarfile
2910 def __enter__(self
):
2911 return self
._filename
2913 def __exit__(self
, exc_type
, exc_value
, tb
):
2916 os
.unlink(self
._tmpfile
)
2919 self
._tmpfile
= None
2922 # Find working command-line tool
2926 def __init__(self
, setup
):
2930 cmdline
= self
.get_cmdline("check_cmd", None)
2932 p
= custom_popen(cmdline
)
2933 out
, _
= p
.communicate()
2934 return p
.returncode
== 0
2935 except RarCannotExec
:
2938 def open_cmdline(self
, psw
, rarfn
, filefn
=None):
2939 cmdline
= self
.get_cmdline("open_cmd", psw
)
2940 cmdline
.append(rarfn
)
2942 self
.add_file_arg(cmdline
, filefn
)
2945 def test_cmdline(self
, psw
, rarfn
):
2946 cmdline
= self
.get_cmdline("test_cmd", psw
)
2947 cmdline
.append(rarfn
)
2950 def extract_cmdline(self
, psw
, rarfn
, fnlist
, path
):
2951 cmdline
= self
.get_cmdline("extract_cmd", psw
, nodash
=True)
2952 dstdir
= "DSTDIR" in cmdline
2956 cmdline
[cmdline
.index("DSTDIR", 1)] = path
2958 cmdline
.append("--")
2960 cmdline
.append(rarfn
)
2962 self
.add_file_arg(cmdline
, fn
)
2964 if path
and not dstdir
:
2965 cmdline
.append(path
+ os
.sep
)
2968 def get_errmap(self
):
2969 return self
.setup
["errmap"]
2971 def get_cmdline(self
, key
, psw
, nodash
=False):
2972 cmdline
= list(self
.setup
[key
])
2973 cmdline
[0] = globals()[cmdline
[0]]
2974 self
.add_password_arg(cmdline
, psw
)
2976 cmdline
.append("--")
2979 def add_file_arg(self
, cmdline
, filename
):
2980 filename
= filename
.replace('\\', '/')
2981 cmdline
.append(filename
)
2983 def add_password_arg(self
, cmdline
, psw
):
2984 """Append password switch to commandline.
2987 if not isinstance(psw
, str):
2988 psw
= psw
.decode("utf8")
2989 args
= self
.setup
["password"]
2990 if isinstance(args
, str):
2991 cmdline
.append(args
+ psw
)
2993 cmdline
.extend(args
)
2996 cmdline
.extend(self
.setup
["no_password"])
3000 "open_cmd": ("UNRAR_TOOL", "p", "-inul"),
3001 "extract_cmd": ("UNRAR_TOOL", "x", "-y", "-idq"),
3002 "test_cmd": ("UNRAR_TOOL", "t", "-idq"),
3003 "check_cmd": ("UNRAR_TOOL", "-inul"),
3005 "no_password": ("-p-",),
3006 # map return code to exception class, codes from rar.txt
3008 RarWarning
, RarFatalError
, RarCRCError
, RarLockedArchiveError
, # 1..4
3009 RarWriteError
, RarOpenError
, RarUserError
, RarMemoryError
, # 5..8
3010 RarCreateError
, RarNoFilesError
, RarWrongPassword
] # 9..11
3013 # Problems with unar RAR backend:
3014 # - Does not support RAR2 locked files [fails to read]
3015 # - Does not support RAR5 Blake2sp hash [reading works]
3017 "open_cmd": ("UNAR_TOOL", "-q", "-o", "-"),
3018 "extract_cmd": ("UNAR_TOOL", "-q", "-f", "-D", "-o", "DSTDIR"),
3019 "test_cmd": ("LSAR_TOOL", "-test"),
3020 "check_cmd": ("UNAR_TOOL", "-version"),
3021 "password": ("-p",),
3022 "no_password": ("-p", ""),
3026 # Problems with libarchive RAR backend:
3027 # - Does not support solid archives.
3028 # - Does not support password-protected archives.
3029 # - Does not support RARVM-based compression filters.
3031 "open_cmd": ("BSDTAR_TOOL", "-x", "--to-stdout", "-f"),
3032 "extract_cmd": ("BSDTAR_TOOL", "-x", "-C", "DSTDIR", "-f"),
3033 "test_cmd": ("BSDTAR_TOOL", "-t", "-f"),
3034 "check_cmd": ("BSDTAR_TOOL", "--version"),
3040 CURRENT_SETUP
= None
3042 def tool_setup(unrar
=True, unar
=True, bsdtar
=True, force
=False):
3043 """Pick a tool, return cached ToolSetup.
3045 global CURRENT_SETUP
3047 CURRENT_SETUP
= None
3048 if CURRENT_SETUP
is not None:
3049 return CURRENT_SETUP
3052 lst
.append(UNRAR_CONFIG
)
3054 lst
.append(UNAR_CONFIG
)
3056 lst
.append(BSDTAR_CONFIG
)
3059 setup
= ToolSetup(conf
)
3061 CURRENT_SETUP
= setup
3063 if CURRENT_SETUP
is None:
3064 raise RarCannotExec("Cannot find working tool")
3065 return CURRENT_SETUP