3 # Copyright (c) 2005-2024 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.
213 RAR_MAX_PASSWORD
= 127 #: Max number of utf-16 chars in passwords.
214 RAR_MAX_KDF_SHIFT
= 24 #: Max power-of-2 for KDF count
222 RAR5_BLOCK_SERVICE
= 3
223 RAR5_BLOCK_ENCRYPTION
= 4
224 RAR5_BLOCK_ENDARC
= 5
226 RAR5_BLOCK_FLAG_EXTRA_DATA
= 0x01
227 RAR5_BLOCK_FLAG_DATA_AREA
= 0x02
228 RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN
= 0x04
229 RAR5_BLOCK_FLAG_SPLIT_BEFORE
= 0x08
230 RAR5_BLOCK_FLAG_SPLIT_AFTER
= 0x10
231 RAR5_BLOCK_FLAG_DEPENDS_PREV
= 0x20
232 RAR5_BLOCK_FLAG_KEEP_WITH_PARENT
= 0x40
234 RAR5_MAIN_FLAG_ISVOL
= 0x01
235 RAR5_MAIN_FLAG_HAS_VOLNR
= 0x02
236 RAR5_MAIN_FLAG_SOLID
= 0x04
237 RAR5_MAIN_FLAG_RECOVERY
= 0x08
238 RAR5_MAIN_FLAG_LOCKED
= 0x10
240 RAR5_FILE_FLAG_ISDIR
= 0x01
241 RAR5_FILE_FLAG_HAS_MTIME
= 0x02
242 RAR5_FILE_FLAG_HAS_CRC32
= 0x04
243 RAR5_FILE_FLAG_UNKNOWN_SIZE
= 0x08
245 RAR5_COMPR_SOLID
= 0x40
247 RAR5_ENC_FLAG_HAS_CHECKVAL
= 0x01
249 RAR5_ENDARC_FLAG_NEXT_VOL
= 0x01
251 RAR5_XFILE_ENCRYPTION
= 1
254 RAR5_XFILE_VERSION
= 4
257 RAR5_XFILE_SERVICE
= 7
259 RAR5_XTIME_UNIXTIME
= 0x01
260 RAR5_XTIME_HAS_MTIME
= 0x02
261 RAR5_XTIME_HAS_CTIME
= 0x04
262 RAR5_XTIME_HAS_ATIME
= 0x08
263 RAR5_XTIME_UNIXTIME_NS
= 0x10
265 RAR5_XENC_CIPHER_AES256
= 0
267 RAR5_XENC_CHECKVAL
= 0x01
268 RAR5_XENC_TWEAKED
= 0x02
270 RAR5_XHASH_BLAKE2SP
= 0
272 RAR5_XREDIR_UNIX_SYMLINK
= 1
273 RAR5_XREDIR_WINDOWS_SYMLINK
= 2
274 RAR5_XREDIR_WINDOWS_JUNCTION
= 3
275 RAR5_XREDIR_HARD_LINK
= 4
276 RAR5_XREDIR_FILE_COPY
= 5
278 RAR5_XREDIR_ISDIR
= 0x01
280 RAR5_XOWNER_UNAME
= 0x01
281 RAR5_XOWNER_GNAME
= 0x02
282 RAR5_XOWNER_UID
= 0x04
283 RAR5_XOWNER_GID
= 0x08
288 DOS_MODE_ARCHIVE
= 0x20
290 DOS_MODE_SYSTEM
= 0x04
291 DOS_MODE_HIDDEN
= 0x02
292 DOS_MODE_READONLY
= 0x01
294 RAR5_PW_CHECK_SIZE
= 8
298 ## internal constants
301 RAR_ID
= b
"Rar!\x1a\x07\x00"
302 RAR5_ID
= b
"Rar!\x1a\x07\x01\x00"
304 WIN32
= sys
.platform
== "win32"
305 BSIZE
= 512 * 1024 if WIN32
else 64 * 1024
307 SFX_MAX_SIZE
= 2 * 1024 * 1024
311 _BAD_CHARS
= r
"""\x00-\x1F<>|"?*"""
312 RC_BAD_CHARS_UNIX
= re
.compile(r
"[%s]" % _BAD_CHARS
)
313 RC_BAD_CHARS_WIN32
= re
.compile(r
"[%s:^\\]" % _BAD_CHARS
)
318 def _find_sfx_header(xfile
):
321 steps
= (64, SFX_MAX_SIZE
)
323 with
XFile(xfile
) as fd
:
329 curdata
= buf
.getvalue()
332 pos
= curdata
.find(sig
, findpos
)
335 if curdata
[pos
:pos
+ len(RAR_ID
)] == RAR_ID
:
337 if curdata
[pos
:pos
+ len(RAR5_ID
)] == RAR5_ID
:
339 findpos
= pos
+ len(sig
)
348 def get_rar_version(xfile
):
349 """Check quickly whether file is rar archive.
351 with
XFile(xfile
) as fd
:
352 buf
= fd
.read(len(RAR5_ID
))
353 if buf
.startswith(RAR_ID
):
355 elif buf
.startswith(RAR5_ID
):
360 def is_rarfile(xfile
):
361 """Check quickly whether file is rar archive.
364 return get_rar_version(xfile
) > 0
366 # File not found or not accessible, ignore
370 def is_rarfile_sfx(xfile
):
371 """Check whether file is rar archive with support for SFX.
373 It will read 2M from file.
375 return _find_sfx_header(xfile
)[0] > 0
378 class Error(Exception):
379 """Base class for rarfile errors."""
382 class BadRarFile(Error
):
383 """Incorrect data in archive."""
386 class NotRarFile(Error
):
387 """The file is not RAR archive."""
390 class BadRarName(Error
):
391 """Cannot guess multipart name components."""
394 class NoRarEntry(Error
):
395 """File not found in RAR"""
398 class PasswordRequired(Error
):
399 """File requires password"""
402 class NeedFirstVolume(Error
):
403 """Need to start from first volume.
408 Volume number of current file or None if not known
410 def __init__(self
, msg
, volume
):
411 super().__init
__(msg
)
412 self
.current_volume
= volume
415 class NoCrypto(Error
):
416 """Cannot parse encrypted headers - no crypto available."""
419 class RarExecError(Error
):
420 """Problem reported by unrar/rar."""
423 class RarWarning(RarExecError
):
424 """Non-fatal error"""
427 class RarFatalError(RarExecError
):
431 class RarCRCError(RarExecError
):
432 """CRC error during unpacking"""
435 class RarLockedArchiveError(RarExecError
):
436 """Must not modify locked archive"""
439 class RarWriteError(RarExecError
):
443 class RarOpenError(RarExecError
):
447 class RarUserError(RarExecError
):
451 class RarMemoryError(RarExecError
):
455 class RarCreateError(RarExecError
):
459 class RarNoFilesError(RarExecError
):
460 """No files that match pattern were found"""
463 class RarUserBreak(RarExecError
):
467 class RarWrongPassword(RarExecError
):
468 """Incorrect password"""
471 class RarUnknownError(RarExecError
):
472 """Unknown exit code"""
475 class RarSignalExit(RarExecError
):
476 """Unrar exited with signal"""
479 class RarCannotExec(RarExecError
):
480 """Executable not found."""
483 class UnsupportedWarning(UserWarning):
484 """Archive uses feature that are unsupported by rarfile.
486 .. versionadded:: 4.0
491 r
"""An entry in rar archive.
493 Timestamps as :class:`~datetime.datetime` are without timezone in RAR3,
494 with UTC timezone in RAR5 archives.
499 File name with relative path.
500 Path separator is "/". Always unicode string.
503 File modification timestamp. As tuple of (year, month, day, hour, minute, second).
504 RAR5 allows archives where it is missing, it's None then.
507 Optional file comment field. Unicode string. (RAR3-only)
516 Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants.
519 Minimal Rar version needed for decompressing. As (major*10 + minor),
524 RAR5 does not have such field in archive, it's simply set to 50.
527 Host OS type, one of RAR_OS_* constants.
529 RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`,
530 :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`.
532 RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`.
535 File attributes. May be either dos-style or unix-style, depending on host_os.
538 File modification time. Same value as :attr:`date_time`
539 but as :class:`~datetime.datetime` object with extended precision.
542 Optional time field: creation time. As :class:`~datetime.datetime` object.
545 Optional time field: last access time. As :class:`~datetime.datetime` object.
548 Optional time field: archival time. As :class:`~datetime.datetime` object.
552 CRC-32 of uncompressed file, unsigned int.
557 Blake2SP hash over decompressed data. (RAR5-only)
560 Volume nr, starting from 0.
563 Volume file name, where file starts.
566 If not None, file is link of some sort. Contains tuple of (type, flags, target).
569 Type is one of constants:
571 :data:`RAR5_XREDIR_UNIX_SYMLINK`
573 :data:`RAR5_XREDIR_WINDOWS_SYMLINK`
575 :data:`RAR5_XREDIR_WINDOWS_JUNCTION`
577 :data:`RAR5_XREDIR_HARD_LINK`
579 :data:`RAR5_XREDIR_FILE_COPY`
580 Current file is copy of another archive entry.
582 Flags may contain bits:
584 :data:`RAR5_XREDIR_ISDIR`
585 Symlink points to directory.
588 # zipfile-compatible fields
597 # optional extended time fields, datetime() objects.
602 extract_version
= None
621 """Returns True if entry is a directory.
623 .. versionadded:: 4.0
627 def is_symlink(self
):
628 """Returns True if entry is a symlink.
630 .. versionadded:: 4.0
635 """Returns True if entry is a normal file.
637 .. versionadded:: 4.0
641 def needs_password(self
):
642 """Returns True if data is stored password-protected.
644 if self
.type == RAR_BLOCK_FILE
:
645 return (self
.flags
& RAR_FILE_PASSWORD
) > 0
649 """Returns True if entry is a directory.
657 """Parse RAR structure, provide access to files in archive.
662 archive file name or file-like object.
664 only "r" is supported.
666 fallback charset to use, if filenames are not already Unicode-enabled.
668 debug callback, gets to see all archive entries.
670 set to False to disable CRC checks
672 Either "stop" to quietly stop parsing on errors,
673 or "strict" to raise errors. Default is "stop".
675 If True, read only single file and allow it to be middle-part
676 of multi-volume archive.
678 .. versionadded:: 4.0
681 #: File name, if available. Unicode string or None.
684 #: Archive comment. Unicode string or None.
687 def __init__(self
, file, mode
="r", charset
=None, info_callback
=None,
688 crc_check
=True, errors
="stop", part_only
=False):
689 if is_filelike(file):
690 self
.filename
= getattr(file, "name", None)
692 if isinstance(file, Path
):
697 self
._charset
= charset
or DEFAULT_CHARSET
698 self
._info
_callback
= info_callback
699 self
._crc
_check
= crc_check
700 self
._part
_only
= part_only
701 self
._password
= None
702 self
._file
_parser
= None
706 elif errors
== "strict":
709 raise ValueError("Invalid value for errors= parameter.")
712 raise NotImplementedError("RarFile supports only mode=r")
720 def __exit__(self
, typ
, value
, traceback
):
725 """Iterate over members."""
726 return iter(self
.infolist())
728 def setpassword(self
, pwd
):
729 """Sets the password to use when extracting.
732 if self
._file
_parser
:
733 if self
._file
_parser
.has_header_encryption():
734 self
._file
_parser
= None
735 if not self
._file
_parser
:
738 self
._file
_parser
.setpassword(self
._password
)
740 def needs_password(self
):
741 """Returns True if any archive entries require password for extraction.
743 return self
._file
_parser
.needs_password()
746 """Returns True if archive uses solid compression.
748 .. versionadded:: 4.2
750 return self
._file
_parser
.is_solid()
753 """Return list of filenames in archive.
755 return [f
.filename
for f
in self
.infolist()]
758 """Return RarInfo objects for all files/directories in archive.
760 return self
._file
_parser
.infolist()
762 def volumelist(self
):
763 """Returns filenames of archive volumes.
765 In case of single-volume archive, the list contains
766 just the name of main archive file.
768 return self
._file
_parser
.volumelist()
770 def getinfo(self
, name
):
771 """Return RarInfo for file.
773 return self
._file
_parser
.getinfo(name
)
775 def getinfo_orig(self
, name
):
776 """Return RarInfo for file source.
778 RAR5: if name is hard-linked or copied file,
779 returns original entry with original filename.
781 .. versionadded:: 4.1
783 return self
._file
_parser
.getinfo_orig(name
)
785 def open(self
, name
, mode
="r", pwd
=None):
786 """Returns file-like object (:class:`RarExtFile`) from where the data can be read.
788 The object implements :class:`io.RawIOBase` interface, so it can
789 be further wrapped with :class:`io.BufferedReader`
790 and :class:`io.TextIOWrapper`.
792 On older Python where io module is not available, it implements
793 only .read(), .seek(), .tell() and .close() methods.
795 The object is seekable, although the seeking is fast only on
796 uncompressed files, on compressed files the seeking is implemented
797 by reading ahead and/or restarting the decompression.
802 file name or RarInfo instance.
806 password to use for extracting.
810 raise NotImplementedError("RarFile.open() supports only mode=r")
813 inf
= self
.getinfo(name
)
815 raise io
.UnsupportedOperation("Directory does not have any data: " + inf
.filename
)
818 if inf
.needs_password():
819 pwd
= pwd
or self
._password
821 raise PasswordRequired("File %s requires password" % inf
.filename
)
825 return self
._file
_parser
.open(inf
, pwd
)
827 def read(self
, name
, pwd
=None):
828 """Return uncompressed data for archive entry.
830 For longer files using :meth:`~RarFile.open` may be better idea.
835 filename or RarInfo instance
837 password to use for extracting.
840 with self
.open(name
, "r", pwd
) as f
:
844 """Release open resources."""
847 def printdir(self
, file=None):
848 """Print archive file list to stdout or given file.
852 for f
in self
.infolist():
853 print(f
.filename
, file=file)
855 def extract(self
, member
, path
=None, pwd
=None):
856 """Extract single file into current directory.
861 filename or :class:`RarInfo` instance
863 optional destination path
865 optional password to use
867 inf
= self
.getinfo(member
)
868 return self
._extract
_one
(inf
, path
, pwd
, True)
870 def extractall(self
, path
=None, members
=None, pwd
=None):
871 """Extract all files into current directory.
876 optional destination path
878 optional filename or :class:`RarInfo` instance list to extract
880 optional password to use
883 members
= self
.namelist()
888 inf
= self
.getinfo(m
)
889 dst
= self
._extract
_one
(inf
, path
, pwd
, not inf
.is_dir())
892 dirs
.append((dst
, inf
))
895 dirs
.sort(reverse
=True)
896 for dst
, inf
in dirs
:
897 self
._set
_attrs
(inf
, dst
)
899 def testrar(self
, pwd
=None):
900 """Read all files and test CRC.
902 for member
in self
.infolist():
904 with self
.open(member
, 'r', pwd
) as f
:
905 empty_read(f
, member
.file_size
, BSIZE
)
908 """Return error string if parsing failed or None if no problems.
910 if not self
._file
_parser
:
911 return "Not a RAR file"
912 return self
._file
_parser
.strerror()
919 """Run parser for file type
921 ver
, sfx_ofs
= _find_sfx_header(self
._rarfile
)
923 p3
= RAR3Parser(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
= p3
# noqa
928 p5
= RAR5Parser(self
._rarfile
, self
._password
, self
._crc
_check
,
929 self
._charset
, self
._strict
, self
._info
_callback
,
930 sfx_ofs
, self
._part
_only
)
931 self
._file
_parser
= p5
# noqa
933 raise NotRarFile("Not a RAR file")
935 self
._file
_parser
.parse()
936 self
.comment
= self
._file
_parser
.comment
938 def _extract_one(self
, info
, path
, pwd
, set_attrs
):
939 fname
= sanitize_filename(
940 info
.filename
, os
.path
.sep
, WIN32
946 path
= os
.fspath(path
)
947 dstfn
= os
.path
.join(path
, fname
)
949 dirname
= os
.path
.dirname(dstfn
)
950 if dirname
and dirname
!= ".":
951 os
.makedirs(dirname
, exist_ok
=True)
954 return self
._make
_file
(info
, dstfn
, pwd
, set_attrs
)
956 return self
._make
_dir
(info
, dstfn
, pwd
, set_attrs
)
957 if info
.is_symlink():
958 return self
._make
_symlink
(info
, dstfn
, pwd
, set_attrs
)
961 def _create_helper(self
, name
, flags
, info
):
962 return os
.open(name
, flags
)
964 def _make_file(self
, info
, dstfn
, pwd
, set_attrs
):
965 def helper(name
, flags
):
966 return self
._create
_helper
(name
, flags
, info
)
967 with self
.open(info
, "r", pwd
) as src
:
968 with
open(dstfn
, "wb", opener
=helper
) as dst
:
969 shutil
.copyfileobj(src
, dst
)
971 self
._set
_attrs
(info
, dstfn
)
974 def _make_dir(self
, info
, dstfn
, pwd
, set_attrs
):
975 os
.makedirs(dstfn
, exist_ok
=True)
977 self
._set
_attrs
(info
, dstfn
)
980 def _make_symlink(self
, info
, dstfn
, pwd
, set_attrs
):
981 target_is_directory
= False
982 if info
.host_os
== RAR_OS_UNIX
:
983 link_name
= self
.read(info
, pwd
)
984 target_is_directory
= (info
.flags
& RAR_FILE_DIRECTORY
) == RAR_FILE_DIRECTORY
985 elif info
.file_redir
:
986 redir_type
, redir_flags
, link_name
= info
.file_redir
987 if redir_type
== RAR5_XREDIR_WINDOWS_JUNCTION
:
988 warnings
.warn(f
"Windows junction not supported - {info.filename}", UnsupportedWarning
)
990 target_is_directory
= (redir_type
& RAR5_XREDIR_ISDIR
) > 0
992 warnings
.warn(f
"Unsupported link type - {info.filename}", UnsupportedWarning
)
995 os
.symlink(link_name
, dstfn
, target_is_directory
=target_is_directory
)
998 def _set_attrs(self
, info
, dstfn
):
999 if info
.host_os
== RAR_OS_UNIX
:
1000 os
.chmod(dstfn
, info
.mode
& 0o777)
1001 elif info
.host_os
in (RAR_OS_WIN32
, RAR_OS_MSDOS
):
1002 # only keep R/O attr, except for dirs on win32
1003 if info
.mode
& DOS_MODE_READONLY
and (info
.is_file() or not WIN32
):
1005 new_mode
= st
.st_mode
& ~
0o222
1006 os
.chmod(dstfn
, new_mode
)
1009 mtime_ns
= to_nsecs(info
.mtime
)
1010 atime_ns
= to_nsecs(info
.atime
) if info
.atime
else mtime_ns
1011 os
.utime(dstfn
, ns
=(atime_ns
, mtime_ns
))
1015 # File format parsing
1019 """Shared parser parts."""
1022 _needs_password
= False
1029 def __init__(self
, rarfile
, password
, crc_check
, charset
, strict
,
1030 info_cb
, sfx_offset
, part_only
):
1031 self
._rarfile
= rarfile
1032 self
._password
= password
1033 self
._crc
_check
= crc_check
1034 self
._charset
= charset
1035 self
._strict
= strict
1036 self
._info
_callback
= info_cb
1037 self
._info
_list
= []
1040 self
._sfx
_offset
= sfx_offset
1041 self
._part
_only
= part_only
1044 """Returns True if archive uses solid compression.
1047 if self
._main
.flags
& RAR_MAIN_SOLID
:
1051 def has_header_encryption(self
):
1052 """Returns True if headers are encrypted
1054 if self
._hdrenc
_main
:
1057 if self
._main
.flags
& RAR_MAIN_PASSWORD
:
1061 def setpassword(self
, pwd
):
1062 """Set cached password."""
1063 self
._password
= pwd
1065 def volumelist(self
):
1067 return self
._vol
_list
1069 def needs_password(self
):
1070 """Is password required"""
1071 return self
._needs
_password
1075 return self
._parse
_error
1078 """List of RarInfo records.
1080 return self
._info
_list
1082 def getinfo(self
, member
):
1083 """Return RarInfo for filename
1085 if isinstance(member
, RarInfo
):
1086 fname
= member
.filename
1087 elif isinstance(member
, Path
):
1092 if fname
.endswith("/"):
1093 fname
= fname
.rstrip("/")
1096 return self
._info
_map
[fname
]
1098 raise NoRarEntry("No such file: %s" % fname
) from None
1100 def getinfo_orig(self
, member
):
1101 inf
= self
.getinfo(member
)
1103 redir_type
, redir_flags
, redir_name
= inf
.file_redir
1104 # cannot leave to unrar as it expects copied file to exist
1105 if redir_type
in (RAR5_XREDIR_FILE_COPY
, RAR5_XREDIR_HARD_LINK
):
1106 inf
= self
.getinfo(redir_name
)
1119 def _parse_real(self
):
1120 """Actually read file.
1122 fd
= XFile(self
._rarfile
)
1124 fd
.seek(self
._sfx
_offset
, 0)
1125 sig
= fd
.read(len(self
._expect
_sig
))
1126 if sig
!= self
._expect
_sig
:
1127 raise NotRarFile("Not a Rar archive")
1129 volume
= 0 # first vol (.rar) is 0
1132 volfile
= self
._rarfile
1133 self
._vol
_list
= [self
._rarfile
]
1134 raise_need_first_vol
= False
1137 h
= None # don"t read past ENDARC
1139 h
= self
._parse
_header
(fd
)
1141 if raise_need_first_vol
:
1142 # did not find ENDARC with VOLNR
1143 raise NeedFirstVolume("Need to start from first volume", None)
1144 if more_vols
and not self
._part
_only
:
1148 volfile
= self
._next
_volname
(volfile
)
1151 self
._set
_error
("Cannot open next volume: %s", volfile
)
1154 sig
= fd
.read(len(self
._expect
_sig
))
1155 if sig
!= self
._expect
_sig
:
1156 self
._set
_error
("Invalid volume sig: %s", volfile
)
1160 self
._vol
_list
.append(volfile
)
1162 self
._hdrenc
_main
= None
1166 h
.volume_file
= volfile
1168 if h
.type == RAR_BLOCK_MAIN
and not self
._main
:
1170 if volume
== 0 and (h
.flags
& RAR_MAIN_NEWNUMBERING
) and not self
._part
_only
:
1171 # RAR 2.x does not set FIRSTVOLUME,
1172 # so check it only if NEWNUMBERING is used
1173 if (h
.flags
& RAR_MAIN_FIRSTVOLUME
) == 0:
1174 if getattr(h
, "main_volume_number", None) is not None:
1175 # rar5 may have more info
1176 raise NeedFirstVolume(
1177 "Need to start from first volume (current: %r)"
1178 % (h
.main_volume_number
,),
1179 h
.main_volume_number
1181 # delay raise until we have volnr from ENDARC
1182 raise_need_first_vol
= True
1183 if h
.flags
& RAR_MAIN_PASSWORD
:
1184 self
._needs
_password
= True
1185 if not self
._password
:
1187 elif h
.type == RAR_BLOCK_ENDARC
:
1188 # use flag, but also allow RAR 2.x logic below to trigger
1189 if h
.flags
& RAR_ENDARC_NEXT_VOLUME
:
1192 if raise_need_first_vol
and (h
.flags
& RAR_ENDARC_VOLNR
) > 0:
1193 raise NeedFirstVolume(
1194 "Need to start from first volume (current: %r)"
1195 % (h
.endarc_volnr
,),
1198 elif h
.type == RAR_BLOCK_FILE
:
1199 # RAR 2.x does not write RAR_BLOCK_ENDARC
1200 if h
.flags
& RAR_FILE_SPLIT_AFTER
:
1202 # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
1203 if volume
== 0 and h
.flags
& RAR_FILE_SPLIT_BEFORE
:
1204 if not self
._part
_only
:
1205 raise_need_first_vol
= True
1207 if h
.needs_password():
1208 self
._needs
_password
= True
1211 self
.process_entry(fd
, h
)
1213 if self
._info
_callback
:
1214 self
._info
_callback
(h
)
1218 fd
.seek(h
.data_offset
+ h
.add_size
, 0)
1220 def process_entry(self
, fd
, item
):
1221 """Examine item, add into lookup cache."""
1222 raise NotImplementedError()
1224 def _decrypt_header(self
, fd
):
1225 raise NotImplementedError("_decrypt_header")
1227 def _parse_block_header(self
, fd
):
1228 raise NotImplementedError("_parse_block_header")
1230 def _open_hack(self
, inf
, pwd
):
1231 raise NotImplementedError("_open_hack")
1233 def _parse_header(self
, fd
):
1234 """Read single header
1237 # handle encrypted headers
1238 if (self
._main
and self
._main
.flags
& RAR_MAIN_PASSWORD
) or self
._hdrenc
_main
:
1239 if not self
._password
:
1241 fd
= self
._decrypt
_header
(fd
)
1243 # now read actual header
1244 return self
._parse
_block
_header
(fd
)
1245 except struct
.error
:
1246 self
._set
_error
("Broken header in RAR file")
1249 def _next_volname(self
, volfile
):
1250 """Given current vol name, construct next one
1252 if is_filelike(volfile
):
1253 raise IOError("Working on single FD")
1254 if self
._main
.flags
& RAR_MAIN_NEWNUMBERING
:
1255 return _next_newvol(volfile
)
1256 return _next_oldvol(volfile
)
1258 def _set_error(self
, msg
, *args
):
1261 self
._parse
_error
= msg
1263 raise BadRarFile(msg
)
1265 def open(self
, inf
, pwd
):
1266 """Return stream object for file data."""
1269 redir_type
, redir_flags
, redir_name
= inf
.file_redir
1270 # cannot leave to unrar as it expects copied file to exist
1271 if redir_type
in (RAR5_XREDIR_FILE_COPY
, RAR5_XREDIR_HARD_LINK
):
1272 inf
= self
.getinfo(redir_name
)
1274 raise BadRarFile("cannot find copied file")
1275 elif redir_type
in (
1276 RAR5_XREDIR_UNIX_SYMLINK
, RAR5_XREDIR_WINDOWS_SYMLINK
,
1277 RAR5_XREDIR_WINDOWS_JUNCTION
,
1279 return io
.BytesIO(redir_name
.encode("utf8"))
1280 if inf
.flags
& RAR_FILE_SPLIT_BEFORE
:
1281 raise NeedFirstVolume("Partial file, please start from first volume: " + inf
.filename
, None)
1283 # is temp write usable?
1287 elif self
._main
._must
_disable
_hack
():
1289 elif inf
._must
_disable
_hack
():
1291 elif is_filelike(self
._rarfile
):
1293 elif inf
.file_size
> HACK_SIZE_LIMIT
:
1295 elif not USE_EXTRACT_HACK
:
1299 if inf
.compress_type
== RAR_M0
and (inf
.flags
& RAR_FILE_PASSWORD
) == 0 and inf
.file_redir
is None:
1300 return self
._open
_clear
(inf
)
1302 return self
._open
_hack
(inf
, pwd
)
1303 elif is_filelike(self
._rarfile
):
1304 return self
._open
_unrar
_membuf
(self
._rarfile
, inf
, pwd
)
1306 return self
._open
_unrar
(self
._rarfile
, inf
, pwd
)
1308 def _open_clear(self
, inf
):
1310 return self
._open
_unrar
(self
._rarfile
, inf
)
1311 return DirectReader(self
, inf
)
1313 def _open_hack_core(self
, inf
, pwd
, prefix
, suffix
):
1315 size
= inf
.compress_size
+ inf
.header_size
1316 rf
= XFile(inf
.volume_file
, 0)
1317 rf
.seek(inf
.header_offset
)
1319 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
1320 tmpf
= os
.fdopen(tmpfd
, "wb")
1326 buf
= rf
.read(BSIZE
)
1330 raise BadRarFile("read failed: " + inf
.filename
)
1336 except BaseException
:
1342 return self
._open
_unrar
(tmpname
, inf
, pwd
, tmpname
)
1344 def _open_unrar_membuf(self
, memfile
, inf
, pwd
):
1345 """Write in-memory archive to temp file, needed for solid archives.
1347 tmpname
= membuf_tempfile(memfile
)
1348 return self
._open
_unrar
(tmpname
, inf
, pwd
, tmpname
, force_file
=True)
1350 def _open_unrar(self
, rarfile
, inf
, pwd
=None, tmpfile
=None, force_file
=False):
1351 """Extract using unrar
1353 setup
= tool_setup()
1355 # not giving filename avoids encoding related problems
1357 if not tmpfile
or force_file
:
1358 fn
= inf
.filename
.replace("/", os
.path
.sep
)
1360 # read from unrar pipe
1361 cmd
= setup
.open_cmdline(pwd
, rarfile
, fn
)
1362 return PipeReader(self
, inf
, cmd
, tmpfile
)
1369 class Rar3Info(RarInfo
):
1370 """RAR3 specific fields."""
1371 extract_version
= 15
1376 header_offset
= None
1382 # make sure some rar5 fields are always present
1384 blake2sp_hash
= None
1386 endarc_datacrc
= None
1389 def _must_disable_hack(self
):
1390 if self
.type == RAR_BLOCK_FILE
:
1391 if self
.flags
& RAR_FILE_PASSWORD
:
1393 elif self
.flags
& (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER
):
1395 elif self
.type == RAR_BLOCK_MAIN
:
1396 if self
.flags
& (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD
):
1401 """Returns True if entry is a directory."""
1402 if self
.type == RAR_BLOCK_FILE
and not self
.is_symlink():
1403 return (self
.flags
& RAR_FILE_DIRECTORY
) == RAR_FILE_DIRECTORY
1406 def is_symlink(self
):
1407 """Returns True if entry is a symlink."""
1409 self
.type == RAR_BLOCK_FILE
and
1410 self
.host_os
== RAR_OS_UNIX
and
1411 self
.mode
& 0xF000 == 0xA000
1415 """Returns True if entry is a normal file."""
1417 self
.type == RAR_BLOCK_FILE
and
1418 not (self
.is_dir() or self
.is_symlink())
1422 class RAR3Parser(CommonParser
):
1423 """Parse RAR3 file format.
1425 _expect_sig
= RAR_ID
1426 _last_aes_key
= (None, None, None) # (salt, key, iv)
1428 def _decrypt_header(self
, fd
):
1429 if not _have_crypto
:
1430 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1432 if self
._last
_aes
_key
[0] == salt
:
1433 key
, iv
= self
._last
_aes
_key
[1:]
1435 key
, iv
= rar3_s2k(self
._password
, salt
)
1436 self
._last
_aes
_key
= (salt
, key
, iv
)
1437 return HeaderDecrypt(fd
, key
, iv
)
1439 def _parse_block_header(self
, fd
):
1440 """Parse common block header
1443 h
.header_offset
= fd
.tell()
1445 # read and parse base header
1446 buf
= fd
.read(S_BLK_HDR
.size
)
1449 if len(buf
) < S_BLK_HDR
.size
:
1450 self
._set
_error
("Unexpected EOF when reading header")
1452 t
= S_BLK_HDR
.unpack_from(buf
)
1453 h
.header_crc
, h
.type, h
.flags
, h
.header_size
= t
1456 if h
.header_size
> S_BLK_HDR
.size
:
1457 hdata
= buf
+ fd
.read(h
.header_size
- S_BLK_HDR
.size
)
1460 h
.data_offset
= fd
.tell()
1463 if len(hdata
) != h
.header_size
:
1464 self
._set
_error
("Unexpected EOF when reading header")
1467 pos
= S_BLK_HDR
.size
1469 # block has data assiciated with it?
1470 if h
.flags
& RAR_LONG_BLOCK
:
1471 h
.add_size
, pos
= load_le32(hdata
, pos
)
1475 # parse interesting ones, decide header boundaries for crc
1476 if h
.type == RAR_BLOCK_MARK
:
1478 elif h
.type == RAR_BLOCK_MAIN
:
1480 if h
.flags
& RAR_MAIN_ENCRYPTVER
:
1483 if h
.flags
& RAR_MAIN_COMMENT
:
1484 self
._parse
_subblocks
(h
, hdata
, pos
)
1485 elif h
.type == RAR_BLOCK_FILE
:
1486 pos
= self
._parse
_file
_header
(h
, hdata
, pos
- 4)
1488 if h
.flags
& RAR_FILE_COMMENT
:
1489 pos
= self
._parse
_subblocks
(h
, hdata
, pos
)
1490 elif h
.type == RAR_BLOCK_SUB
:
1491 pos
= self
._parse
_file
_header
(h
, hdata
, pos
- 4)
1492 crc_pos
= h
.header_size
1493 elif h
.type == RAR_BLOCK_OLD_AUTH
:
1496 elif h
.type == RAR_BLOCK_OLD_EXTRA
:
1499 elif h
.type == RAR_BLOCK_ENDARC
:
1500 if h
.flags
& RAR_ENDARC_DATACRC
:
1501 h
.endarc_datacrc
, pos
= load_le32(hdata
, pos
)
1502 if h
.flags
& RAR_ENDARC_VOLNR
:
1503 h
.endarc_volnr
= S_SHORT
.unpack_from(hdata
, pos
)[0]
1505 crc_pos
= h
.header_size
1507 crc_pos
= h
.header_size
1510 if h
.type == RAR_BLOCK_OLD_SUB
:
1511 crcdat
= hdata
[2:] + fd
.read(h
.add_size
)
1513 crcdat
= hdata
[2:crc_pos
]
1515 calc_crc
= crc32(crcdat
) & 0xFFFF
1517 # return good header
1518 if h
.header_crc
== calc_crc
:
1521 # header parsing failed.
1522 self
._set
_error
("Header CRC error (%02x): exp=%x got=%x (xlen = %d)",
1523 h
.type, h
.header_crc
, calc_crc
, len(crcdat
))
1525 # instead panicing, send eof
1528 def _parse_file_header(self
, h
, hdata
, pos
):
1529 """Read file-specific header
1531 fld
= S_FILE_HDR
.unpack_from(hdata
, pos
)
1532 pos
+= S_FILE_HDR
.size
1534 h
.compress_size
= fld
[0]
1535 h
.file_size
= fld
[1]
1538 h
.date_time
= parse_dos_time(fld
[4])
1539 h
.mtime
= to_datetime(h
.date_time
)
1540 h
.extract_version
= fld
[5]
1541 h
.compress_type
= fld
[6]
1542 h
._name
_size
= name_size
= fld
[7]
1545 h
._md
_class
= CRC32Context
1546 h
._md
_expect
= h
.CRC
1548 if h
.flags
& RAR_FILE_LARGE
:
1549 h1
, pos
= load_le32(hdata
, pos
)
1550 h2
, pos
= load_le32(hdata
, pos
)
1551 h
.compress_size |
= h1
<< 32
1552 h
.file_size |
= h2
<< 32
1553 h
.add_size
= h
.compress_size
1555 name
, pos
= load_bytes(hdata
, name_size
, pos
)
1556 if h
.flags
& RAR_FILE_UNICODE
and b
"\0" in name
:
1557 # stored in custom encoding
1558 nul
= name
.find(b
"\0")
1559 h
.orig_filename
= name
[:nul
]
1560 u
= UnicodeFilename(h
.orig_filename
, name
[nul
+ 1:])
1561 h
.filename
= u
.decode()
1563 # if parsing failed fall back to simple name
1565 h
.filename
= self
._decode
(h
.orig_filename
)
1566 elif h
.flags
& RAR_FILE_UNICODE
:
1568 h
.orig_filename
= name
1569 h
.filename
= name
.decode("utf8", "replace")
1571 # stored in random encoding
1572 h
.orig_filename
= name
1573 h
.filename
= self
._decode
(name
)
1575 # change separator, set dir suffix
1576 h
.filename
= h
.filename
.replace("\\", "/").rstrip("/")
1578 h
.filename
= h
.filename
+ "/"
1580 if h
.flags
& RAR_FILE_SALT
:
1581 h
.salt
, pos
= load_bytes(hdata
, 8, pos
)
1585 # optional extended time stamps
1586 if h
.flags
& RAR_FILE_EXTTIME
:
1587 pos
= _parse_ext_time(h
, hdata
, pos
)
1589 h
.mtime
= h
.atime
= h
.ctime
= h
.arctime
= None
1593 def _parse_subblocks(self
, h
, hdata
, pos
):
1594 """Find old-style comment subblock
1596 while pos
< len(hdata
):
1597 # ordinary block header
1598 t
= S_BLK_HDR
.unpack_from(hdata
, pos
)
1599 ___scrc
, stype
, sflags
, slen
= t
1600 pos_next
= pos
+ slen
1601 pos
+= S_BLK_HDR
.size
1607 # followed by block-specific header
1608 if stype
== RAR_BLOCK_OLD_COMMENT
and pos
+ S_COMMENT_HDR
.size
<= pos_next
:
1609 declen
, ver
, meth
, crc
= S_COMMENT_HDR
.unpack_from(hdata
, pos
)
1610 pos
+= S_COMMENT_HDR
.size
1611 data
= hdata
[pos
: pos_next
]
1612 cmt
= rar3_decompress(ver
, meth
, data
, declen
, sflags
,
1613 crc
, self
._password
)
1614 if not self
._crc
_check
or (crc32(cmt
) & 0xFFFF == crc
):
1615 h
.comment
= self
._decode
_comment
(cmt
)
1620 def _read_comment_v3(self
, inf
, pwd
=None):
1623 with
XFile(inf
.volume_file
) as rf
:
1624 rf
.seek(inf
.data_offset
)
1625 data
= rf
.read(inf
.compress_size
)
1628 cmt
= rar3_decompress(inf
.extract_version
, inf
.compress_type
, data
,
1629 inf
.file_size
, inf
.flags
, inf
.CRC
, pwd
, inf
.salt
)
1637 return self
._decode
_comment
(cmt
)
1639 def _decode(self
, val
):
1640 for c
in TRY_ENCODINGS
:
1642 return val
.decode(c
)
1643 except UnicodeError:
1645 return val
.decode(self
._charset
, "replace")
1647 def _decode_comment(self
, val
):
1648 return self
._decode
(val
)
1650 def process_entry(self
, fd
, item
):
1651 if item
.type == RAR_BLOCK_FILE
:
1652 # use only first part
1653 if item
.flags
& RAR_FILE_VERSION
:
1654 pass # skip old versions
1655 elif (item
.flags
& RAR_FILE_SPLIT_BEFORE
) == 0:
1656 self
._info
_map
[item
.filename
.rstrip("/")] = item
1657 self
._info
_list
.append(item
)
1658 elif len(self
._info
_list
) > 0:
1659 # final crc is in last block
1660 old
= self
._info
_list
[-1]
1662 old
._md
_expect
= item
._md
_expect
1663 old
.compress_size
+= item
.compress_size
1665 # parse new-style comment
1666 if item
.type == RAR_BLOCK_SUB
and item
.filename
== "CMT":
1667 if item
.flags
& (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER
):
1669 elif item
.flags
& RAR_FILE_SOLID
:
1671 cmt
= self
._read
_comment
_v
3(item
, self
._password
)
1672 if len(self
._info
_list
) > 0:
1673 old
= self
._info
_list
[-1]
1677 cmt
= self
._read
_comment
_v
3(item
, self
._password
)
1680 if item
.type == RAR_BLOCK_MAIN
:
1681 if item
.flags
& RAR_MAIN_COMMENT
:
1682 self
.comment
= item
.comment
1683 if item
.flags
& RAR_MAIN_PASSWORD
:
1684 self
._needs
_password
= True
1686 # put file compressed data into temporary .rar archive, and run
1687 # unrar on that, thus avoiding unrar going over whole archive
1688 def _open_hack(self
, inf
, pwd
):
1689 # create main header: crc, type, flags, size, res1, res2
1690 prefix
= RAR_ID
+ S_BLK_HDR
.pack(0x90CF, 0x73, 0, 13) + b
"\0" * (2 + 4)
1691 return self
._open
_hack
_core
(inf
, pwd
, prefix
, b
"")
1698 class Rar5Info(RarInfo
):
1699 """Shared fields for RAR5 records.
1701 extract_version
= 50
1704 header_offset
= None
1711 block_extra_size
= 0
1714 volume_number
= None
1718 def _must_disable_hack(self
):
1722 class Rar5BaseFile(Rar5Info
):
1723 """Shared sturct for file & service record.
1727 file_encryption
= (0, 0, 0, b
"", b
"", b
"")
1728 file_compress_flags
= None
1732 blake2sp_hash
= None
1734 def _must_disable_hack(self
):
1735 if self
.flags
& RAR_FILE_PASSWORD
:
1737 if self
.block_flags
& (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER
):
1739 if self
.file_compress_flags
& RAR5_COMPR_SOLID
:
1746 class Rar5FileInfo(Rar5BaseFile
):
1747 """RAR5 file record.
1749 type = RAR_BLOCK_FILE
1751 def is_symlink(self
):
1752 """Returns True if entry is a symlink."""
1753 # pylint: disable=unsubscriptable-object
1755 self
.file_redir
is not None and
1756 self
.file_redir
[0] in (
1757 RAR5_XREDIR_UNIX_SYMLINK
,
1758 RAR5_XREDIR_WINDOWS_SYMLINK
,
1759 RAR5_XREDIR_WINDOWS_JUNCTION
,
1764 """Returns True if entry is a normal file."""
1765 return not (self
.is_dir() or self
.is_symlink())
1768 """Returns True if entry is a directory."""
1769 if not self
.file_redir
:
1770 if self
.file_flags
& RAR5_FILE_FLAG_ISDIR
:
1775 class Rar5ServiceInfo(Rar5BaseFile
):
1776 """RAR5 service record.
1778 type = RAR_BLOCK_SUB
1781 class Rar5MainInfo(Rar5Info
):
1782 """RAR5 archive main record.
1784 type = RAR_BLOCK_MAIN
1786 main_volume_number
= None
1788 def _must_disable_hack(self
):
1789 if self
.main_flags
& RAR5_MAIN_FLAG_SOLID
:
1794 class Rar5EncryptionInfo(Rar5Info
):
1795 """RAR5 archive header encryption record.
1797 type = RAR5_BLOCK_ENCRYPTION
1798 encryption_algo
= None
1799 encryption_flags
= None
1800 encryption_kdf_count
= None
1801 encryption_salt
= None
1802 encryption_check_value
= None
1804 def needs_password(self
):
1808 class Rar5EndArcInfo(Rar5Info
):
1809 """RAR5 end of archive record.
1811 type = RAR_BLOCK_ENDARC
1815 class RAR5Parser(CommonParser
):
1816 """Parse RAR5 format.
1818 _expect_sig
= RAR5_ID
1821 # AES encrypted headers
1822 _last_aes256_key
= (-1, None, None) # (kdf_count, salt, key)
1824 def _get_utf8_password(self
):
1825 pwd
= self
._password
1826 if isinstance(pwd
, str):
1827 return pwd
.encode("utf8")
1830 def _gen_key(self
, kdf_count
, salt
):
1831 if self
._last
_aes
256_key
[:2] == (kdf_count
, salt
):
1832 return self
._last
_aes
256_key
[2]
1833 if kdf_count
> RAR_MAX_KDF_SHIFT
:
1834 raise BadRarFile("Too large kdf_count")
1835 pwd
= self
._get
_utf
8_password
()
1836 key
= rar5_s2k(pwd
, salt
, 1 << kdf_count
)
1837 self
._last
_aes
256_key
= (kdf_count
, salt
, key
)
1840 def _decrypt_header(self
, fd
):
1841 if not _have_crypto
:
1842 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1843 h
= self
._hdrenc
_main
1844 key
= self
._gen
_key
(h
.encryption_kdf_count
, h
.encryption_salt
)
1846 return HeaderDecrypt(fd
, key
, iv
)
1848 def _parse_block_header(self
, fd
):
1849 """Parse common block header
1851 header_offset
= fd
.tell()
1854 start_bytes
= fd
.read(preload
)
1855 if len(start_bytes
) < preload
:
1856 self
._set
_error
("Unexpected EOF when reading header")
1858 while start_bytes
[-1] & 0x80:
1861 self
._set
_error
("Unexpected EOF when reading header")
1864 header_crc
, pos
= load_le32(start_bytes
, 0)
1865 hdrlen
, pos
= load_vint(start_bytes
, pos
)
1866 if hdrlen
> 2 * 1024 * 1024:
1868 header_size
= pos
+ hdrlen
1870 # read full header, check for EOF
1871 hdata
= start_bytes
+ fd
.read(header_size
- len(start_bytes
))
1872 if len(hdata
) != header_size
:
1873 self
._set
_error
("Unexpected EOF when reading header")
1875 data_offset
= fd
.tell()
1877 calc_crc
= crc32(memoryview(hdata
)[4:])
1878 if header_crc
!= calc_crc
:
1879 # header parsing failed.
1880 self
._set
_error
("Header CRC error: exp=%x got=%x (xlen = %d)",
1881 header_crc
, calc_crc
, len(hdata
))
1884 block_type
, pos
= load_vint(hdata
, pos
)
1886 if block_type
== RAR5_BLOCK_MAIN
:
1887 h
, pos
= self
._parse
_block
_common
(Rar5MainInfo(), hdata
)
1888 h
= self
._parse
_main
_block
(h
, hdata
, pos
)
1889 elif block_type
== RAR5_BLOCK_FILE
:
1890 h
, pos
= self
._parse
_block
_common
(Rar5FileInfo(), hdata
)
1891 h
= self
._parse
_file
_block
(h
, hdata
, pos
)
1892 elif block_type
== RAR5_BLOCK_SERVICE
:
1893 h
, pos
= self
._parse
_block
_common
(Rar5ServiceInfo(), hdata
)
1894 h
= self
._parse
_file
_block
(h
, hdata
, pos
)
1895 elif block_type
== RAR5_BLOCK_ENCRYPTION
:
1896 h
, pos
= self
._parse
_block
_common
(Rar5EncryptionInfo(), hdata
)
1897 h
= self
._parse
_encryption
_block
(h
, hdata
, pos
)
1898 elif block_type
== RAR5_BLOCK_ENDARC
:
1899 h
, pos
= self
._parse
_block
_common
(Rar5EndArcInfo(), hdata
)
1900 h
= self
._parse
_endarc
_block
(h
, hdata
, pos
)
1904 h
.header_offset
= header_offset
1905 h
.data_offset
= data_offset
1908 def _parse_block_common(self
, h
, hdata
):
1909 h
.header_crc
, pos
= load_le32(hdata
, 0)
1910 hdrlen
, pos
= load_vint(hdata
, pos
)
1911 h
.header_size
= hdrlen
+ pos
1912 h
.block_type
, pos
= load_vint(hdata
, pos
)
1913 h
.block_flags
, pos
= load_vint(hdata
, pos
)
1915 if h
.block_flags
& RAR5_BLOCK_FLAG_EXTRA_DATA
:
1916 h
.block_extra_size
, pos
= load_vint(hdata
, pos
)
1917 if h
.block_flags
& RAR5_BLOCK_FLAG_DATA_AREA
:
1918 h
.add_size
, pos
= load_vint(hdata
, pos
)
1920 h
.compress_size
= h
.add_size
1922 if h
.block_flags
& RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN
:
1923 h
.flags |
= RAR_SKIP_IF_UNKNOWN
1924 if h
.block_flags
& RAR5_BLOCK_FLAG_DATA_AREA
:
1925 h
.flags |
= RAR_LONG_BLOCK
1928 def _parse_main_block(self
, h
, hdata
, pos
):
1929 h
.main_flags
, pos
= load_vint(hdata
, pos
)
1930 if h
.main_flags
& RAR5_MAIN_FLAG_HAS_VOLNR
:
1931 h
.main_volume_number
, pos
= load_vint(hdata
, pos
)
1933 h
.flags |
= RAR_MAIN_NEWNUMBERING
1934 if h
.main_flags
& RAR5_MAIN_FLAG_SOLID
:
1935 h
.flags |
= RAR_MAIN_SOLID
1936 if h
.main_flags
& RAR5_MAIN_FLAG_ISVOL
:
1937 h
.flags |
= RAR_MAIN_VOLUME
1938 if h
.main_flags
& RAR5_MAIN_FLAG_RECOVERY
:
1939 h
.flags |
= RAR_MAIN_RECOVERY
1940 if self
._hdrenc
_main
:
1941 h
.flags |
= RAR_MAIN_PASSWORD
1942 if h
.main_flags
& RAR5_MAIN_FLAG_HAS_VOLNR
== 0:
1943 h
.flags |
= RAR_MAIN_FIRSTVOLUME
1947 def _parse_file_block(self
, h
, hdata
, pos
):
1948 h
.file_flags
, pos
= load_vint(hdata
, pos
)
1949 h
.file_size
, pos
= load_vint(hdata
, pos
)
1950 h
.mode
, pos
= load_vint(hdata
, pos
)
1952 if h
.file_flags
& RAR5_FILE_FLAG_HAS_MTIME
:
1953 h
.mtime
, pos
= load_unixtime(hdata
, pos
)
1954 h
.date_time
= h
.mtime
.timetuple()[:6]
1955 if h
.file_flags
& RAR5_FILE_FLAG_HAS_CRC32
:
1956 h
.CRC
, pos
= load_le32(hdata
, pos
)
1957 h
._md
_class
= CRC32Context
1958 h
._md
_expect
= h
.CRC
1960 h
.file_compress_flags
, pos
= load_vint(hdata
, pos
)
1961 h
.file_host_os
, pos
= load_vint(hdata
, pos
)
1962 h
.orig_filename
, pos
= load_vstr(hdata
, pos
)
1963 h
.filename
= h
.orig_filename
.decode("utf8", "replace").rstrip("/")
1965 # use compatible values
1966 if h
.file_host_os
== RAR5_OS_WINDOWS
:
1967 h
.host_os
= RAR_OS_WIN32
1969 h
.host_os
= RAR_OS_UNIX
1970 h
.compress_type
= RAR_M0
+ ((h
.file_compress_flags
>> 7) & 7)
1972 if h
.block_extra_size
:
1973 # allow 1 byte of garbage
1974 while pos
< len(hdata
) - 1:
1975 xsize
, pos
= load_vint(hdata
, pos
)
1976 xdata
, pos
= load_bytes(hdata
, xsize
, pos
)
1977 self
._process
_file
_extra
(h
, xdata
)
1979 if h
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_BEFORE
:
1980 h
.flags |
= RAR_FILE_SPLIT_BEFORE
1981 if h
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_AFTER
:
1982 h
.flags |
= RAR_FILE_SPLIT_AFTER
1983 if h
.file_flags
& RAR5_FILE_FLAG_ISDIR
:
1984 h
.flags |
= RAR_FILE_DIRECTORY
1985 if h
.file_compress_flags
& RAR5_COMPR_SOLID
:
1986 h
.flags |
= RAR_FILE_SOLID
1989 h
.filename
= h
.filename
+ "/"
1992 def _parse_endarc_block(self
, h
, hdata
, pos
):
1993 h
.endarc_flags
, pos
= load_vint(hdata
, pos
)
1994 if h
.endarc_flags
& RAR5_ENDARC_FLAG_NEXT_VOL
:
1995 h
.flags |
= RAR_ENDARC_NEXT_VOLUME
1998 def _check_password(self
, check_value
, kdf_count_shift
, salt
):
1999 if len(check_value
) != RAR5_PW_CHECK_SIZE
+ RAR5_PW_SUM_SIZE
:
2001 if kdf_count_shift
> RAR_MAX_KDF_SHIFT
:
2002 raise BadRarFile("Too large kdf_count")
2004 hdr_check
= check_value
[:RAR5_PW_CHECK_SIZE
]
2005 hdr_sum
= check_value
[RAR5_PW_CHECK_SIZE
:]
2006 sum_hash
= sha256(hdr_check
).digest()
2007 if sum_hash
[:RAR5_PW_SUM_SIZE
] != hdr_sum
:
2010 kdf_count
= (1 << kdf_count_shift
) + 32
2011 pwd
= self
._get
_utf
8_password
()
2012 pwd_hash
= rar5_s2k(pwd
, salt
, kdf_count
)
2014 pwd_check
= bytearray(RAR5_PW_CHECK_SIZE
)
2015 len_mask
= RAR5_PW_CHECK_SIZE
- 1
2016 for i
, v
in enumerate(pwd_hash
):
2017 pwd_check
[i
& len_mask
] ^
= v
2019 if pwd_check
!= hdr_check
:
2020 raise RarWrongPassword()
2022 def _parse_encryption_block(self
, h
, hdata
, pos
):
2023 h
.encryption_algo
, pos
= load_vint(hdata
, pos
)
2024 h
.encryption_flags
, pos
= load_vint(hdata
, pos
)
2025 h
.encryption_kdf_count
, pos
= load_byte(hdata
, pos
)
2026 h
.encryption_salt
, pos
= load_bytes(hdata
, 16, pos
)
2027 if h
.encryption_flags
& RAR5_ENC_FLAG_HAS_CHECKVAL
:
2028 h
.encryption_check_value
, pos
= load_bytes(hdata
, 12, pos
)
2029 if h
.encryption_algo
!= RAR5_XENC_CIPHER_AES256
:
2030 raise BadRarFile("Unsupported header encryption cipher")
2031 if h
.encryption_check_value
and self
._password
:
2032 self
._check
_password
(h
.encryption_check_value
, h
.encryption_kdf_count
, h
.encryption_salt
)
2033 self
._hdrenc
_main
= h
2036 def _process_file_extra(self
, h
, xdata
):
2037 xtype
, pos
= load_vint(xdata
, 0)
2038 if xtype
== RAR5_XFILE_TIME
:
2039 self
._parse
_file
_xtime
(h
, xdata
, pos
)
2040 elif xtype
== RAR5_XFILE_ENCRYPTION
:
2041 self
._parse
_file
_encryption
(h
, xdata
, pos
)
2042 elif xtype
== RAR5_XFILE_HASH
:
2043 self
._parse
_file
_hash
(h
, xdata
, pos
)
2044 elif xtype
== RAR5_XFILE_VERSION
:
2045 self
._parse
_file
_version
(h
, xdata
, pos
)
2046 elif xtype
== RAR5_XFILE_REDIR
:
2047 self
._parse
_file
_redir
(h
, xdata
, pos
)
2048 elif xtype
== RAR5_XFILE_OWNER
:
2049 self
._parse
_file
_owner
(h
, xdata
, pos
)
2050 elif xtype
== RAR5_XFILE_SERVICE
:
2055 # extra block for file time record
2056 def _parse_file_xtime(self
, h
, xdata
, pos
):
2057 tflags
, pos
= load_vint(xdata
, pos
)
2059 ldr
= load_windowstime
2060 if tflags
& RAR5_XTIME_UNIXTIME
:
2063 if tflags
& RAR5_XTIME_HAS_MTIME
:
2064 h
.mtime
, pos
= ldr(xdata
, pos
)
2065 h
.date_time
= h
.mtime
.timetuple()[:6]
2066 if tflags
& RAR5_XTIME_HAS_CTIME
:
2067 h
.ctime
, pos
= ldr(xdata
, pos
)
2068 if tflags
& RAR5_XTIME_HAS_ATIME
:
2069 h
.atime
, pos
= ldr(xdata
, pos
)
2071 if tflags
& RAR5_XTIME_UNIXTIME_NS
:
2072 if tflags
& RAR5_XTIME_HAS_MTIME
:
2073 nsec
, pos
= load_le32(xdata
, pos
)
2074 h
.mtime
= to_nsdatetime(h
.mtime
, nsec
)
2075 if tflags
& RAR5_XTIME_HAS_CTIME
:
2076 nsec
, pos
= load_le32(xdata
, pos
)
2077 h
.ctime
= to_nsdatetime(h
.ctime
, nsec
)
2078 if tflags
& RAR5_XTIME_HAS_ATIME
:
2079 nsec
, pos
= load_le32(xdata
, pos
)
2080 h
.atime
= to_nsdatetime(h
.atime
, nsec
)
2082 # just remember encryption info
2083 def _parse_file_encryption(self
, h
, xdata
, pos
):
2084 algo
, pos
= load_vint(xdata
, pos
)
2085 flags
, pos
= load_vint(xdata
, pos
)
2086 kdf_count
, pos
= load_byte(xdata
, pos
)
2087 salt
, pos
= load_bytes(xdata
, 16, pos
)
2088 iv
, pos
= load_bytes(xdata
, 16, pos
)
2090 if flags
& RAR5_XENC_CHECKVAL
:
2091 checkval
, pos
= load_bytes(xdata
, 12, pos
)
2092 if flags
& RAR5_XENC_TWEAKED
:
2094 h
._md
_class
= NoHashContext
2096 h
.file_encryption
= (algo
, flags
, kdf_count
, salt
, iv
, checkval
)
2097 h
.flags |
= RAR_FILE_PASSWORD
2099 def _parse_file_hash(self
, h
, xdata
, pos
):
2100 hash_type
, pos
= load_vint(xdata
, pos
)
2101 if hash_type
== RAR5_XHASH_BLAKE2SP
:
2102 h
.blake2sp_hash
, pos
= load_bytes(xdata
, 32, pos
)
2103 if (h
.file_encryption
[1] & RAR5_XENC_TWEAKED
) == 0:
2104 h
._md
_class
= Blake2SP
2105 h
._md
_expect
= h
.blake2sp_hash
2107 def _parse_file_version(self
, h
, xdata
, pos
):
2108 flags
, pos
= load_vint(xdata
, pos
)
2109 version
, pos
= load_vint(xdata
, pos
)
2110 h
.file_version
= (flags
, version
)
2112 def _parse_file_redir(self
, h
, xdata
, pos
):
2113 redir_type
, pos
= load_vint(xdata
, pos
)
2114 redir_flags
, pos
= load_vint(xdata
, pos
)
2115 redir_name
, pos
= load_vstr(xdata
, pos
)
2116 redir_name
= redir_name
.decode("utf8", "replace")
2117 h
.file_redir
= (redir_type
, redir_flags
, redir_name
)
2119 def _parse_file_owner(self
, h
, xdata
, pos
):
2120 user_name
= group_name
= user_id
= group_id
= None
2122 flags
, pos
= load_vint(xdata
, pos
)
2123 if flags
& RAR5_XOWNER_UNAME
:
2124 user_name
, pos
= load_vstr(xdata
, pos
)
2125 if flags
& RAR5_XOWNER_GNAME
:
2126 group_name
, pos
= load_vstr(xdata
, pos
)
2127 if flags
& RAR5_XOWNER_UID
:
2128 user_id
, pos
= load_vint(xdata
, pos
)
2129 if flags
& RAR5_XOWNER_GID
:
2130 group_id
, pos
= load_vint(xdata
, pos
)
2132 h
.file_owner
= (user_name
, group_name
, user_id
, group_id
)
2134 def process_entry(self
, fd
, item
):
2135 if item
.block_type
== RAR5_BLOCK_FILE
:
2136 if item
.file_version
:
2137 pass # skip old versions
2138 elif (item
.block_flags
& RAR5_BLOCK_FLAG_SPLIT_BEFORE
) == 0:
2139 # use only first part
2140 self
._info
_map
[item
.filename
.rstrip("/")] = item
2141 self
._info
_list
.append(item
)
2142 elif len(self
._info
_list
) > 0:
2143 # final crc is in last block
2144 old
= self
._info
_list
[-1]
2146 old
._md
_expect
= item
._md
_expect
2147 old
.blake2sp_hash
= item
.blake2sp_hash
2148 old
.compress_size
+= item
.compress_size
2149 elif item
.block_type
== RAR5_BLOCK_SERVICE
:
2150 if item
.filename
== "CMT":
2151 self
._load
_comment
(fd
, item
)
2153 def _load_comment(self
, fd
, item
):
2154 if item
.block_flags
& (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER
):
2156 if item
.compress_type
!= RAR_M0
:
2159 if item
.flags
& RAR_FILE_PASSWORD
:
2160 algo
, ___flags
, kdf_count
, salt
, iv
, ___checkval
= item
.file_encryption
2161 if algo
!= RAR5_XENC_CIPHER_AES256
:
2163 key
= self
._gen
_key
(kdf_count
, salt
)
2164 f
= HeaderDecrypt(fd
, key
, iv
)
2165 cmt
= f
.read(item
.file_size
)
2168 with self
._open
_clear
(item
) as cmtstream
:
2169 cmt
= cmtstream
.read()
2171 # rar bug? - appends zero to comment
2172 cmt
= cmt
.split(b
"\0", 1)[0]
2173 self
.comment
= cmt
.decode("utf8")
2176 def _open_hack(self
, inf
, pwd
):
2177 # len, type, blk_flags, flags
2178 main_hdr
= b
"\x03\x01\x00\x00"
2179 endarc_hdr
= b
"\x03\x05\x00\x00"
2180 main_hdr
= S_LONG
.pack(crc32(main_hdr
)) + main_hdr
2181 endarc_hdr
= S_LONG
.pack(crc32(endarc_hdr
)) + endarc_hdr
2182 return self
._open
_hack
_core
(inf
, pwd
, RAR5_ID
+ main_hdr
, endarc_hdr
)
2189 class UnicodeFilename
:
2190 """Handle RAR3 unicode filename decompression.
2192 def __init__(self
, name
, encdata
):
2193 self
.std_name
= bytearray(name
)
2194 self
.encdata
= bytearray(encdata
)
2195 self
.pos
= self
.encpos
= 0
2196 self
.buf
= bytearray()
2200 """Copy encoded byte."""
2202 c
= self
.encdata
[self
.encpos
]
2210 """Copy byte from 8-bit representation."""
2212 return self
.std_name
[self
.pos
]
2217 def put(self
, lo
, hi
):
2218 """Copy 16-bit value to result."""
2224 """Decompress compressed UTF16 value."""
2225 hi
= self
.enc_byte()
2227 while self
.encpos
< len(self
.encdata
):
2229 flags
= self
.enc_byte()
2232 t
= (flags
>> flagbits
) & 3
2234 self
.put(self
.enc_byte(), 0)
2236 self
.put(self
.enc_byte(), hi
)
2238 self
.put(self
.enc_byte(), self
.enc_byte())
2243 for _
in range((n
& 0x7f) + 2):
2244 lo
= (self
.std_byte() + c
) & 0xFF
2247 for _
in range(n
+ 2):
2248 self
.put(self
.std_byte(), 0)
2249 return self
.buf
.decode("utf-16le", "replace")
2252 class RarExtFile(io
.RawIOBase
):
2253 """Base class for file-like object that :meth:`RarFile.open` returns.
2255 Provides public methods and common crc checking.
2258 - no short reads - .read() and .readinfo() read as much as requested.
2259 - no internal buffer, use io.BufferedReader for that.
2261 name
= None #: Filename of the archive entry
2271 def _open_extfile(self
, parser
, inf
):
2272 self
.name
= inf
.filename
2273 self
._parser
= parser
2279 md_class
= NoHashContext
2281 md_class
= self
._inf
._md
_class
or NoHashContext
2282 self
._md
_context
= md_class()
2284 self
._remain
= self
._inf
.file_size
2286 def read(self
, n
=-1):
2287 """Read all or specified amount of data from archive entry."""
2290 if n
is None or n
< 0:
2292 elif n
> self
._remain
:
2301 data
= self
._read
(n
)
2305 self
._md
_context
.update(data
)
2306 self
._remain
-= len(data
)
2308 data
= b
"".join(buf
)
2310 raise BadRarFile("Failed the read enough data: req=%d got=%d" % (orig
, len(data
)))
2313 if not data
or self
._remain
== 0:
2319 """Check final CRC."""
2320 final
= self
._md
_context
.digest()
2321 exp
= self
._inf
._md
_expect
2326 if self
._returncode
:
2327 check_returncode(self
._returncode
, "", tool_setup().get_errmap())
2328 if self
._remain
!= 0:
2329 raise BadRarFile("Failed the read enough data")
2331 raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % (
2332 self
._inf
.filename
, exp
, final
))
2334 def _read(self
, cnt
):
2335 """Actual read that gets sanitized cnt."""
2336 raise NotImplementedError("_read")
2339 """Close open resources."""
2348 """Hook delete to make sure tempfile is removed."""
2351 def readinto(self
, buf
):
2352 """Zero-copy read directly into buffer.
2356 raise NotImplementedError("readinto")
2359 """Return current reading position in uncompressed data."""
2360 return self
._inf
.file_size
- self
._remain
2362 def seek(self
, offset
, whence
=0):
2365 On uncompressed files, the seeking works by actual
2366 seeks so it's fast. On compressed files its slow
2367 - forward seeking happens by reading ahead,
2368 backwards by re-opening and decompressing from the start.
2371 # disable crc check when seeking
2372 if not self
._seeking
:
2373 self
._md
_context
= NoHashContext()
2374 self
._seeking
= True
2376 fsize
= self
._inf
.file_size
2377 cur_ofs
= self
.tell()
2379 if whence
== 0: # seek from beginning of file
2381 elif whence
== 1: # seek from current position
2382 new_ofs
= cur_ofs
+ offset
2383 elif whence
== 2: # seek from end of file
2384 new_ofs
= fsize
+ offset
2386 raise ValueError("Invalid value for whence")
2391 elif new_ofs
> fsize
:
2394 # do the actual seek
2395 if new_ofs
>= cur_ofs
:
2396 self
._skip
(new_ofs
- cur_ofs
)
2399 self
._open
_extfile
(self
._parser
, self
._inf
)
2403 def _skip(self
, cnt
):
2404 """Read and discard data"""
2405 empty_read(self
, cnt
, BSIZE
)
2414 Writing is not supported.
2421 Seeking is supported, although it's slow on compressed files.
2426 """Read all remaining data"""
2427 # avoid RawIOBase default impl
2431 class PipeReader(RarExtFile
):
2432 """Read data from pipe, handle tempfile cleanup."""
2434 def __init__(self
, parser
, inf
, cmd
, tempfile
=None):
2438 self
._tempfile
= tempfile
2439 self
._open
_extfile
(parser
, inf
)
2441 def _close_proc(self
):
2444 for f
in (self
._proc
.stdout
, self
._proc
.stderr
, self
._proc
.stdin
):
2448 self
._returncode
= self
._proc
.returncode
2451 def _open_extfile(self
, parser
, inf
):
2452 super()._open
_extfile
(parser
, inf
)
2457 # launch new process
2458 self
._returncode
= 0
2459 self
._proc
= custom_popen(self
._cmd
)
2460 self
._fd
= self
._proc
.stdout
2462 def _read(self
, cnt
):
2463 """Read from pipe."""
2465 # normal read is usually enough
2466 data
= self
._fd
.read(cnt
)
2467 if len(data
) == cnt
or not data
:
2470 # short read, try looping
2474 data
= self
._fd
.read(cnt
)
2479 return b
"".join(buf
)
2482 """Close open resources."""
2489 os
.unlink(self
._tempfile
)
2492 self
._tempfile
= None
2494 def readinto(self
, buf
):
2495 """Zero-copy read directly into buffer."""
2497 if cnt
> self
._remain
:
2499 vbuf
= memoryview(buf
)
2502 res
= self
._fd
.readinto(vbuf
[got
: cnt
])
2505 self
._md
_context
.update(vbuf
[got
: got
+ res
])
2511 class DirectReader(RarExtFile
):
2512 """Read uncompressed data directly from archive.
2518 def __init__(self
, parser
, inf
):
2520 self
._open
_extfile
(parser
, inf
)
2522 def _open_extfile(self
, parser
, inf
):
2523 super()._open
_extfile
(parser
, inf
)
2525 self
._volfile
= self
._inf
.volume_file
2526 self
._fd
= XFile(self
._volfile
, 0)
2527 self
._fd
.seek(self
._inf
.header_offset
, 0)
2528 self
._cur
= self
._parser
._parse
_header
(self
._fd
)
2529 self
._cur
_avail
= self
._cur
.add_size
2531 def _skip(self
, cnt
):
2532 """RAR Seek, skipping through rar files to get to correct position
2537 if self
._cur
_avail
== 0:
2538 if not self
._open
_next
():
2541 # fd is in read pos, do the read
2542 if cnt
> self
._cur
_avail
:
2543 cnt
-= self
._cur
_avail
2544 self
._remain
-= self
._cur
_avail
2547 self
._fd
.seek(cnt
, 1)
2548 self
._cur
_avail
-= cnt
2552 def _read(self
, cnt
):
2553 """Read from potentially multi-volume archive."""
2555 pos
= self
._fd
.tell()
2556 need
= self
._cur
.data_offset
+ self
._cur
.add_size
- self
._cur
_avail
2558 self
._fd
.seek(need
, 0)
2563 if self
._cur
_avail
== 0:
2564 if not self
._open
_next
():
2567 # fd is in read pos, do the read
2568 if cnt
> self
._cur
_avail
:
2569 data
= self
._fd
.read(self
._cur
_avail
)
2571 data
= self
._fd
.read(cnt
)
2577 self
._cur
_avail
-= len(data
)
2582 return b
"".join(buf
)
2584 def _open_next(self
):
2585 """Proceed to next volume."""
2587 # is the file split over archives?
2588 if (self
._cur
.flags
& RAR_FILE_SPLIT_AFTER
) == 0:
2596 self
._volfile
= self
._parser
._next
_volname
(self
._volfile
)
2597 fd
= open(self
._volfile
, "rb", 0)
2599 sig
= fd
.read(len(self
._parser
._expect
_sig
))
2600 if sig
!= self
._parser
._expect
_sig
:
2601 raise BadRarFile("Invalid signature")
2603 # loop until first file header
2605 cur
= self
._parser
._parse
_header
(fd
)
2607 raise BadRarFile("Unexpected EOF")
2608 if cur
.type in (RAR_BLOCK_MARK
, RAR_BLOCK_MAIN
):
2610 fd
.seek(cur
.add_size
, 1)
2612 if cur
.orig_filename
!= self
._inf
.orig_filename
:
2613 raise BadRarFile("Did not found file entry")
2615 self
._cur
_avail
= cur
.add_size
2618 def readinto(self
, buf
):
2619 """Zero-copy read directly into buffer."""
2621 vbuf
= memoryview(buf
)
2622 while got
< len(buf
):
2624 if self
._cur
_avail
== 0:
2625 if not self
._open
_next
():
2628 # length for next read
2629 cnt
= len(buf
) - got
2630 if cnt
> self
._cur
_avail
:
2631 cnt
= self
._cur
_avail
2633 # read into temp view
2634 res
= self
._fd
.readinto(vbuf
[got
: got
+ cnt
])
2637 self
._md
_context
.update(vbuf
[got
: got
+ res
])
2638 self
._cur
_avail
-= res
2644 class HeaderDecrypt
:
2645 """File-like object that decrypts from another file"""
2646 def __init__(self
, f
, key
, iv
):
2648 self
.ciph
= AES_CBC_Decrypt(key
, iv
)
2652 """Current file pos - works only on block boundaries."""
2653 return self
.f
.tell()
2655 def read(self
, cnt
=None):
2656 """Read and decrypt."""
2658 raise BadRarFile("Bad count to header decrypt - wrong password?")
2661 if cnt
<= len(self
.buf
):
2662 res
= self
.buf
[:cnt
]
2663 self
.buf
= self
.buf
[cnt
:]
2672 enc
= self
.f
.read(blklen
)
2673 if len(enc
) < blklen
:
2675 dec
= self
.ciph
.decrypt(enc
)
2681 self
.buf
= dec
[cnt
:]
2688 """Input may be filename or file object.
2690 __slots__
= ("_fd", "_need_close")
2692 def __init__(self
, xfile
, bufsize
=1024):
2693 if is_filelike(xfile
):
2694 self
._need
_close
= False
2698 self
._need
_close
= True
2699 self
._fd
= open(xfile
, "rb", bufsize
)
2701 def read(self
, n
=None):
2702 """Read from file."""
2703 return self
._fd
.read(n
)
2706 """Return file pos."""
2707 return self
._fd
.tell()
2709 def seek(self
, ofs
, whence
=0):
2710 """Move file pos."""
2711 return self
._fd
.seek(ofs
, whence
)
2713 def readinto(self
, buf
):
2714 """Read into buffer."""
2715 return self
._fd
.readinto(buf
)
2718 """Close file object."""
2719 if self
._need
_close
:
2722 def __enter__(self
):
2725 def __exit__(self
, typ
, val
, tb
):
2729 class NoHashContext
:
2730 """No-op hash function."""
2731 def __init__(self
, data
=None):
2733 def update(self
, data
):
2737 def hexdigest(self
):
2738 """Hexadecimal digest."""
2742 """Hash context that uses CRC32."""
2743 __slots__
= ["_crc"]
2745 def __init__(self
, data
=None):
2750 def update(self
, data
):
2752 self
._crc
= crc32(data
, self
._crc
)
2758 def hexdigest(self
):
2759 """Hexadecimal digest."""
2760 return "%08x" % self
.digest()
2764 """Blake2sp hash context.
2766 __slots__
= ["_thread", "_buf", "_cur", "_digest"]
2771 def __init__(self
, data
=None):
2777 for i
in range(self
.parallelism
):
2778 ctx
= self
._blake
2s
(i
, 0, i
== (self
.parallelism
- 1))
2779 self
._thread
.append(ctx
)
2784 def _blake2s(self
, ofs
, depth
, is_last
):
2785 return blake2s(node_offset
=ofs
, node_depth
=depth
, last_node
=is_last
,
2786 depth
=2, inner_size
=32, fanout
=self
.parallelism
)
2788 def _add_block(self
, blk
):
2789 self
._thread
[self
._cur
].update(blk
)
2790 self
._cur
= (self
._cur
+ 1) % self
.parallelism
2792 def update(self
, data
):
2795 view
= memoryview(data
)
2796 bs
= self
.block_size
2798 need
= bs
- len(self
._buf
)
2799 if len(view
) < need
:
2800 self
._buf
+= view
.tobytes()
2802 self
._add
_block
(self
._buf
+ view
[:need
].tobytes())
2804 while len(view
) >= bs
:
2805 self
._add
_block
(view
[:bs
])
2807 self
._buf
= view
.tobytes()
2810 """Return final digest value.
2812 if self
._digest
is None:
2814 self
._add
_block
(self
._buf
)
2816 ctx
= self
._blake
2s
(0, 1, True)
2817 for t
in self
._thread
:
2818 ctx
.update(t
.digest())
2819 self
._digest
= ctx
.digest()
2822 def hexdigest(self
):
2823 """Hexadecimal digest."""
2824 return hexlify(self
.digest()).decode("ascii")
2828 """Emulate buggy SHA1 from RAR3.
2833 _BLK_BE
= struct
.Struct(b
">16L")
2834 _BLK_LE
= struct
.Struct(b
"<16L")
2836 __slots__
= ("_nbytes", "_md", "_rarbug")
2838 def __init__(self
, data
=b
"", rarbug
=False):
2841 self
._rarbug
= rarbug
2844 def update(self
, data
):
2845 """Process more data."""
2846 self
._md
.update(data
)
2847 bufpos
= self
._nbytes
& 63
2848 self
._nbytes
+= len(data
)
2850 if self
._rarbug
and len(data
) > 64:
2851 dpos
= self
.block_size
- bufpos
2852 while dpos
+ self
.block_size
<= len(data
):
2853 self
._corrupt
(data
, dpos
)
2854 dpos
+= self
.block_size
2857 """Return final state."""
2858 return self
._md
.digest()
2860 def hexdigest(self
):
2861 """Return final state as hex string."""
2862 return self
._md
.hexdigest()
2864 def _corrupt(self
, data
, dpos
):
2865 """Corruption from SHA1 core."""
2866 ws
= list(self
._BLK
_BE
.unpack_from(data
, dpos
))
2867 for t
in range(16, 80):
2868 tmp
= ws
[(t
- 3) & 15] ^ ws
[(t
- 8) & 15] ^ ws
[(t
- 14) & 15] ^ ws
[(t
- 16) & 15]
2869 ws
[t
& 15] = ((tmp
<< 1) |
(tmp
>> (32 - 1))) & 0xFFFFFFFF
2870 self
._BLK
_LE
.pack_into(data
, dpos
, *ws
)
2874 ## Utility functions
2877 S_LONG
= Struct("<L")
2878 S_SHORT
= Struct("<H")
2879 S_BYTE
= Struct("<B")
2881 S_BLK_HDR
= Struct("<HBHH")
2882 S_FILE_HDR
= Struct("<LLBLLBBHL")
2883 S_COMMENT_HDR
= Struct("<HBBH")
2886 def load_vint(buf
, pos
):
2887 """Load RAR5 variable-size int."""
2888 limit
= min(pos
+ 11, len(buf
))
2892 res
+= ((b
& 0x7F) << ofs
)
2897 raise BadRarFile("cannot load vint")
2900 def load_byte(buf
, pos
):
2901 """Load single byte"""
2904 raise BadRarFile("cannot load byte")
2905 return S_BYTE
.unpack_from(buf
, pos
)[0], end
2908 def load_le32(buf
, pos
):
2909 """Load little-endian 32-bit integer"""
2912 raise BadRarFile("cannot load le32")
2913 return S_LONG
.unpack_from(buf
, pos
)[0], end
2916 def load_bytes(buf
, num
, pos
):
2917 """Load sequence of bytes"""
2920 raise BadRarFile("cannot load bytes")
2921 return buf
[pos
: end
], end
2924 def load_vstr(buf
, pos
):
2925 """Load bytes prefixed by vint length"""
2926 slen
, pos
= load_vint(buf
, pos
)
2927 return load_bytes(buf
, slen
, pos
)
2930 def load_dostime(buf
, pos
):
2931 """Load LE32 dos timestamp"""
2932 stamp
, pos
= load_le32(buf
, pos
)
2933 tup
= parse_dos_time(stamp
)
2934 return to_datetime(tup
), pos
2937 def load_unixtime(buf
, pos
):
2938 """Load LE32 unix timestamp"""
2939 secs
, pos
= load_le32(buf
, pos
)
2940 dt
= datetime
.fromtimestamp(secs
, timezone
.utc
)
2944 def load_windowstime(buf
, pos
):
2945 """Load LE64 windows timestamp"""
2946 # unix epoch (1970) in seconds from windows epoch (1601)
2947 unix_epoch
= 11644473600
2948 val1
, pos
= load_le32(buf
, pos
)
2949 val2
, pos
= load_le32(buf
, pos
)
2950 secs
, n1secs
= divmod((val2
<< 32) | val1
, 10000000)
2951 dt
= datetime
.fromtimestamp(secs
- unix_epoch
, timezone
.utc
)
2952 dt
= to_nsdatetime(dt
, n1secs
* 100)
2960 _rc_num
= re
.compile('^[0-9]+$')
2963 def _next_newvol(volfile
):
2964 """New-style next volume
2966 name
, ext
= os
.path
.splitext(volfile
)
2967 if ext
.lower() in ("", ".exe", ".sfx"):
2968 volfile
= name
+ ".rar"
2969 i
= len(volfile
) - 1
2971 if "0" <= volfile
[i
] <= "9":
2972 return _inc_volname(volfile
, i
, False)
2973 if volfile
[i
] in ("/", os
.sep
):
2976 raise BadRarName("Cannot construct volume name: " + volfile
)
2980 def _next_oldvol(volfile
):
2981 """Old-style next volume
2983 name
, ext
= os
.path
.splitext(volfile
)
2984 if ext
.lower() in ("", ".exe", ".sfx"):
2987 if _rc_num
.match(sfx
):
2988 ext
= _inc_volname(ext
, len(ext
) - 1, True)
2991 ext
= ext
[:2] + "00"
2995 def _inc_volname(volfile
, i
, inc_chars
):
2996 """increase digits with carry, otherwise just increment char
3005 elif "0" <= fn
[i
] < "9" or inc_chars
:
3006 fn
[i
] = chr(ord(fn
[i
]) + 1)
3009 fn
.insert(i
+ 1, "1")
3014 def _parse_ext_time(h
, data
, pos
):
3015 """Parse all RAR3 extended time fields
3017 # flags and rest of data can be missing
3019 if pos
+ 2 <= len(data
):
3020 flags
= S_SHORT
.unpack_from(data
, pos
)[0]
3023 mtime
, pos
= _parse_xtime(flags
>> 3 * 4, data
, pos
, h
.mtime
)
3024 h
.ctime
, pos
= _parse_xtime(flags
>> 2 * 4, data
, pos
)
3025 h
.atime
, pos
= _parse_xtime(flags
>> 1 * 4, data
, pos
)
3026 h
.arctime
, pos
= _parse_xtime(flags
>> 0 * 4, data
, pos
)
3029 h
.date_time
= mtime
.timetuple()[:6]
3033 def _parse_xtime(flag
, data
, pos
, basetime
=None):
3034 """Parse one RAR3 extended time field
3039 basetime
, pos
= load_dostime(data
, pos
)
3041 # load second fractions of 100ns units
3044 for _
in range(cnt
):
3045 b
, pos
= load_byte(data
, pos
)
3046 rem
= (b
<< 16) |
(rem
>> 8)
3048 # dostime has room for 30 seconds only, correct if needed
3049 if flag
& 4 and basetime
.second
< 59:
3050 basetime
= basetime
.replace(second
=basetime
.second
+ 1)
3052 res
= to_nsdatetime(basetime
, rem
* 100)
3056 def is_filelike(obj
):
3057 """Filename or file object?
3059 if isinstance(obj
, (bytes
, str, Path
)):
3062 for a
in ("read", "tell", "seek"):
3063 res
= res
and hasattr(obj
, a
)
3065 raise ValueError("Invalid object passed as file")
3069 def rar5_s2k(pwd
, salt
, kdf_count
):
3070 """String-to-key hash for RAR5.
3072 if not isinstance(pwd
, str):
3073 pwd
= pwd
.decode("utf8")
3074 wstr
= pwd
.encode("utf-16le")[:RAR_MAX_PASSWORD
*2]
3075 ustr
= wstr
.decode("utf-16le").encode("utf8")
3076 return pbkdf2_hmac("sha256", ustr
, salt
, kdf_count
)
3079 def rar3_s2k(pwd
, salt
):
3080 """String-to-key hash for RAR3.
3082 if not isinstance(pwd
, str):
3083 pwd
= pwd
.decode("utf8")
3084 wstr
= pwd
.encode("utf-16le")[:RAR_MAX_PASSWORD
*2]
3085 seed
= bytearray(wstr
+ salt
)
3086 h
= Rar3Sha1(rarbug
=True)
3089 for j
in range(0x4000):
3090 cnt
= S_LONG
.pack(i
* 0x4000 + j
)
3094 iv
+= h
.digest()[19:20]
3095 key_be
= h
.digest()[:16]
3096 key_le
= pack("<LLLL", *unpack(">LLLL", key_be
))
3100 def rar3_decompress(vers
, meth
, data
, declen
=0, flags
=0, crc
=0, pwd
=None, salt
=None):
3101 """Decompress blob of compressed data.
3103 Used for data with non-standard header - eg. comments.
3105 # already uncompressed?
3106 if meth
== RAR_M0
and (flags
& RAR_FILE_PASSWORD
) == 0:
3109 # take only necessary flags
3110 flags
= flags
& (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK
)
3111 flags |
= RAR_LONG_BLOCK
3115 date
= ((2010 - 1980) << 25) + (12 << 21) + (31 << 16)
3116 mode
= DOS_MODE_ARCHIVE
3117 fhdr
= S_FILE_HDR
.pack(len(data
), declen
, RAR_OS_MSDOS
, crc
,
3118 date
, vers
, meth
, len(fname
), mode
)
3124 hlen
= S_BLK_HDR
.size
+ len(fhdr
)
3125 hdr
= S_BLK_HDR
.pack(0, RAR_BLOCK_FILE
, flags
, hlen
) + fhdr
3126 hcrc
= crc32(hdr
[2:]) & 0xFFFF
3127 hdr
= S_BLK_HDR
.pack(hcrc
, RAR_BLOCK_FILE
, flags
, hlen
) + fhdr
3129 # archive main header
3130 mh
= S_BLK_HDR
.pack(0x90CF, RAR_BLOCK_MAIN
, 0, 13) + b
"\0" * (2 + 4)
3132 # decompress via temp rar
3133 setup
= tool_setup()
3134 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
3135 tmpf
= os
.fdopen(tmpfd
, "wb")
3137 tmpf
.write(RAR_ID
+ mh
+ hdr
+ data
)
3140 curpwd
= (flags
& RAR_FILE_PASSWORD
) and pwd
or None
3141 cmd
= setup
.open_cmdline(curpwd
, tmpname
)
3142 p
= custom_popen(cmd
)
3143 return p
.communicate()[0]
3149 def sanitize_filename(fname
, pathsep
, is_win32
):
3150 """Make filename safe for write access.
3153 if len(fname
) > 1 and fname
[1] == ":":
3155 rc
= RC_BAD_CHARS_WIN32
3157 rc
= RC_BAD_CHARS_UNIX
3158 if rc
.search(fname
):
3159 fname
= rc
.sub("_", fname
)
3162 for seg
in fname
.split("/"):
3163 if seg
in ("", ".", ".."):
3165 if is_win32
and seg
[-1] in (" ", "."):
3166 seg
= seg
[:-1] + "_"
3168 return pathsep
.join(parts
)
3171 def empty_read(src
, size
, blklen
):
3172 """Read and drop fixed amount of data.
3176 res
= src
.read(blklen
)
3178 res
= src
.read(size
)
3180 raise BadRarFile("cannot load data")
3185 """Convert 6-part time tuple into datetime object.
3188 year
, mon
, day
, h
, m
, s
= t
3190 # assume the values are valid
3192 return datetime(year
, mon
, day
, h
, m
, s
)
3196 # sanitize invalid values
3197 mday
= (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
3198 mon
= max(1, min(mon
, 12))
3199 day
= max(1, min(day
, mday
[mon
]))
3203 return datetime(year
, mon
, day
, h
, m
, s
)
3206 def parse_dos_time(stamp
):
3207 """Parse standard 32-bit DOS timestamp.
3209 sec
, stamp
= stamp
& 0x1F, stamp
>> 5
3210 mn
, stamp
= stamp
& 0x3F, stamp
>> 6
3211 hr
, stamp
= stamp
& 0x1F, stamp
>> 5
3212 day
, stamp
= stamp
& 0x1F, stamp
>> 5
3213 mon
, stamp
= stamp
& 0x0F, stamp
>> 4
3214 yr
= (stamp
& 0x7F) + 1980
3215 return (yr
, mon
, day
, hr
, mn
, sec
* 2)
3218 # pylint: disable=arguments-differ,signature-differs
3219 class nsdatetime(datetime
):
3220 """Datetime that carries nanoseconds.
3222 Arithmetic operations will lose nanoseconds.
3224 .. versionadded:: 4.0
3226 __slots__
= ("nanosecond",)
3227 nanosecond
: int #: Number of nanoseconds, 0 <= nanosecond <= 999999999
3229 def __new__(cls
, year
, month
=None, day
=None, hour
=0, minute
=0, second
=0,
3230 microsecond
=0, tzinfo
=None, *, fold
=0, nanosecond
=0):
3231 usec
, mod
= divmod(nanosecond
, 1000) if nanosecond
else (microsecond
, 0)
3233 return datetime(year
, month
, day
, hour
, minute
, second
, usec
, tzinfo
, fold
=fold
)
3234 self
= super().__new
__(cls
, year
, month
, day
, hour
, minute
, second
, usec
, tzinfo
, fold
=fold
)
3235 self
.nanosecond
= nanosecond
3238 def isoformat(self
, sep
="T", timespec
="auto"):
3239 """Formats with nanosecond precision by default.
3241 if timespec
== "auto":
3242 pre
, post
= super().isoformat(sep
, "microseconds").split(".", 1)
3243 return f
"{pre}.{self.nanosecond:09d}{post[6:]}"
3244 return super().isoformat(sep
, timespec
)
3246 def astimezone(self
, tz
=None):
3247 """Convert to new timezone.
3249 tmp
= super().astimezone(tz
)
3250 return self
.__class
__(tmp
.year
, tmp
.month
, tmp
.day
, tmp
.hour
, tmp
.minute
, tmp
.second
,
3251 nanosecond
=self
.nanosecond
, tzinfo
=tmp
.tzinfo
, fold
=tmp
.fold
)
3253 def replace(self
, year
=None, month
=None, day
=None, hour
=None, minute
=None, second
=None,
3254 microsecond
=None, tzinfo
=None, *, fold
=None, nanosecond
=None):
3255 """Return new timestamp with specified fields replaced.
3257 return self
.__class
__(
3258 self
.year
if year
is None else year
,
3259 self
.month
if month
is None else month
,
3260 self
.day
if day
is None else day
,
3261 self
.hour
if hour
is None else hour
,
3262 self
.minute
if minute
is None else minute
,
3263 self
.second
if second
is None else second
,
3264 nanosecond
=((self
.nanosecond
if microsecond
is None else microsecond
* 1000)
3265 if nanosecond
is None else nanosecond
),
3266 tzinfo
=self
.tzinfo
if tzinfo
is None else tzinfo
,
3267 fold
=self
.fold
if fold
is None else fold
)
3270 return hash((super().__hash
__(), self
.nanosecond
)) if self
.nanosecond
else super().__hash
__()
3272 def __eq__(self
, other
):
3273 return super().__eq
__(other
) and self
.nanosecond
== (
3274 other
.nanosecond
if isinstance(other
, nsdatetime
) else other
.microsecond
* 1000)
3276 def __gt__(self
, other
):
3277 return super().__gt
__(other
) or (super().__eq
__(other
) and self
.nanosecond
> (
3278 other
.nanosecond
if isinstance(other
, nsdatetime
) else other
.microsecond
* 1000))
3280 def __lt__(self
, other
):
3281 return not (self
> other
or self
== other
)
3283 def __ge__(self
, other
):
3284 return not self
< other
3286 def __le__(self
, other
):
3287 return not self
> other
3289 def __ne__(self
, other
):
3290 return not self
== other
3293 def to_nsdatetime(dt
, nsec
):
3294 """Apply nanoseconds to datetime.
3298 return nsdatetime(dt
.year
, dt
.month
, dt
.day
, dt
.hour
, dt
.minute
, dt
.second
,
3299 tzinfo
=dt
.tzinfo
, fold
=dt
.fold
, nanosecond
=nsec
)
3303 """Convert datatime instance to nanoseconds.
3305 secs
= int(dt
.timestamp())
3306 nsecs
= dt
.nanosecond
if isinstance(dt
, nsdatetime
) else dt
.microsecond
* 1000
3307 return secs
* 1000000000 + nsecs
3310 def custom_popen(cmd
):
3311 """Disconnect cmd from parent fds, read only from stdout.
3313 creationflags
= 0x08000000 if WIN32
else 0 # CREATE_NO_WINDOW
3315 p
= Popen(cmd
, bufsize
=0, stdout
=PIPE
, stderr
=STDOUT
, stdin
=DEVNULL
,
3316 creationflags
=creationflags
)
3317 except OSError as ex
:
3318 if ex
.errno
== errno
.ENOENT
:
3319 raise RarCannotExec("Unrar not installed?") from None
3320 if ex
.errno
== errno
.EACCES
or ex
.errno
== errno
.EPERM
:
3321 raise RarCannotExec("Cannot execute unrar") from None
3326 def check_returncode(code
, out
, errmap
):
3327 """Raise exception according to unrar exit code.
3332 if code
> 0 and code
< len(errmap
):
3339 exc
= RarUnknownError
3343 msg
= "%s [%d]: %s" % (exc
.__doc
__, code
, out
)
3345 msg
= "%s [%d]" % (exc
.__doc
__, code
)
3350 def membuf_tempfile(memfile
):
3351 """Write in-memory file object to real file.
3355 tmpfd
, tmpname
= mkstemp(suffix
=".rar", dir=HACK_TMP_DIR
)
3356 tmpf
= os
.fdopen(tmpfd
, "wb")
3359 shutil
.copyfileobj(memfile
, tmpf
, BSIZE
)
3361 except BaseException
:
3369 # Find working command-line tool
3373 def __init__(self
, setup
):
3377 cmdline
= self
.get_cmdline("check_cmd", None)
3379 p
= custom_popen(cmdline
)
3380 out
, _
= p
.communicate()
3381 return p
.returncode
== 0
3382 except RarCannotExec
:
3385 def open_cmdline(self
, pwd
, rarfn
, filefn
=None):
3386 cmdline
= self
.get_cmdline("open_cmd", pwd
)
3387 cmdline
.append(rarfn
)
3389 self
.add_file_arg(cmdline
, filefn
)
3392 def get_errmap(self
):
3393 return self
.setup
["errmap"]
3395 def get_cmdline(self
, key
, pwd
, nodash
=False):
3396 cmdline
= list(self
.setup
[key
])
3397 cmdline
[0] = globals()[cmdline
[0]]
3398 if key
== "check_cmd":
3400 self
.add_password_arg(cmdline
, pwd
)
3402 cmdline
.append("--")
3405 def add_file_arg(self
, cmdline
, filename
):
3406 cmdline
.append(filename
)
3408 def add_password_arg(self
, cmdline
, pwd
):
3409 """Append password switch to commandline.
3412 if not isinstance(pwd
, str):
3413 pwd
= pwd
.decode("utf8")
3414 args
= self
.setup
["password"]
3416 tool
= self
.setup
["open_cmd"][0]
3417 raise RarCannotExec(f
"{tool} does not support passwords")
3418 elif isinstance(args
, str):
3419 cmdline
.append(args
+ pwd
)
3421 cmdline
.extend(args
)
3424 cmdline
.extend(self
.setup
["no_password"])
3428 "open_cmd": ("UNRAR_TOOL", "p", "-inul"),
3429 "check_cmd": ("UNRAR_TOOL", "-inul", "-?"),
3431 "no_password": ("-p-",),
3432 # map return code to exception class, codes from rar.txt
3434 RarWarning
, RarFatalError
, RarCRCError
, RarLockedArchiveError
, # 1..4
3435 RarWriteError
, RarOpenError
, RarUserError
, RarMemoryError
, # 5..8
3436 RarCreateError
, RarNoFilesError
, RarWrongPassword
] # 9..11
3439 # Problems with unar RAR backend:
3440 # - Does not support RAR2 locked files [fails to read]
3441 # - Does not support RAR5 Blake2sp hash [reading works]
3443 "open_cmd": ("UNAR_TOOL", "-q", "-o", "-"),
3444 "check_cmd": ("UNAR_TOOL", "-version"),
3445 "password": ("-p",),
3446 "no_password": ("-p", ""),
3450 # Problems with libarchive RAR backend:
3451 # - Does not support solid archives.
3452 # - Does not support password-protected archives.
3453 # - Does not support RARVM-based compression filters.
3455 "open_cmd": ("BSDTAR_TOOL", "-x", "--to-stdout", "-f"),
3456 "check_cmd": ("BSDTAR_TOOL", "--version"),
3463 "open_cmd": ("SEVENZIP_TOOL", "e", "-so", "-bb0"),
3464 "check_cmd": ("SEVENZIP_TOOL", "i"),
3466 "no_password": ("-p",),
3468 RarWarning
, RarFatalError
, None, None, # 1..4
3469 None, None, RarUserError
, RarMemoryError
] # 5..8
3472 SEVENZIP2_CONFIG
= {
3473 "open_cmd": ("SEVENZIP2_TOOL", "e", "-so", "-bb0"),
3474 "check_cmd": ("SEVENZIP2_TOOL", "i"),
3476 "no_password": ("-p",),
3478 RarWarning
, RarFatalError
, None, None, # 1..4
3479 None, None, RarUserError
, RarMemoryError
] # 5..8
3482 CURRENT_SETUP
= None
3485 def tool_setup(unrar
=True, unar
=True, bsdtar
=True, sevenzip
=True, sevenzip2
=True, force
=False):
3486 """Pick a tool, return cached ToolSetup.
3488 global CURRENT_SETUP
3490 CURRENT_SETUP
= None
3491 if CURRENT_SETUP
is not None:
3492 return CURRENT_SETUP
3495 lst
.append(UNRAR_CONFIG
)
3497 lst
.append(UNAR_CONFIG
)
3499 lst
.append(SEVENZIP_CONFIG
)
3501 lst
.append(SEVENZIP2_CONFIG
)
3503 lst
.append(BSDTAR_CONFIG
)
3506 setup
= ToolSetup(conf
)
3508 CURRENT_SETUP
= setup
3510 if CURRENT_SETUP
is None:
3511 raise RarCannotExec("Cannot find working tool")
3512 return CURRENT_SETUP
3516 """Minimal command-line interface for rarfile module.
3519 p
= argparse
.ArgumentParser(description
=main
.__doc
__)
3520 g
= p
.add_mutually_exclusive_group(required
=True)
3521 g
.add_argument("-l", "--list", metavar
="<rarfile>",
3522 help="Show archive listing")
3523 g
.add_argument("-e", "--extract", nargs
=2,
3524 metavar
=("<rarfile>", "<output_dir>"),
3525 help="Extract archive into target dir")
3526 g
.add_argument("-t", "--test", metavar
="<rarfile>",
3527 help="Test if a archive is valid")
3528 cmd
= p
.parse_args(args
)
3531 with
RarFile(cmd
.list) as rf
:
3534 with
RarFile(cmd
.test
) as rf
:
3537 with
RarFile(cmd
.extract
[0]) as rf
:
3538 rf
.extractall(cmd
.extract
[1])
3541 if __name__
== "__main__":