Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rarfile.py: 34%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# rarfile.py
2#
3# Copyright (c) 2005-2026 Marko Kreen <markokr@gmail.com>
4#
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.
8#
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.
19This is Python module for Rar archive reading. The interface
20is made as :mod:`zipfile`-like as possible.
22Basic logic:
23 - Parse archive structure with Python.
24 - Extract non-compressed files with Python
25 - Extract compressed files with unrar.
26 - Optionally write compressed data to temp file to speed up unrar,
27 otherwise it needs to scan whole archive on each execution.
29Example::
31 import rarfile
33 rf = rarfile.RarFile("myarchive.rar")
34 for f in rf.infolist():
35 print(f.filename, f.file_size)
36 if f.filename == "README":
37 print(rf.read(f))
39Archive files can also be accessed via file-like object returned
40by :meth:`RarFile.open`::
42 import rarfile
44 with rarfile.RarFile("archive.rar") as rf:
45 with rf.open("README") as f:
46 for ln in f:
47 print(ln.strip())
49For decompression to work, either ``unrar`` or ``unar`` tool must be in PATH.
50"""
52import errno
53import io
54import os
55import re
56import shutil
57import struct
58import sys
59import warnings
60from binascii import crc32, hexlify
61from datetime import datetime, timezone
62from hashlib import blake2s, pbkdf2_hmac, sha1, sha256
63from pathlib import Path
64from struct import Struct, pack, unpack
65from subprocess import DEVNULL, PIPE, STDOUT, Popen
66from tempfile import mkstemp
68AES = None
70# only needed for encrypted headers
71try:
72 try:
73 from cryptography.hazmat.backends import default_backend
74 from cryptography.hazmat.primitives.ciphers import (
75 Cipher, algorithms, modes,
76 )
77 _have_crypto = 1
78 except ImportError:
79 from Crypto.Cipher import AES
80 _have_crypto = 2
81except ImportError:
82 _have_crypto = 0
85class AES_CBC_Decrypt:
86 """Decrypt API"""
87 def __init__(self, key, iv):
88 if _have_crypto == 2:
89 self.decrypt = AES.new(key, AES.MODE_CBC, iv).decrypt
90 else:
91 ciph = Cipher(algorithms.AES(key), modes.CBC(iv), default_backend())
92 self.decrypt = ciph.decryptor().update
95__version__ = "4.4"
97# export only interesting items
98__all__ = ["get_rar_version", "is_rarfile", "is_rarfile_sfx", "RarInfo", "RarFile", "RarExtFile"]
100##
101## Module configuration. Can be tuned after importing.
102##
104#: executable for unrar tool
105UNRAR_TOOL = "unrar"
107#: executable for unar tool
108UNAR_TOOL = "unar"
110#: executable for bsdtar tool
111BSDTAR_TOOL = "bsdtar"
113#: executable for p7zip/7z tool
114SEVENZIP_TOOL = "7z"
116#: executable for alternative 7z tool
117SEVENZIP2_TOOL = "7zz"
119#: default fallback charset
120DEFAULT_CHARSET = "windows-1252"
122#: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
123TRY_ENCODINGS = ("utf8", "utf-16le")
125#: whether to speed up decompression by using tmp archive
126USE_EXTRACT_HACK = 1
128#: limit the filesize for tmp archive usage
129HACK_SIZE_LIMIT = 20 * 1024 * 1024
131#: set specific directory for mkstemp() used by hack dir usage
132HACK_TMP_DIR = None
134#: Separator for path name components. Always "/".
135PATH_SEP = "/"
137##
138## rar constants
139##
141# block types
142RAR_BLOCK_MARK = 0x72 # r
143RAR_BLOCK_MAIN = 0x73 # s
144RAR_BLOCK_FILE = 0x74 # t
145RAR_BLOCK_OLD_COMMENT = 0x75 # u
146RAR_BLOCK_OLD_EXTRA = 0x76 # v
147RAR_BLOCK_OLD_SUB = 0x77 # w
148RAR_BLOCK_OLD_RECOVERY = 0x78 # x
149RAR_BLOCK_OLD_AUTH = 0x79 # y
150RAR_BLOCK_SUB = 0x7a # z
151RAR_BLOCK_ENDARC = 0x7b # {
153# flags for RAR_BLOCK_MAIN
154RAR_MAIN_VOLUME = 0x0001
155RAR_MAIN_COMMENT = 0x0002
156RAR_MAIN_LOCK = 0x0004
157RAR_MAIN_SOLID = 0x0008
158RAR_MAIN_NEWNUMBERING = 0x0010
159RAR_MAIN_AUTH = 0x0020
160RAR_MAIN_RECOVERY = 0x0040
161RAR_MAIN_PASSWORD = 0x0080
162RAR_MAIN_FIRSTVOLUME = 0x0100
163RAR_MAIN_ENCRYPTVER = 0x0200
165# flags for RAR_BLOCK_FILE
166RAR_FILE_SPLIT_BEFORE = 0x0001
167RAR_FILE_SPLIT_AFTER = 0x0002
168RAR_FILE_PASSWORD = 0x0004
169RAR_FILE_COMMENT = 0x0008
170RAR_FILE_SOLID = 0x0010
171RAR_FILE_DICTMASK = 0x00e0
172RAR_FILE_DICT64 = 0x0000
173RAR_FILE_DICT128 = 0x0020
174RAR_FILE_DICT256 = 0x0040
175RAR_FILE_DICT512 = 0x0060
176RAR_FILE_DICT1024 = 0x0080
177RAR_FILE_DICT2048 = 0x00a0
178RAR_FILE_DICT4096 = 0x00c0
179RAR_FILE_DIRECTORY = 0x00e0
180RAR_FILE_LARGE = 0x0100
181RAR_FILE_UNICODE = 0x0200
182RAR_FILE_SALT = 0x0400
183RAR_FILE_VERSION = 0x0800
184RAR_FILE_EXTTIME = 0x1000
185RAR_FILE_EXTFLAGS = 0x2000
187# flags for RAR_BLOCK_ENDARC
188RAR_ENDARC_NEXT_VOLUME = 0x0001
189RAR_ENDARC_DATACRC = 0x0002
190RAR_ENDARC_REVSPACE = 0x0004
191RAR_ENDARC_VOLNR = 0x0008
193# flags common to all blocks
194RAR_SKIP_IF_UNKNOWN = 0x4000
195RAR_LONG_BLOCK = 0x8000
197# Subtypes for RAR_BLOCK_OLD_SUB
198RAR_OLD_SUB_OS2 = 0x100
199RAR_OLD_SUB_UNIX = 0x101
200RAR_OLD_SUB_MAC = 0x102
201RAR_OLD_SUB_BEOS = 0x103
202RAR_OLD_SUB_NT = 0x104
203RAR_OLD_SUB_STREAM = 0x105
205# Host OS types
206RAR_OS_MSDOS = 0 #: MSDOS (only in RAR3)
207RAR_OS_OS2 = 1 #: OS2 (only in RAR3)
208RAR_OS_WIN32 = 2 #: Windows
209RAR_OS_UNIX = 3 #: UNIX
210RAR_OS_MACOS = 4 #: MacOS (only in RAR3)
211RAR_OS_BEOS = 5 #: BeOS (only in RAR3)
213# Compression methods - "0".."5"
214RAR_M0 = 0x30 #: No compression.
215RAR_M1 = 0x31 #: Compression level `-m1` - Fastest compression.
216RAR_M2 = 0x32 #: Compression level `-m2`.
217RAR_M3 = 0x33 #: Compression level `-m3`.
218RAR_M4 = 0x34 #: Compression level `-m4`.
219RAR_M5 = 0x35 #: Compression level `-m5` - Maximum compression.
221RAR_MAX_PASSWORD = 127 #: Max number of utf-16 chars in passwords.
222RAR_MAX_KDF_SHIFT = 24 #: Max power-of-2 for KDF count
224#
225# RAR5 constants
226#
228RAR5_BLOCK_MAIN = 1
229RAR5_BLOCK_FILE = 2
230RAR5_BLOCK_SERVICE = 3
231RAR5_BLOCK_ENCRYPTION = 4
232RAR5_BLOCK_ENDARC = 5
234RAR5_BLOCK_FLAG_EXTRA_DATA = 0x01
235RAR5_BLOCK_FLAG_DATA_AREA = 0x02
236RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN = 0x04
237RAR5_BLOCK_FLAG_SPLIT_BEFORE = 0x08
238RAR5_BLOCK_FLAG_SPLIT_AFTER = 0x10
239RAR5_BLOCK_FLAG_DEPENDS_PREV = 0x20
240RAR5_BLOCK_FLAG_KEEP_WITH_PARENT = 0x40
242RAR5_MAIN_FLAG_ISVOL = 0x01
243RAR5_MAIN_FLAG_HAS_VOLNR = 0x02
244RAR5_MAIN_FLAG_SOLID = 0x04
245RAR5_MAIN_FLAG_RECOVERY = 0x08
246RAR5_MAIN_FLAG_LOCKED = 0x10
248RAR5_FILE_FLAG_ISDIR = 0x01
249RAR5_FILE_FLAG_HAS_MTIME = 0x02
250RAR5_FILE_FLAG_HAS_CRC32 = 0x04
251RAR5_FILE_FLAG_UNKNOWN_SIZE = 0x08
253RAR5_COMPR_SOLID = 0x40
255RAR5_ENC_FLAG_HAS_CHECKVAL = 0x01
257RAR5_ENDARC_FLAG_NEXT_VOL = 0x01
259RAR5_XFILE_ENCRYPTION = 1
260RAR5_XFILE_HASH = 2
261RAR5_XFILE_TIME = 3
262RAR5_XFILE_VERSION = 4
263RAR5_XFILE_REDIR = 5
264RAR5_XFILE_OWNER = 6
265RAR5_XFILE_SERVICE = 7
267RAR5_XTIME_UNIXTIME = 0x01
268RAR5_XTIME_HAS_MTIME = 0x02
269RAR5_XTIME_HAS_CTIME = 0x04
270RAR5_XTIME_HAS_ATIME = 0x08
271RAR5_XTIME_UNIXTIME_NS = 0x10
273RAR5_XENC_CIPHER_AES256 = 0
275RAR5_XENC_CHECKVAL = 0x01
276RAR5_XENC_TWEAKED = 0x02
278RAR5_XHASH_BLAKE2SP = 0
280RAR5_XREDIR_UNIX_SYMLINK = 1
281RAR5_XREDIR_WINDOWS_SYMLINK = 2
282RAR5_XREDIR_WINDOWS_JUNCTION = 3
283RAR5_XREDIR_HARD_LINK = 4
284RAR5_XREDIR_FILE_COPY = 5
286RAR5_XREDIR_ISDIR = 0x01
288RAR5_XOWNER_UNAME = 0x01
289RAR5_XOWNER_GNAME = 0x02
290RAR5_XOWNER_UID = 0x04
291RAR5_XOWNER_GID = 0x08
293RAR5_OS_WINDOWS = 0
294RAR5_OS_UNIX = 1
296DOS_MODE_ARCHIVE = 0x20
297DOS_MODE_DIR = 0x10
298DOS_MODE_SYSTEM = 0x04
299DOS_MODE_HIDDEN = 0x02
300DOS_MODE_READONLY = 0x01
302RAR5_PW_CHECK_SIZE = 8
303RAR5_PW_SUM_SIZE = 4
305##
306## internal constants
307##
309RAR_ID = b"Rar!\x1a\x07\x00"
310RAR5_ID = b"Rar!\x1a\x07\x01\x00"
312WIN32 = sys.platform == "win32"
313BSIZE = 512 * 1024 if WIN32 else 64 * 1024
315SFX_MAX_SIZE = 2 * 1024 * 1024
316RAR_V3 = 3
317RAR_V5 = 5
319_BAD_CHARS = r"""\x00-\x1F<>|"?*"""
320RC_BAD_CHARS_UNIX = re.compile(r"[%s]" % _BAD_CHARS)
321RC_BAD_CHARS_WIN32 = re.compile(r"[%s:^\\]" % _BAD_CHARS)
323FORCE_TOOL = False
326def _find_sfx_header(xfile):
327 sig = RAR_ID[:-1]
328 buf = io.BytesIO()
329 steps = (64, SFX_MAX_SIZE)
331 with XFile(xfile) as fd:
332 for step in steps:
333 data = fd.read(step)
334 if not data:
335 break
336 buf.write(data)
337 curdata = buf.getvalue()
338 findpos = 0
339 while True:
340 pos = curdata.find(sig, findpos)
341 if pos < 0:
342 break
343 if curdata[pos:pos + len(RAR_ID)] == RAR_ID:
344 return RAR_V3, pos
345 if curdata[pos:pos + len(RAR5_ID)] == RAR5_ID:
346 return RAR_V5, pos
347 findpos = pos + len(sig)
348 fd.restore_pos()
349 return 0, 0
352##
353## Public interface
354##
357def get_rar_version(xfile):
358 """Check quickly whether file is rar archive.
359 """
360 with XFile(xfile) as fd:
361 buf = fd.read(len(RAR5_ID))
362 fd.restore_pos()
363 if buf.startswith(RAR_ID):
364 return RAR_V3
365 elif buf.startswith(RAR5_ID):
366 return RAR_V5
367 return 0
370def is_rarfile(xfile):
371 """Check quickly whether file is rar archive.
372 """
373 try:
374 return get_rar_version(xfile) > 0
375 except OSError:
376 # File not found or not accessible, ignore
377 return False
380def is_rarfile_sfx(xfile):
381 """Check whether file is rar archive with support for SFX.
383 It will read 2M from file.
384 """
385 return _find_sfx_header(xfile)[0] > 0
388class Error(Exception):
389 """Base class for rarfile errors."""
392class BadRarFile(Error):
393 """Incorrect data in archive."""
396class NotRarFile(Error):
397 """The file is not RAR archive."""
400class BadRarName(Error):
401 """Cannot guess multipart name components."""
404class NoRarEntry(Error):
405 """File not found in RAR"""
408class PasswordRequired(Error):
409 """File requires password"""
412class BadSymLinkError(Error):
413 """Invalid symbolic link"""
416class NeedFirstVolume(Error):
417 """Need to start from first volume.
419 Attributes:
421 current_volume
422 Volume number of current file or None if not known
423 """
424 def __init__(self, msg, volume):
425 super().__init__(msg)
426 self.current_volume = volume
429class NoCrypto(Error):
430 """Cannot parse encrypted headers - no crypto available."""
433class RarExecError(Error):
434 """Problem reported by unrar/rar."""
437class RarWarning(RarExecError):
438 """Non-fatal error"""
441class RarFatalError(RarExecError):
442 """Fatal error"""
445class RarCRCError(RarExecError):
446 """CRC error during unpacking"""
449class RarLockedArchiveError(RarExecError):
450 """Must not modify locked archive"""
453class RarWriteError(RarExecError):
454 """Write error"""
457class RarOpenError(RarExecError):
458 """Open error"""
461class RarUserError(RarExecError):
462 """User error"""
465class RarMemoryError(RarExecError):
466 """Memory error"""
469class RarCreateError(RarExecError):
470 """Create error"""
473class RarNoFilesError(RarExecError):
474 """No files that match pattern were found"""
477class RarUserBreak(RarExecError):
478 """User stop"""
481class RarWrongPassword(RarExecError):
482 """Incorrect password"""
485class RarUnknownError(RarExecError):
486 """Unknown exit code"""
489class RarSignalExit(RarExecError):
490 """Unrar exited with signal"""
493class RarCannotExec(RarExecError):
494 """Executable not found."""
497class UnsupportedWarning(UserWarning):
498 """Archive uses feature that are unsupported by rarfile.
500 .. versionadded:: 4.0
501 """
504class RarInfo:
505 r"""An entry in rar archive.
507 Timestamps as :class:`~datetime.datetime` are without timezone in RAR3,
508 with UTC timezone in RAR5 archives.
510 Attributes:
512 filename
513 File name with relative path.
514 Path separator is "/". Always unicode string.
516 date_time
517 File modification timestamp. As tuple of (year, month, day, hour, minute, second).
518 RAR5 allows archives where it is missing, it's None then.
520 comment
521 Optional file comment field. Unicode string. (RAR3-only)
523 file_size
524 Uncompressed size.
526 compress_size
527 Compressed size.
529 compress_type
530 Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants.
532 extract_version
533 Minimal Rar version needed for decompressing. As (major*10 + minor),
534 so 2.9 is 29.
536 RAR3: 10, 20, 29
538 RAR5 does not have such field in archive, it's simply set to 50.
540 host_os
541 Host OS type, one of RAR_OS_* constants.
543 RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`,
544 :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`.
546 RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`.
548 mode
549 File attributes. May be either dos-style or unix-style, depending on host_os.
551 mtime
552 File modification time. Same value as :attr:`date_time`
553 but as :class:`~datetime.datetime` object with extended precision.
555 ctime
556 Optional time field: creation time. As :class:`~datetime.datetime` object.
558 atime
559 Optional time field: last access time. As :class:`~datetime.datetime` object.
561 arctime
562 Optional time field: archival time. As :class:`~datetime.datetime` object.
563 (RAR3-only)
565 CRC
566 CRC-32 of uncompressed file, unsigned int.
568 RAR5: may be None.
570 blake2sp_hash
571 Blake2SP hash over decompressed data. (RAR5-only)
573 volume
574 Volume nr, starting from 0.
576 volume_file
577 Volume file name, where file starts.
579 file_redir
580 If not None, file is link of some sort. Contains tuple of (type, flags, target).
581 (RAR5-only)
583 Type is one of constants:
585 :data:`RAR5_XREDIR_UNIX_SYMLINK`
586 Unix symlink.
587 :data:`RAR5_XREDIR_WINDOWS_SYMLINK`
588 Windows symlink.
589 :data:`RAR5_XREDIR_WINDOWS_JUNCTION`
590 Windows junction.
591 :data:`RAR5_XREDIR_HARD_LINK`
592 Hard link to target.
593 :data:`RAR5_XREDIR_FILE_COPY`
594 Current file is copy of another archive entry.
596 Flags may contain bits:
598 :data:`RAR5_XREDIR_ISDIR`
599 Symlink points to directory.
600 """
602 # zipfile-compatible fields
603 filename = None
604 file_size = None
605 compress_size = None
606 date_time = None
607 CRC = None
608 volume = None
609 orig_filename = None
611 # optional extended time fields, datetime() objects.
612 mtime = None
613 ctime = None
614 atime = None
616 extract_version = None
617 mode = None
618 host_os = None
619 compress_type = None
621 # rar3-only fields
622 comment = None
623 arctime = None
625 # rar5-only fields
626 blake2sp_hash = None
627 file_redir = None
629 # internal fields
630 flags = 0
631 type = None
633 # zipfile compat
634 def is_dir(self):
635 """Returns True if entry is a directory.
637 .. versionadded:: 4.0
638 """
639 return False
641 def is_symlink(self):
642 """Returns True if entry is a symlink.
644 .. versionadded:: 4.0
645 """
646 return False
648 def is_file(self):
649 """Returns True if entry is a normal file.
651 .. versionadded:: 4.0
652 """
653 return False
655 def needs_password(self):
656 """Returns True if data is stored password-protected.
657 """
658 if self.type == RAR_BLOCK_FILE:
659 return (self.flags & RAR_FILE_PASSWORD) > 0
660 return False
662 def isdir(self):
663 """Returns True if entry is a directory.
665 .. deprecated:: 4.0
666 """
667 return self.is_dir()
670class RarFile:
671 """Parse RAR structure, provide access to files in archive.
673 Parameters:
675 file
676 archive file name or file-like object.
677 mode
678 only "r" is supported.
679 charset
680 fallback charset to use, if filenames are not already Unicode-enabled.
681 info_callback
682 debug callback, gets to see all archive entries.
683 crc_check
684 set to False to disable CRC checks
685 errors
686 Either "stop" to quietly stop parsing on errors,
687 or "strict" to raise errors. Default is "stop".
688 part_only
689 If True, read only single file and allow it to be middle-part
690 of multi-volume archive.
692 .. versionadded:: 4.0
693 """
695 #: File name, if available. Unicode string or None.
696 filename = None
698 #: Archive comment. Unicode string or None.
699 comment = None
701 def __init__(self, file, mode="r", charset=None, info_callback=None,
702 crc_check=True, errors="stop", part_only=False):
703 if is_filelike(file):
704 self.filename = getattr(file, "name", None)
705 else:
706 if isinstance(file, Path):
707 file = str(file)
708 self.filename = file
709 self._rarfile = file
711 self._charset = charset or DEFAULT_CHARSET
712 self._info_callback = info_callback
713 self._crc_check = crc_check
714 self._part_only = part_only
715 self._password = None
716 self._file_parser = None
718 if errors == "stop":
719 self._strict = False
720 elif errors == "strict":
721 self._strict = True
722 else:
723 raise ValueError("Invalid value for errors= parameter.")
725 if mode != "r":
726 raise NotImplementedError("RarFile supports only mode=r")
728 self._parse()
730 def __enter__(self):
731 """Open context."""
732 return self
734 def __exit__(self, typ, value, traceback):
735 """Exit context."""
736 self.close()
738 def __iter__(self):
739 """Iterate over members."""
740 return iter(self.infolist())
742 def setpassword(self, pwd):
743 """Sets the password to use when extracting.
744 """
745 self._password = pwd
746 if self._file_parser:
747 if self._file_parser.has_header_encryption():
748 self._file_parser = None
749 if not self._file_parser:
750 self._parse()
751 else:
752 self._file_parser.setpassword(self._password)
754 def needs_password(self):
755 """Returns True if any archive entries require password for extraction.
756 """
757 return self._file_parser.needs_password()
759 def is_solid(self):
760 """Returns True if archive uses solid compression.
762 .. versionadded:: 4.2
763 """
764 return self._file_parser.is_solid()
766 def namelist(self):
767 """Return list of filenames in archive.
768 """
769 return [f.filename for f in self.infolist()]
771 def infolist(self):
772 """Return RarInfo objects for all files/directories in archive.
773 """
774 return self._file_parser.infolist()
776 def volumelist(self):
777 """Returns filenames of archive volumes.
779 In case of single-volume archive, the list contains
780 just the name of main archive file.
781 """
782 return self._file_parser.volumelist()
784 def getinfo(self, name):
785 """Return RarInfo for file.
786 """
787 return self._file_parser.getinfo(name)
789 def getinfo_orig(self, name):
790 """Return RarInfo for file source.
792 RAR5: if name is hard-linked or copied file,
793 returns original entry with original filename.
795 .. versionadded:: 4.1
796 """
797 return self._file_parser.getinfo_orig(name)
799 def open(self, name, mode="r", pwd=None):
800 """Returns file-like object (:class:`RarExtFile`) from where the data can be read.
802 The object implements :class:`io.RawIOBase` interface, so it can
803 be further wrapped with :class:`io.BufferedReader`
804 and :class:`io.TextIOWrapper`.
806 On older Python where io module is not available, it implements
807 only .read(), .seek(), .tell() and .close() methods.
809 The object is seekable, although the seeking is fast only on
810 uncompressed files, on compressed files the seeking is implemented
811 by reading ahead and/or restarting the decompression.
813 Parameters:
815 name
816 file name or RarInfo instance.
817 mode
818 must be "r"
819 pwd
820 password to use for extracting.
821 """
823 if mode != "r":
824 raise NotImplementedError("RarFile.open() supports only mode=r")
826 # entry lookup
827 inf = self.getinfo(name)
828 if inf.is_dir():
829 raise io.UnsupportedOperation("Directory does not have any data: " + inf.filename)
831 # check password
832 if inf.needs_password():
833 pwd = pwd or self._password
834 if pwd is None:
835 raise PasswordRequired("File %s requires password" % inf.filename)
836 else:
837 pwd = None
839 return self._file_parser.open(inf, pwd)
841 def read(self, name, pwd=None):
842 """Return uncompressed data for archive entry.
844 For longer files using :meth:`~RarFile.open` may be better idea.
846 Parameters:
848 name
849 filename or RarInfo instance
850 pwd
851 password to use for extracting.
852 """
854 with self.open(name, "r", pwd) as f:
855 return f.read()
857 def close(self):
858 """Release open resources."""
859 pass
861 def printdir(self, file=None):
862 """Print archive file list to stdout or given file.
863 """
864 if file is None:
865 file = sys.stdout
866 for f in self.infolist():
867 print(f.filename, file=file)
869 def extract(self, member, path=None, pwd=None):
870 """Extract single file into current directory.
872 Parameters:
874 member
875 filename or :class:`RarInfo` instance
876 path
877 optional destination path
878 pwd
879 optional password to use
880 """
881 inf = self.getinfo(member)
882 return self._extract_one(inf, path, pwd, True)
884 def extractall(self, path=None, members=None, pwd=None):
885 """Extract all files into current directory.
887 Parameters:
889 path
890 optional destination path
891 members
892 optional filename or :class:`RarInfo` instance list to extract
893 pwd
894 optional password to use
895 """
896 if members is None:
897 members = self.namelist()
899 done = set()
900 dirs = []
901 for m in members:
902 inf = self.getinfo(m)
903 dst = self._extract_one(inf, path, pwd, not inf.is_dir())
904 if inf.is_dir():
905 if dst not in done:
906 dirs.append((dst, inf))
907 done.add(dst)
908 if dirs:
909 dirs.sort(reverse=True)
910 for dst, inf in dirs:
911 self._set_attrs(inf, dst)
913 def testrar(self, pwd=None):
914 """Read all files and test CRC.
915 """
916 for member in self.infolist():
917 if member.is_file():
918 with self.open(member, 'r', pwd) as f:
919 empty_read(f, member.file_size, BSIZE)
921 def strerror(self):
922 """Return error string if parsing failed or None if no problems.
923 """
924 if not self._file_parser:
925 return "Not a RAR file"
926 return self._file_parser.strerror()
928 ##
929 ## private methods
930 ##
932 def _parse(self):
933 """Run parser for file type
934 """
935 ver, sfx_ofs = _find_sfx_header(self._rarfile)
936 if ver == RAR_V3:
937 p3 = RAR3Parser(self._rarfile, self._password, self._crc_check,
938 self._charset, self._strict, self._info_callback,
939 sfx_ofs, self._part_only)
940 self._file_parser = p3 # noqa
941 elif ver == RAR_V5:
942 p5 = RAR5Parser(self._rarfile, self._password, self._crc_check,
943 self._charset, self._strict, self._info_callback,
944 sfx_ofs, self._part_only)
945 self._file_parser = p5 # noqa
946 else:
947 raise NotRarFile("Not a RAR file")
949 self._file_parser.parse()
950 self.comment = self._file_parser.comment
952 def _extract_one(self, info, path, pwd, set_attrs):
953 fname = sanitize_filename(
954 info.filename, os.path.sep, WIN32
955 )
957 if path is None:
958 path = os.getcwd()
959 else:
960 path = os.fspath(path)
961 dstfn = os.path.join(path, fname)
963 # Reject members whose destination escapes `path` once symlinks
964 # already created on disk are resolved. Without this, a symlink
965 # member can point outside `path` and a later file/dir member
966 # named through it will be written outside the extraction root.
967 real_path = os.path.realpath(path)
968 real_dst = os.path.realpath(dstfn)
969 if real_dst != real_path and not real_dst.startswith(real_path + os.sep):
970 raise BadRarFile(
971 "Refusing to extract entry that escapes destination: %r" % info.filename
972 )
974 dirname = os.path.dirname(dstfn)
975 if dirname and dirname != ".":
976 os.makedirs(dirname, exist_ok=True)
978 if info.is_file():
979 return self._make_file(info, dstfn, pwd, set_attrs)
980 if info.is_dir():
981 return self._make_dir(info, dstfn, pwd, set_attrs)
982 if info.is_symlink():
983 return self._make_symlink(info, dstfn, pwd, set_attrs, path)
984 return None
986 def _create_helper(self, name, flags, info):
987 return os.open(name, flags)
989 def _make_file(self, info, dstfn, pwd, set_attrs):
990 def helper(name, flags):
991 return self._create_helper(name, flags, info)
992 with self.open(info, "r", pwd) as src:
993 with open(dstfn, "wb", opener=helper) as dst:
994 shutil.copyfileobj(src, dst)
995 if set_attrs:
996 self._set_attrs(info, dstfn)
997 return dstfn
999 def _make_dir(self, info, dstfn, pwd, set_attrs):
1000 os.makedirs(dstfn, exist_ok=True)
1001 if set_attrs:
1002 self._set_attrs(info, dstfn)
1003 return dstfn
1005 def _make_symlink(self, info, dstfn, pwd, set_attrs, top):
1006 target_is_directory = False
1007 if info.host_os == RAR_OS_UNIX:
1008 link_name = self.read(info, pwd).decode("utf8", "replace")
1009 target_is_directory = (info.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY
1010 elif info.file_redir:
1011 redir_type, redir_flags, link_name = info.file_redir
1012 if redir_type == RAR5_XREDIR_WINDOWS_JUNCTION:
1013 warnings.warn(f"Windows junction not supported - {info.filename}", UnsupportedWarning)
1014 return None
1015 target_is_directory = (redir_type & RAR5_XREDIR_ISDIR) > 0
1016 else:
1017 warnings.warn(f"Unsupported link type - {info.filename}", UnsupportedWarning)
1018 return None
1020 # disallow abs paths
1021 target = os.path.normpath(link_name)
1022 if os.path.isabs(target) or os.path.splitdrive(target)[0]:
1023 raise BadSymLinkError('Absolute links not allowed')
1025 # disallow ../ traversal
1026 dest_abs = os.path.realpath(top)
1027 target_base = os.path.dirname(dstfn)
1028 target_abs = os.path.realpath(os.path.join(target_base, target))
1029 if os.path.commonpath([target_abs, dest_abs]) != dest_abs:
1030 raise BadSymLinkError('Link to outside not allowed')
1032 os.symlink(link_name, dstfn, target_is_directory=target_is_directory)
1033 return dstfn
1035 def _set_attrs(self, info, dstfn):
1036 if info.host_os == RAR_OS_UNIX:
1037 os.chmod(dstfn, info.mode & 0o777)
1038 elif info.host_os in (RAR_OS_WIN32, RAR_OS_MSDOS):
1039 # only keep R/O attr, except for dirs on win32
1040 if info.mode & DOS_MODE_READONLY and (info.is_file() or not WIN32):
1041 st = os.stat(dstfn)
1042 new_mode = st.st_mode & ~0o222
1043 os.chmod(dstfn, new_mode)
1045 if info.mtime:
1046 mtime_ns = to_nsecs(info.mtime)
1047 atime_ns = to_nsecs(info.atime) if info.atime else mtime_ns
1048 os.utime(dstfn, ns=(atime_ns, mtime_ns))
1051#
1052# File format parsing
1053#
1055class CommonParser:
1056 """Shared parser parts."""
1057 _main = None
1058 _hdrenc_main = None
1059 _needs_password = False
1060 _fd = None
1061 _expect_sig = None
1062 _parse_error = None
1063 _password = None
1064 comment = None
1066 def __init__(self, rarfile, password, crc_check, charset, strict,
1067 info_cb, sfx_offset, part_only):
1068 self._rarfile = rarfile
1069 self._password = password
1070 self._crc_check = crc_check
1071 self._charset = charset
1072 self._strict = strict
1073 self._info_callback = info_cb
1074 self._info_list = []
1075 self._info_map = {}
1076 self._vol_list = []
1077 self._sfx_offset = sfx_offset
1078 self._part_only = part_only
1080 def is_solid(self):
1081 """Returns True if archive uses solid compression.
1082 """
1083 if self._main:
1084 if self._main.flags & RAR_MAIN_SOLID:
1085 return True
1086 return False
1088 def has_header_encryption(self):
1089 """Returns True if headers are encrypted
1090 """
1091 if self._hdrenc_main:
1092 return True
1093 if self._main:
1094 if self._main.flags & RAR_MAIN_PASSWORD:
1095 return True
1096 return False
1098 def setpassword(self, pwd):
1099 """Set cached password."""
1100 self._password = pwd
1102 def volumelist(self):
1103 """Volume files"""
1104 return self._vol_list
1106 def needs_password(self):
1107 """Is password required"""
1108 return self._needs_password
1110 def strerror(self):
1111 """Last error"""
1112 return self._parse_error
1114 def infolist(self):
1115 """List of RarInfo records.
1116 """
1117 return self._info_list
1119 def getinfo(self, member):
1120 """Return RarInfo for filename
1121 """
1122 if isinstance(member, RarInfo):
1123 fname = member.filename
1124 elif isinstance(member, Path):
1125 fname = str(member)
1126 else:
1127 fname = member
1129 if fname.endswith("/"):
1130 fname = fname.rstrip("/")
1132 try:
1133 return self._info_map[fname]
1134 except KeyError:
1135 raise NoRarEntry("No such file: %s" % fname) from None
1137 def getinfo_orig(self, member):
1138 inf = self.getinfo(member)
1139 if inf.file_redir:
1140 redir_type, redir_flags, redir_name = inf.file_redir
1141 # cannot leave to unrar as it expects copied file to exist
1142 if redir_type in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK):
1143 inf = self.getinfo(redir_name)
1144 return inf
1146 def parse(self):
1147 """Process file."""
1148 self._fd = None
1149 try:
1150 self._parse_real()
1151 finally:
1152 if self._fd:
1153 self._fd.close()
1154 self._fd = None
1156 def _parse_real(self):
1157 """Actually read file.
1158 """
1159 fd = XFile(self._rarfile)
1160 self._fd = fd
1161 fd.seek(self._sfx_offset, 0)
1162 sig = fd.read(len(self._expect_sig))
1163 if sig != self._expect_sig:
1164 raise NotRarFile("Not a Rar archive")
1166 volume = 0 # first vol (.rar) is 0
1167 more_vols = False
1168 endarc = False
1169 volfile = self._rarfile
1170 self._vol_list = [self._rarfile]
1171 raise_need_first_vol = False
1172 while True:
1173 if endarc:
1174 h = None # don"t read past ENDARC
1175 else:
1176 h = self._parse_header(fd)
1177 if not h:
1178 if raise_need_first_vol:
1179 # did not find ENDARC with VOLNR
1180 raise NeedFirstVolume("Need to start from first volume", None)
1181 if more_vols and not self._part_only:
1182 volume += 1
1183 fd.close()
1184 try:
1185 volfile = self._next_volname(volfile)
1186 fd = XFile(volfile)
1187 except IOError:
1188 self._set_error("Cannot open next volume: %s", volfile)
1189 break
1190 self._fd = fd
1191 sig = fd.read(len(self._expect_sig))
1192 if sig != self._expect_sig:
1193 self._set_error("Invalid volume sig: %s", volfile)
1194 break
1195 more_vols = False
1196 endarc = False
1197 self._vol_list.append(volfile)
1198 self._main = None
1199 self._hdrenc_main = None
1200 continue
1201 break
1202 h.volume = volume
1203 h.volume_file = volfile
1205 if h.type == RAR_BLOCK_MAIN and not self._main:
1206 self._main = h
1207 if volume == 0 and (h.flags & RAR_MAIN_NEWNUMBERING) and not self._part_only:
1208 # RAR 2.x does not set FIRSTVOLUME,
1209 # so check it only if NEWNUMBERING is used
1210 if (h.flags & RAR_MAIN_FIRSTVOLUME) == 0:
1211 if getattr(h, "main_volume_number", None) is not None:
1212 # rar5 may have more info
1213 raise NeedFirstVolume(
1214 "Need to start from first volume (current: %r)"
1215 % (h.main_volume_number,),
1216 h.main_volume_number
1217 )
1218 # delay raise until we have volnr from ENDARC
1219 raise_need_first_vol = True
1220 if h.flags & RAR_MAIN_PASSWORD:
1221 self._needs_password = True
1222 if not self._password:
1223 break
1224 elif h.type == RAR_BLOCK_ENDARC:
1225 # use flag, but also allow RAR 2.x logic below to trigger
1226 if h.flags & RAR_ENDARC_NEXT_VOLUME:
1227 more_vols = True
1228 endarc = True
1229 if raise_need_first_vol and (h.flags & RAR_ENDARC_VOLNR) > 0:
1230 raise NeedFirstVolume(
1231 "Need to start from first volume (current: %r)"
1232 % (h.endarc_volnr,),
1233 h.endarc_volnr
1234 )
1235 elif h.type == RAR_BLOCK_FILE:
1236 # RAR 2.x does not write RAR_BLOCK_ENDARC
1237 if h.flags & RAR_FILE_SPLIT_AFTER:
1238 more_vols = True
1239 # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
1240 if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE:
1241 if not self._part_only:
1242 raise_need_first_vol = True
1244 if h.needs_password():
1245 self._needs_password = True
1247 # store it
1248 self.process_entry(fd, h)
1250 if self._info_callback:
1251 self._info_callback(h)
1253 # go to next header
1254 if h.add_size > 0:
1255 fd.seek(h.data_offset + h.add_size, 0)
1257 def process_entry(self, fd, item):
1258 """Examine item, add into lookup cache."""
1259 raise NotImplementedError()
1261 def _decrypt_header(self, fd):
1262 raise NotImplementedError("_decrypt_header")
1264 def _parse_block_header(self, fd):
1265 raise NotImplementedError("_parse_block_header")
1267 def _open_hack(self, inf, pwd):
1268 raise NotImplementedError("_open_hack")
1270 def _parse_header(self, fd):
1271 """Read single header
1272 """
1273 try:
1274 # handle encrypted headers
1275 if (self._main and self._main.flags & RAR_MAIN_PASSWORD) or self._hdrenc_main:
1276 if not self._password:
1277 return None
1278 fd = self._decrypt_header(fd)
1280 # now read actual header
1281 return self._parse_block_header(fd)
1282 except struct.error:
1283 self._set_error("Broken header in RAR file")
1284 return None
1286 def _next_volname(self, volfile):
1287 """Given current vol name, construct next one
1288 """
1289 if is_filelike(volfile):
1290 raise IOError("Working on single FD")
1291 if self._main.flags & RAR_MAIN_NEWNUMBERING:
1292 return _next_newvol(volfile)
1293 return _next_oldvol(volfile)
1295 def _set_error(self, msg, *args):
1296 if args:
1297 msg = msg % args
1298 self._parse_error = msg
1299 if self._strict:
1300 raise BadRarFile(msg)
1302 def open(self, inf, pwd):
1303 """Return stream object for file data."""
1305 if inf.file_redir:
1306 redir_type, redir_flags, redir_name = inf.file_redir
1307 # cannot leave to unrar as it expects copied file to exist
1308 if redir_type in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK):
1309 inf = self.getinfo(redir_name)
1310 if not inf:
1311 raise BadRarFile("cannot find copied file")
1312 elif redir_type in (
1313 RAR5_XREDIR_UNIX_SYMLINK, RAR5_XREDIR_WINDOWS_SYMLINK,
1314 RAR5_XREDIR_WINDOWS_JUNCTION,
1315 ):
1316 return io.BytesIO(redir_name.encode("utf8"))
1317 if inf.flags & RAR_FILE_SPLIT_BEFORE:
1318 raise NeedFirstVolume("Partial file, please start from first volume: " + inf.filename, None)
1320 # is temp write usable?
1321 use_hack = 1
1322 if not self._main:
1323 use_hack = 0
1324 elif self._main._must_disable_hack():
1325 use_hack = 0
1326 elif inf._must_disable_hack():
1327 use_hack = 0
1328 elif is_filelike(self._rarfile):
1329 pass
1330 elif inf.file_size > HACK_SIZE_LIMIT:
1331 use_hack = 0
1332 elif not USE_EXTRACT_HACK:
1333 use_hack = 0
1335 # now extract
1336 if inf.compress_type == RAR_M0 and (inf.flags & RAR_FILE_PASSWORD) == 0 and inf.file_redir is None:
1337 return self._open_clear(inf)
1338 elif use_hack:
1339 return self._open_hack(inf, pwd)
1340 elif is_filelike(self._rarfile):
1341 return self._open_unrar_membuf(self._rarfile, inf, pwd)
1342 else:
1343 return self._open_unrar(self._rarfile, inf, pwd)
1345 def _open_clear(self, inf):
1346 if FORCE_TOOL:
1347 return self._open_unrar(self._rarfile, inf)
1348 return DirectReader(self, inf)
1350 def _open_hack_core(self, inf, pwd, prefix, suffix):
1352 size = inf.compress_size + inf.header_size
1353 rf = XFile(inf.volume_file, 0)
1354 rf.seek(inf.header_offset)
1356 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR)
1357 tmpf = os.fdopen(tmpfd, "wb")
1359 try:
1360 tmpf.write(prefix)
1361 while size > 0:
1362 if size > BSIZE:
1363 buf = rf.read(BSIZE)
1364 else:
1365 buf = rf.read(size)
1366 if not buf:
1367 raise BadRarFile("read failed: " + inf.filename)
1368 tmpf.write(buf)
1369 size -= len(buf)
1370 tmpf.write(suffix)
1371 tmpf.close()
1372 rf.close()
1373 except BaseException:
1374 rf.close()
1375 tmpf.close()
1376 os.unlink(tmpname)
1377 raise
1379 return self._open_unrar(tmpname, inf, pwd, tmpname)
1381 def _open_unrar_membuf(self, memfile, inf, pwd):
1382 """Write in-memory archive to temp file, needed for solid archives.
1383 """
1384 tmpname = membuf_tempfile(memfile)
1385 return self._open_unrar(tmpname, inf, pwd, tmpname, force_file=True)
1387 def _open_unrar(self, rarfile, inf, pwd=None, tmpfile=None, force_file=False):
1388 """Extract using unrar
1389 """
1390 setup = tool_setup()
1392 # not giving filename avoids encoding related problems
1393 fn = None
1394 if not tmpfile or force_file:
1395 fn = inf.filename.replace("/", os.path.sep)
1397 # read from unrar pipe
1398 cmd = setup.open_cmdline(pwd, rarfile, fn)
1399 return PipeReader(self, inf, cmd, tmpfile)
1402#
1403# RAR3 format
1404#
1406class Rar3Info(RarInfo):
1407 """RAR3 specific fields."""
1408 extract_version = 15
1409 salt = None
1410 add_size = 0
1411 header_crc = None
1412 header_size = None
1413 header_offset = None
1414 data_offset = None
1415 _md_class = None
1416 _md_expect = None
1417 _name_size = None
1419 # make sure some rar5 fields are always present
1420 file_redir = None
1421 blake2sp_hash = None
1423 endarc_datacrc = None
1424 endarc_volnr = None
1426 old_sub_type = None
1428 def _must_disable_hack(self):
1429 if self.type == RAR_BLOCK_FILE:
1430 if self.flags & RAR_FILE_PASSWORD:
1431 return True
1432 elif self.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
1433 return True
1434 elif self.type == RAR_BLOCK_MAIN:
1435 if self.flags & (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD):
1436 return True
1437 return False
1439 def is_dir(self):
1440 """Returns True if entry is a directory."""
1441 if self.type == RAR_BLOCK_FILE and not self.is_symlink():
1442 return (self.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY
1443 return False
1445 def is_symlink(self):
1446 """Returns True if entry is a symlink."""
1447 return (
1448 self.type == RAR_BLOCK_FILE and
1449 self.host_os == RAR_OS_UNIX and
1450 self.mode & 0xF000 == 0xA000
1451 )
1453 def is_file(self):
1454 """Returns True if entry is a normal file."""
1455 return (
1456 self.type == RAR_BLOCK_FILE and
1457 not (self.is_dir() or self.is_symlink())
1458 )
1461class RAR3Parser(CommonParser):
1462 """Parse RAR3 file format.
1463 """
1464 _expect_sig = RAR_ID
1465 _last_aes_key = (None, None, None) # (salt, key, iv)
1467 def _decrypt_header(self, fd):
1468 if not _have_crypto:
1469 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1470 salt = fd.read(8)
1471 if self._last_aes_key[0] == salt:
1472 key, iv = self._last_aes_key[1:]
1473 else:
1474 key, iv = rar3_s2k(self._password, salt)
1475 self._last_aes_key = (salt, key, iv)
1476 return HeaderDecrypt(fd, key, iv)
1478 def _parse_block_header(self, fd):
1479 """Parse common block header
1480 """
1481 h = Rar3Info()
1482 h.header_offset = fd.tell()
1484 # read and parse base header
1485 buf = fd.read(S_BLK_HDR.size)
1486 if not buf:
1487 return None
1488 if len(buf) < S_BLK_HDR.size:
1489 self._set_error("Unexpected EOF when reading header")
1490 return None
1491 t = S_BLK_HDR.unpack_from(buf)
1492 h.header_crc, h.type, h.flags, h.header_size = t
1494 # read full header
1495 if h.header_size > S_BLK_HDR.size:
1496 hdata = buf + fd.read(h.header_size - S_BLK_HDR.size)
1497 else:
1498 hdata = buf
1499 h.data_offset = fd.tell()
1501 # unexpected EOF?
1502 if len(hdata) != h.header_size:
1503 self._set_error("Unexpected EOF when reading header")
1504 return None
1506 pos = S_BLK_HDR.size
1508 # block has data assiciated with it?
1509 if h.flags & RAR_LONG_BLOCK:
1510 h.add_size, pos = load_le32(hdata, pos)
1511 else:
1512 h.add_size = 0
1514 # parse interesting ones, decide header boundaries for crc
1515 if h.type == RAR_BLOCK_MARK:
1516 return h
1517 elif h.type == RAR_BLOCK_MAIN:
1518 pos += 6
1519 if h.flags & RAR_MAIN_ENCRYPTVER:
1520 pos += 1
1521 crc_pos = pos
1522 if h.flags & RAR_MAIN_COMMENT:
1523 self._parse_subblocks(h, hdata, pos)
1524 elif h.type == RAR_BLOCK_FILE:
1525 pos = self._parse_file_header(h, hdata, pos - 4)
1526 crc_pos = pos
1527 if h.flags & RAR_FILE_COMMENT:
1528 pos = self._parse_subblocks(h, hdata, pos)
1529 elif h.type == RAR_BLOCK_SUB:
1530 pos = self._parse_file_header(h, hdata, pos - 4)
1531 crc_pos = h.header_size
1532 elif h.type == RAR_BLOCK_OLD_AUTH:
1533 pos += 8
1534 crc_pos = pos
1535 elif h.type == RAR_BLOCK_OLD_EXTRA:
1536 pos += 7
1537 crc_pos = pos
1538 elif h.type == RAR_BLOCK_OLD_SUB:
1539 pos = self._parse_old_subblock(h, hdata, pos)
1541 # these types do not have their own data CRC,
1542 # so data was included in header CRC.
1543 if h.old_sub_type in (RAR_OLD_SUB_UNIX, RAR_OLD_SUB_MAC):
1544 # skip CRC check, it requires to read data part
1545 return h
1547 crc_pos = h.header_size
1548 elif h.type == RAR_BLOCK_ENDARC:
1549 if h.flags & RAR_ENDARC_DATACRC:
1550 h.endarc_datacrc, pos = load_le32(hdata, pos)
1551 if h.flags & RAR_ENDARC_VOLNR:
1552 h.endarc_volnr = S_SHORT.unpack_from(hdata, pos)[0]
1553 pos += 2
1554 crc_pos = h.header_size
1555 else:
1556 crc_pos = h.header_size
1558 # calculate crc
1559 crcdat = hdata[2:crc_pos]
1560 calc_crc = crc32(crcdat) & 0xFFFF
1562 # return good header
1563 if h.header_crc == calc_crc:
1564 return h
1566 # header parsing failed.
1567 self._set_error("Header CRC error (%02x): exp=%x got=%x (xlen = %d)",
1568 h.type, h.header_crc, calc_crc, len(crcdat))
1570 # instead panicing, send eof
1571 return None
1573 def _parse_file_header(self, h, hdata, pos):
1574 """Read file-specific header
1575 """
1576 fld = S_FILE_HDR.unpack_from(hdata, pos)
1577 pos += S_FILE_HDR.size
1579 h.compress_size = fld[0]
1580 h.file_size = fld[1]
1581 h.host_os = fld[2]
1582 h.CRC = fld[3]
1583 h.date_time = parse_dos_time(fld[4])
1584 h.mtime = to_datetime(h.date_time)
1585 h.extract_version = fld[5]
1586 h.compress_type = fld[6]
1587 h._name_size = name_size = fld[7]
1588 h.mode = fld[8]
1590 h._md_class = CRC32Context
1591 h._md_expect = h.CRC
1593 if h.flags & RAR_FILE_LARGE:
1594 h1, pos = load_le32(hdata, pos)
1595 h2, pos = load_le32(hdata, pos)
1596 h.compress_size |= h1 << 32
1597 h.file_size |= h2 << 32
1598 h.add_size = h.compress_size
1600 name, pos = load_bytes(hdata, name_size, pos)
1601 if h.flags & RAR_FILE_UNICODE and b"\0" in name:
1602 # stored in custom encoding
1603 nul = name.find(b"\0")
1604 h.orig_filename = name[:nul]
1605 u = UnicodeFilename(h.orig_filename, name[nul + 1:])
1606 h.filename = u.decode()
1608 # if parsing failed fall back to simple name
1609 if u.failed:
1610 h.filename = self._decode(h.orig_filename)
1611 elif h.flags & RAR_FILE_UNICODE:
1612 # stored in UTF8
1613 h.orig_filename = name
1614 h.filename = name.decode("utf8", "replace")
1615 else:
1616 # stored in random encoding
1617 h.orig_filename = name
1618 h.filename = self._decode(name)
1620 # change separator, set dir suffix
1621 h.filename = h.filename.replace("\\", "/").rstrip("/")
1622 if h.is_dir():
1623 h.filename = h.filename + "/"
1625 if h.flags & RAR_FILE_SALT:
1626 h.salt, pos = load_bytes(hdata, 8, pos)
1627 else:
1628 h.salt = None
1630 # optional extended time stamps
1631 if h.flags & RAR_FILE_EXTTIME:
1632 pos = _parse_ext_time(h, hdata, pos)
1633 else:
1634 h.mtime = h.atime = h.ctime = h.arctime = None
1636 return pos
1638 def _parse_old_subblock(self, h, hdata, pos):
1639 """Parse RAR2 subblock
1640 """
1641 h.old_sub_type, _reserved = S_OLD_SUBBLOCK_HDR.unpack_from(hdata, pos)
1642 return pos
1644 def _parse_subblocks(self, h, hdata, pos):
1645 """Find old-style comment subblock
1646 """
1647 while pos < len(hdata):
1648 # ordinary block header
1649 t = S_BLK_HDR.unpack_from(hdata, pos)
1650 ___scrc, stype, sflags, slen = t
1651 pos_next = pos + slen
1652 pos += S_BLK_HDR.size
1654 # corrupt header
1655 if pos_next < pos:
1656 break
1658 # followed by block-specific header
1659 if stype == RAR_BLOCK_OLD_COMMENT and pos + S_COMMENT_HDR.size <= pos_next:
1660 declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos)
1661 pos += S_COMMENT_HDR.size
1662 data = hdata[pos: pos_next]
1663 cmt = rar3_decompress(ver, meth, data, declen, sflags,
1664 crc, self._password)
1665 if not self._crc_check or (crc32(cmt) & 0xFFFF == crc):
1666 h.comment = self._decode_comment(cmt)
1668 pos = pos_next
1669 return pos
1671 def _read_comment_v3(self, inf, pwd=None):
1673 # read data
1674 with XFile(inf.volume_file) as rf:
1675 rf.seek(inf.data_offset)
1676 data = rf.read(inf.compress_size)
1678 # decompress
1679 cmt = rar3_decompress(inf.extract_version, inf.compress_type, data,
1680 inf.file_size, inf.flags, inf.CRC, pwd, inf.salt)
1682 # check crc
1683 if self._crc_check:
1684 crc = crc32(cmt)
1685 if crc != inf.CRC:
1686 return None
1688 return self._decode_comment(cmt)
1690 def _decode(self, val):
1691 for c in TRY_ENCODINGS:
1692 try:
1693 return val.decode(c)
1694 except UnicodeError:
1695 pass
1696 return val.decode(self._charset, "replace")
1698 def _decode_comment(self, val):
1699 return self._decode(val)
1701 def process_entry(self, fd, item):
1702 if item.type == RAR_BLOCK_FILE:
1703 # use only first part
1704 if item.flags & RAR_FILE_VERSION:
1705 pass # skip old versions
1706 elif (item.flags & RAR_FILE_SPLIT_BEFORE) == 0:
1707 self._info_map[item.filename.rstrip("/")] = item
1708 self._info_list.append(item)
1709 elif len(self._info_list) > 0:
1710 # final crc is in last block
1711 old = self._info_list[-1]
1712 old.CRC = item.CRC
1713 old._md_expect = item._md_expect
1714 old.compress_size += item.compress_size
1716 # parse new-style comment
1717 if item.type == RAR_BLOCK_SUB and item.filename == "CMT":
1718 if item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
1719 pass
1720 elif item.flags & RAR_FILE_SOLID:
1721 # file comment
1722 cmt = self._read_comment_v3(item, self._password)
1723 if len(self._info_list) > 0:
1724 old = self._info_list[-1]
1725 old.comment = cmt
1726 else:
1727 # archive comment
1728 cmt = self._read_comment_v3(item, self._password)
1729 self.comment = cmt
1731 if item.type == RAR_BLOCK_MAIN:
1732 if item.flags & RAR_MAIN_COMMENT:
1733 self.comment = item.comment
1734 if item.flags & RAR_MAIN_PASSWORD:
1735 self._needs_password = True
1737 # put file compressed data into temporary .rar archive, and run
1738 # unrar on that, thus avoiding unrar going over whole archive
1739 def _open_hack(self, inf, pwd):
1740 # create main header: crc, type, flags, size, res1, res2
1741 prefix = RAR_ID + S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + b"\0" * (2 + 4)
1742 return self._open_hack_core(inf, pwd, prefix, b"")
1745#
1746# RAR5 format
1747#
1749class Rar5Info(RarInfo):
1750 """Shared fields for RAR5 records.
1751 """
1752 extract_version = 50
1753 header_crc = None
1754 header_size = None
1755 header_offset = None
1756 data_offset = None
1758 # type=all
1759 block_type = None
1760 block_flags = None
1761 add_size = 0
1762 block_extra_size = 0
1764 # type=MAIN
1765 volume_number = None
1766 _md_class = None
1767 _md_expect = None
1769 def _must_disable_hack(self):
1770 return False
1773class Rar5BaseFile(Rar5Info):
1774 """Shared sturct for file & service record.
1775 """
1776 type = -1
1777 file_flags = None
1778 file_encryption = (0, 0, 0, b"", b"", b"")
1779 file_compress_flags = None
1780 file_redir = None
1781 file_owner = None
1782 file_version = None
1783 blake2sp_hash = None
1785 def _must_disable_hack(self):
1786 if self.flags & RAR_FILE_PASSWORD:
1787 return True
1788 if self.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER):
1789 return True
1790 if self.file_compress_flags & RAR5_COMPR_SOLID:
1791 return True
1792 if self.file_redir:
1793 return True
1794 return False
1797class Rar5FileInfo(Rar5BaseFile):
1798 """RAR5 file record.
1799 """
1800 type = RAR_BLOCK_FILE
1802 def is_symlink(self):
1803 """Returns True if entry is a symlink."""
1804 # pylint: disable=unsubscriptable-object
1805 return (
1806 self.file_redir is not None and
1807 self.file_redir[0] in (
1808 RAR5_XREDIR_UNIX_SYMLINK,
1809 RAR5_XREDIR_WINDOWS_SYMLINK,
1810 RAR5_XREDIR_WINDOWS_JUNCTION,
1811 )
1812 )
1814 def is_file(self):
1815 """Returns True if entry is a normal file."""
1816 return not (self.is_dir() or self.is_symlink())
1818 def is_dir(self):
1819 """Returns True if entry is a directory."""
1820 if not self.file_redir:
1821 if self.file_flags & RAR5_FILE_FLAG_ISDIR:
1822 return True
1823 return False
1826class Rar5ServiceInfo(Rar5BaseFile):
1827 """RAR5 service record.
1828 """
1829 type = RAR_BLOCK_SUB
1832class Rar5MainInfo(Rar5Info):
1833 """RAR5 archive main record.
1834 """
1835 type = RAR_BLOCK_MAIN
1836 main_flags = None
1837 main_volume_number = None
1839 def _must_disable_hack(self):
1840 if self.main_flags & RAR5_MAIN_FLAG_SOLID:
1841 return True
1842 return False
1845class Rar5EncryptionInfo(Rar5Info):
1846 """RAR5 archive header encryption record.
1847 """
1848 type = RAR5_BLOCK_ENCRYPTION
1849 encryption_algo = None
1850 encryption_flags = None
1851 encryption_kdf_count = None
1852 encryption_salt = None
1853 encryption_check_value = None
1855 def needs_password(self):
1856 return True
1859class Rar5EndArcInfo(Rar5Info):
1860 """RAR5 end of archive record.
1861 """
1862 type = RAR_BLOCK_ENDARC
1863 endarc_flags = None
1866class RAR5Parser(CommonParser):
1867 """Parse RAR5 format.
1868 """
1869 _expect_sig = RAR5_ID
1870 _hdrenc_main = None
1872 # AES encrypted headers
1873 _last_aes256_key = (-1, None, None) # (kdf_count, salt, key)
1875 def _get_utf8_password(self):
1876 pwd = self._password
1877 if isinstance(pwd, str):
1878 return pwd.encode("utf8")
1879 return pwd
1881 def _gen_key(self, kdf_count, salt):
1882 if self._last_aes256_key[:2] == (kdf_count, salt):
1883 return self._last_aes256_key[2]
1884 if kdf_count > RAR_MAX_KDF_SHIFT:
1885 raise BadRarFile("Too large kdf_count")
1886 pwd = self._get_utf8_password()
1887 key = rar5_s2k(pwd, salt, 1 << kdf_count)
1888 self._last_aes256_key = (kdf_count, salt, key)
1889 return key
1891 def _decrypt_header(self, fd):
1892 if not _have_crypto:
1893 raise NoCrypto("Cannot parse encrypted headers - no crypto")
1894 h = self._hdrenc_main
1895 key = self._gen_key(h.encryption_kdf_count, h.encryption_salt)
1896 iv = fd.read(16)
1897 return HeaderDecrypt(fd, key, iv)
1899 def _parse_block_header(self, fd):
1900 """Parse common block header
1901 """
1902 header_offset = fd.tell()
1904 preload = 4 + 1
1905 start_bytes = fd.read(preload)
1906 if len(start_bytes) < preload:
1907 self._set_error("Unexpected EOF when reading header")
1908 return None
1909 while start_bytes[-1] & 0x80:
1910 b = fd.read(1)
1911 if not b:
1912 self._set_error("Unexpected EOF when reading header")
1913 return None
1914 start_bytes += b
1915 header_crc, pos = load_le32(start_bytes, 0)
1916 hdrlen, pos = load_vint(start_bytes, pos)
1917 if hdrlen > 2 * 1024 * 1024:
1918 return None
1919 header_size = pos + hdrlen
1921 # read full header, check for EOF
1922 hdata = start_bytes + fd.read(header_size - len(start_bytes))
1923 if len(hdata) != header_size:
1924 self._set_error("Unexpected EOF when reading header")
1925 return None
1926 data_offset = fd.tell()
1928 calc_crc = crc32(memoryview(hdata)[4:])
1929 if header_crc != calc_crc:
1930 # header parsing failed.
1931 self._set_error("Header CRC error: exp=%x got=%x (xlen = %d)",
1932 header_crc, calc_crc, len(hdata))
1933 return None
1935 block_type, pos = load_vint(hdata, pos)
1937 if block_type == RAR5_BLOCK_MAIN:
1938 h, pos = self._parse_block_common(Rar5MainInfo(), hdata)
1939 h = self._parse_main_block(h, hdata, pos)
1940 elif block_type == RAR5_BLOCK_FILE:
1941 h, pos = self._parse_block_common(Rar5FileInfo(), hdata)
1942 h = self._parse_file_block(h, hdata, pos)
1943 elif block_type == RAR5_BLOCK_SERVICE:
1944 h, pos = self._parse_block_common(Rar5ServiceInfo(), hdata)
1945 h = self._parse_file_block(h, hdata, pos)
1946 elif block_type == RAR5_BLOCK_ENCRYPTION:
1947 h, pos = self._parse_block_common(Rar5EncryptionInfo(), hdata)
1948 h = self._parse_encryption_block(h, hdata, pos)
1949 elif block_type == RAR5_BLOCK_ENDARC:
1950 h, pos = self._parse_block_common(Rar5EndArcInfo(), hdata)
1951 h = self._parse_endarc_block(h, hdata, pos)
1952 else:
1953 h = None
1954 if h:
1955 h.header_offset = header_offset
1956 h.data_offset = data_offset
1957 return h
1959 def _parse_block_common(self, h, hdata):
1960 h.header_crc, pos = load_le32(hdata, 0)
1961 hdrlen, pos = load_vint(hdata, pos)
1962 h.header_size = hdrlen + pos
1963 h.block_type, pos = load_vint(hdata, pos)
1964 h.block_flags, pos = load_vint(hdata, pos)
1966 if h.block_flags & RAR5_BLOCK_FLAG_EXTRA_DATA:
1967 h.block_extra_size, pos = load_vint(hdata, pos)
1968 if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA:
1969 h.add_size, pos = load_vint(hdata, pos)
1971 h.compress_size = h.add_size
1973 if h.block_flags & RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN:
1974 h.flags |= RAR_SKIP_IF_UNKNOWN
1975 if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA:
1976 h.flags |= RAR_LONG_BLOCK
1977 return h, pos
1979 def _parse_main_block(self, h, hdata, pos):
1980 h.main_flags, pos = load_vint(hdata, pos)
1981 if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR:
1982 h.main_volume_number, pos = load_vint(hdata, pos)
1984 h.flags |= RAR_MAIN_NEWNUMBERING
1985 if h.main_flags & RAR5_MAIN_FLAG_SOLID:
1986 h.flags |= RAR_MAIN_SOLID
1987 if h.main_flags & RAR5_MAIN_FLAG_ISVOL:
1988 h.flags |= RAR_MAIN_VOLUME
1989 if h.main_flags & RAR5_MAIN_FLAG_RECOVERY:
1990 h.flags |= RAR_MAIN_RECOVERY
1991 if self._hdrenc_main:
1992 h.flags |= RAR_MAIN_PASSWORD
1993 if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR == 0:
1994 h.flags |= RAR_MAIN_FIRSTVOLUME
1996 return h
1998 def _parse_file_block(self, h, hdata, pos):
1999 h.file_flags, pos = load_vint(hdata, pos)
2000 h.file_size, pos = load_vint(hdata, pos)
2001 h.mode, pos = load_vint(hdata, pos)
2003 if h.file_flags & RAR5_FILE_FLAG_HAS_MTIME:
2004 h.mtime, pos = load_unixtime(hdata, pos)
2005 h.date_time = h.mtime.timetuple()[:6]
2006 if h.file_flags & RAR5_FILE_FLAG_HAS_CRC32:
2007 h.CRC, pos = load_le32(hdata, pos)
2008 h._md_class = CRC32Context
2009 h._md_expect = h.CRC
2011 h.file_compress_flags, pos = load_vint(hdata, pos)
2012 h.file_host_os, pos = load_vint(hdata, pos)
2013 h.orig_filename, pos = load_vstr(hdata, pos)
2014 h.filename = h.orig_filename.decode("utf8", "replace").rstrip("/")
2016 # use compatible values
2017 if h.file_host_os == RAR5_OS_WINDOWS:
2018 h.host_os = RAR_OS_WIN32
2019 else:
2020 h.host_os = RAR_OS_UNIX
2021 h.compress_type = RAR_M0 + ((h.file_compress_flags >> 7) & 7)
2023 if h.block_extra_size:
2024 # allow 1 byte of garbage
2025 while pos < len(hdata) - 1:
2026 xsize, pos = load_vint(hdata, pos)
2027 xdata, pos = load_bytes(hdata, xsize, pos)
2028 self._process_file_extra(h, xdata)
2030 if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE:
2031 h.flags |= RAR_FILE_SPLIT_BEFORE
2032 if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_AFTER:
2033 h.flags |= RAR_FILE_SPLIT_AFTER
2034 if h.file_flags & RAR5_FILE_FLAG_ISDIR:
2035 h.flags |= RAR_FILE_DIRECTORY
2036 if h.file_compress_flags & RAR5_COMPR_SOLID:
2037 h.flags |= RAR_FILE_SOLID
2039 if h.is_dir():
2040 h.filename = h.filename + "/"
2041 return h
2043 def _parse_endarc_block(self, h, hdata, pos):
2044 h.endarc_flags, pos = load_vint(hdata, pos)
2045 if h.endarc_flags & RAR5_ENDARC_FLAG_NEXT_VOL:
2046 h.flags |= RAR_ENDARC_NEXT_VOLUME
2047 return h
2049 def _check_password(self, check_value, kdf_count_shift, salt):
2050 if len(check_value) != RAR5_PW_CHECK_SIZE + RAR5_PW_SUM_SIZE:
2051 return
2052 if kdf_count_shift > RAR_MAX_KDF_SHIFT:
2053 raise BadRarFile("Too large kdf_count")
2055 hdr_check = check_value[:RAR5_PW_CHECK_SIZE]
2056 hdr_sum = check_value[RAR5_PW_CHECK_SIZE:]
2057 sum_hash = sha256(hdr_check).digest()
2058 if sum_hash[:RAR5_PW_SUM_SIZE] != hdr_sum:
2059 return
2061 kdf_count = (1 << kdf_count_shift) + 32
2062 pwd = self._get_utf8_password()
2063 pwd_hash = rar5_s2k(pwd, salt, kdf_count)
2065 pwd_check = bytearray(RAR5_PW_CHECK_SIZE)
2066 len_mask = RAR5_PW_CHECK_SIZE - 1
2067 for i, v in enumerate(pwd_hash):
2068 pwd_check[i & len_mask] ^= v
2070 if pwd_check != hdr_check:
2071 raise RarWrongPassword()
2073 def _parse_encryption_block(self, h, hdata, pos):
2074 self._hdrenc_main = h
2075 self._needs_password = True
2076 h.encryption_algo, pos = load_vint(hdata, pos)
2077 h.encryption_flags, pos = load_vint(hdata, pos)
2078 h.encryption_kdf_count, pos = load_byte(hdata, pos)
2079 h.encryption_salt, pos = load_bytes(hdata, 16, pos)
2080 if h.encryption_flags & RAR5_ENC_FLAG_HAS_CHECKVAL:
2081 h.encryption_check_value, pos = load_bytes(hdata, 12, pos)
2082 if h.encryption_algo != RAR5_XENC_CIPHER_AES256:
2083 raise BadRarFile("Unsupported header encryption cipher")
2084 if h.encryption_check_value and self._password:
2085 self._check_password(h.encryption_check_value, h.encryption_kdf_count, h.encryption_salt)
2086 return h
2088 def _process_file_extra(self, h, xdata):
2089 xtype, pos = load_vint(xdata, 0)
2090 if xtype == RAR5_XFILE_TIME:
2091 self._parse_file_xtime(h, xdata, pos)
2092 elif xtype == RAR5_XFILE_ENCRYPTION:
2093 self._parse_file_encryption(h, xdata, pos)
2094 elif xtype == RAR5_XFILE_HASH:
2095 self._parse_file_hash(h, xdata, pos)
2096 elif xtype == RAR5_XFILE_VERSION:
2097 self._parse_file_version(h, xdata, pos)
2098 elif xtype == RAR5_XFILE_REDIR:
2099 self._parse_file_redir(h, xdata, pos)
2100 elif xtype == RAR5_XFILE_OWNER:
2101 self._parse_file_owner(h, xdata, pos)
2102 elif xtype == RAR5_XFILE_SERVICE:
2103 pass
2104 else:
2105 pass
2107 # extra block for file time record
2108 def _parse_file_xtime(self, h, xdata, pos):
2109 tflags, pos = load_vint(xdata, pos)
2111 ldr = load_windowstime
2112 if tflags & RAR5_XTIME_UNIXTIME:
2113 ldr = load_unixtime
2115 if tflags & RAR5_XTIME_HAS_MTIME:
2116 h.mtime, pos = ldr(xdata, pos)
2117 h.date_time = h.mtime.timetuple()[:6]
2118 if tflags & RAR5_XTIME_HAS_CTIME:
2119 h.ctime, pos = ldr(xdata, pos)
2120 if tflags & RAR5_XTIME_HAS_ATIME:
2121 h.atime, pos = ldr(xdata, pos)
2123 if tflags & RAR5_XTIME_UNIXTIME_NS:
2124 if tflags & RAR5_XTIME_HAS_MTIME:
2125 nsec, pos = load_le32(xdata, pos)
2126 h.mtime = to_nsdatetime(h.mtime, nsec)
2127 if tflags & RAR5_XTIME_HAS_CTIME:
2128 nsec, pos = load_le32(xdata, pos)
2129 h.ctime = to_nsdatetime(h.ctime, nsec)
2130 if tflags & RAR5_XTIME_HAS_ATIME:
2131 nsec, pos = load_le32(xdata, pos)
2132 h.atime = to_nsdatetime(h.atime, nsec)
2134 # just remember encryption info
2135 def _parse_file_encryption(self, h, xdata, pos):
2136 algo, pos = load_vint(xdata, pos)
2137 flags, pos = load_vint(xdata, pos)
2138 kdf_count, pos = load_byte(xdata, pos)
2139 salt, pos = load_bytes(xdata, 16, pos)
2140 iv, pos = load_bytes(xdata, 16, pos)
2141 checkval = None
2142 if flags & RAR5_XENC_CHECKVAL:
2143 checkval, pos = load_bytes(xdata, 12, pos)
2144 if flags & RAR5_XENC_TWEAKED:
2145 h._md_expect = None
2146 h._md_class = NoHashContext
2148 h.file_encryption = (algo, flags, kdf_count, salt, iv, checkval)
2149 h.flags |= RAR_FILE_PASSWORD
2151 def _parse_file_hash(self, h, xdata, pos):
2152 hash_type, pos = load_vint(xdata, pos)
2153 if hash_type == RAR5_XHASH_BLAKE2SP:
2154 h.blake2sp_hash, pos = load_bytes(xdata, 32, pos)
2155 if (h.file_encryption[1] & RAR5_XENC_TWEAKED) == 0:
2156 h._md_class = Blake2SP
2157 h._md_expect = h.blake2sp_hash
2159 def _parse_file_version(self, h, xdata, pos):
2160 flags, pos = load_vint(xdata, pos)
2161 version, pos = load_vint(xdata, pos)
2162 h.file_version = (flags, version)
2164 def _parse_file_redir(self, h, xdata, pos):
2165 redir_type, pos = load_vint(xdata, pos)
2166 redir_flags, pos = load_vint(xdata, pos)
2167 redir_name, pos = load_vstr(xdata, pos)
2168 redir_name = redir_name.decode("utf8", "replace")
2169 h.file_redir = (redir_type, redir_flags, redir_name)
2171 def _parse_file_owner(self, h, xdata, pos):
2172 user_name = group_name = user_id = group_id = None
2174 flags, pos = load_vint(xdata, pos)
2175 if flags & RAR5_XOWNER_UNAME:
2176 user_name, pos = load_vstr(xdata, pos)
2177 if flags & RAR5_XOWNER_GNAME:
2178 group_name, pos = load_vstr(xdata, pos)
2179 if flags & RAR5_XOWNER_UID:
2180 user_id, pos = load_vint(xdata, pos)
2181 if flags & RAR5_XOWNER_GID:
2182 group_id, pos = load_vint(xdata, pos)
2184 h.file_owner = (user_name, group_name, user_id, group_id)
2186 def process_entry(self, fd, item):
2187 if item.block_type == RAR5_BLOCK_FILE:
2188 if item.file_version:
2189 pass # skip old versions
2190 elif (item.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE) == 0:
2191 # use only first part
2192 self._info_map[item.filename.rstrip("/")] = item
2193 self._info_list.append(item)
2194 elif len(self._info_list) > 0:
2195 # final crc is in last block
2196 old = self._info_list[-1]
2197 old.CRC = item.CRC
2198 old._md_expect = item._md_expect
2199 old.blake2sp_hash = item.blake2sp_hash
2200 old.compress_size += item.compress_size
2201 elif item.block_type == RAR5_BLOCK_SERVICE:
2202 if item.filename == "CMT":
2203 self._load_comment(fd, item)
2205 def _load_comment(self, fd, item):
2206 if item.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER):
2207 return None
2208 if item.compress_type != RAR_M0:
2209 return None
2211 if item.flags & RAR_FILE_PASSWORD:
2212 algo, ___flags, kdf_count, salt, iv, ___checkval = item.file_encryption
2213 if algo != RAR5_XENC_CIPHER_AES256:
2214 return None
2215 key = self._gen_key(kdf_count, salt)
2216 f = HeaderDecrypt(fd, key, iv)
2217 cmt = f.read(item.file_size)
2218 else:
2219 # archive comment
2220 with self._open_clear(item) as cmtstream:
2221 cmt = cmtstream.read()
2223 # rar bug? - appends zero to comment
2224 cmt = cmt.split(b"\0", 1)[0]
2225 self.comment = cmt.decode("utf8")
2226 return None
2228 def _open_hack(self, inf, pwd):
2229 # len, type, blk_flags, flags
2230 main_hdr = b"\x03\x01\x00\x00"
2231 endarc_hdr = b"\x03\x05\x00\x00"
2232 main_hdr = S_LONG.pack(crc32(main_hdr)) + main_hdr
2233 endarc_hdr = S_LONG.pack(crc32(endarc_hdr)) + endarc_hdr
2234 return self._open_hack_core(inf, pwd, RAR5_ID + main_hdr, endarc_hdr)
2237##
2238## Utility classes
2239##
2241class UnicodeFilename:
2242 """Handle RAR3 unicode filename decompression.
2243 """
2244 def __init__(self, name, encdata):
2245 self.std_name = bytearray(name)
2246 self.encdata = bytearray(encdata)
2247 self.pos = self.encpos = 0
2248 self.buf = bytearray()
2249 self.failed = 0
2251 def enc_byte(self):
2252 """Copy encoded byte."""
2253 try:
2254 c = self.encdata[self.encpos]
2255 self.encpos += 1
2256 return c
2257 except IndexError:
2258 self.failed = 1
2259 return 0
2261 def std_byte(self):
2262 """Copy byte from 8-bit representation."""
2263 try:
2264 return self.std_name[self.pos]
2265 except IndexError:
2266 self.failed = 1
2267 return ord("?")
2269 def put(self, lo, hi):
2270 """Copy 16-bit value to result."""
2271 self.buf.append(lo)
2272 self.buf.append(hi)
2273 self.pos += 1
2275 def decode(self):
2276 """Decompress compressed UTF16 value."""
2277 hi = self.enc_byte()
2278 flagbits = 0
2279 while self.encpos < len(self.encdata):
2280 if flagbits == 0:
2281 flags = self.enc_byte()
2282 flagbits = 8
2283 flagbits -= 2
2284 t = (flags >> flagbits) & 3
2285 if t == 0:
2286 self.put(self.enc_byte(), 0)
2287 elif t == 1:
2288 self.put(self.enc_byte(), hi)
2289 elif t == 2:
2290 self.put(self.enc_byte(), self.enc_byte())
2291 else:
2292 n = self.enc_byte()
2293 if n & 0x80:
2294 c = self.enc_byte()
2295 for _ in range((n & 0x7f) + 2):
2296 lo = (self.std_byte() + c) & 0xFF
2297 self.put(lo, hi)
2298 else:
2299 for _ in range(n + 2):
2300 self.put(self.std_byte(), 0)
2301 return self.buf.decode("utf-16le", "replace")
2304class RarExtFile(io.RawIOBase):
2305 """Base class for file-like object that :meth:`RarFile.open` returns.
2307 Provides public methods and common crc checking.
2309 Behaviour:
2310 - no short reads - .read() and .readinfo() read as much as requested.
2311 - no internal buffer, use io.BufferedReader for that.
2312 """
2313 name = None #: Filename of the archive entry
2314 mode = "rb"
2315 _parser = None
2316 _inf = None
2317 _fd = None
2318 _remain = 0
2319 _returncode = 0
2320 _md_context = None
2321 _seeking = False
2323 def _open_extfile(self, parser, inf):
2324 self.name = inf.filename
2325 self._parser = parser
2326 self._inf = inf
2328 if self._fd:
2329 self._fd.close()
2330 if self._seeking:
2331 md_class = NoHashContext
2332 else:
2333 md_class = self._inf._md_class or NoHashContext
2334 self._md_context = md_class()
2335 self._fd = None
2336 self._remain = self._inf.file_size
2338 def read(self, n=-1):
2339 """Read all or specified amount of data from archive entry."""
2341 # sanitize count
2342 if n is None or n < 0:
2343 n = self._remain
2344 elif n > self._remain:
2345 n = self._remain
2346 if n == 0:
2347 return b""
2349 buf = []
2350 orig = n
2351 while n > 0:
2352 # actual read
2353 data = self._read(n)
2354 if not data:
2355 break
2356 buf.append(data)
2357 self._md_context.update(data)
2358 self._remain -= len(data)
2359 n -= len(data)
2360 data = b"".join(buf)
2361 if n > 0:
2362 if self._returncode:
2363 check_returncode(self._returncode, "", tool_setup().get_errmap())
2364 raise BadRarFile("Failed the read enough data: req=%d got=%d" % (orig, len(data)))
2366 # done?
2367 if not data or self._remain == 0:
2368 # self.close()
2369 self._check()
2370 return data
2372 def _check(self):
2373 """Check final CRC."""
2374 final = self._md_context.digest()
2375 exp = self._inf._md_expect
2376 if exp is None:
2377 return
2378 if final is None:
2379 return
2380 if self._returncode:
2381 check_returncode(self._returncode, "", tool_setup().get_errmap())
2382 if self._remain != 0:
2383 raise BadRarFile("Failed the read enough data")
2384 if final != exp:
2385 raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % (
2386 self._inf.filename, exp, final))
2388 def _read(self, cnt):
2389 """Actual read that gets sanitized cnt."""
2390 raise NotImplementedError("_read")
2392 def close(self):
2393 """Close open resources."""
2395 super().close()
2397 if self._fd:
2398 self._fd.close()
2399 self._fd = None
2401 def __del__(self):
2402 """Hook delete to make sure tempfile is removed."""
2403 self.close()
2405 def readinto(self, buf):
2406 """Zero-copy read directly into buffer.
2408 Returns bytes read.
2409 """
2410 raise NotImplementedError("readinto")
2412 def tell(self):
2413 """Return current reading position in uncompressed data."""
2414 return self._inf.file_size - self._remain
2416 def seek(self, offset, whence=0):
2417 """Seek in data.
2419 On uncompressed files, the seeking works by actual
2420 seeks so it's fast. On compressed files its slow
2421 - forward seeking happens by reading ahead,
2422 backwards by re-opening and decompressing from the start.
2423 """
2425 # disable crc check when seeking
2426 if not self._seeking:
2427 self._md_context = NoHashContext()
2428 self._seeking = True
2430 fsize = self._inf.file_size
2431 cur_ofs = self.tell()
2433 if whence == 0: # seek from beginning of file
2434 new_ofs = offset
2435 elif whence == 1: # seek from current position
2436 new_ofs = cur_ofs + offset
2437 elif whence == 2: # seek from end of file
2438 new_ofs = fsize + offset
2439 else:
2440 raise ValueError("Invalid value for whence")
2442 # sanity check
2443 if new_ofs < 0:
2444 new_ofs = 0
2445 elif new_ofs > fsize:
2446 new_ofs = fsize
2448 # do the actual seek
2449 if new_ofs >= cur_ofs:
2450 self._skip(new_ofs - cur_ofs)
2451 else:
2452 # reopen and seek
2453 self._open_extfile(self._parser, self._inf)
2454 self._skip(new_ofs)
2455 return self.tell()
2457 def _skip(self, cnt):
2458 """Read and discard data"""
2459 empty_read(self, cnt, BSIZE)
2461 def readable(self):
2462 """Returns True"""
2463 return True
2465 def writable(self):
2466 """Returns False.
2468 Writing is not supported.
2469 """
2470 return False
2472 def seekable(self):
2473 """Returns True.
2475 Seeking is supported, although it's slow on compressed files.
2476 """
2477 return True
2479 def readall(self):
2480 """Read all remaining data"""
2481 # avoid RawIOBase default impl
2482 return self.read()
2485class PipeReader(RarExtFile):
2486 """Read data from pipe, handle tempfile cleanup."""
2488 def __init__(self, parser, inf, cmd, tempfile=None):
2489 super().__init__()
2490 self._cmd = cmd
2491 self._proc = None
2492 self._tempfile = tempfile
2493 self._open_extfile(parser, inf)
2495 def _close_proc(self):
2496 if not self._proc:
2497 return
2498 for f in (self._proc.stdout, self._proc.stderr, self._proc.stdin):
2499 if f:
2500 f.close()
2501 self._proc.wait()
2502 self._returncode = self._proc.returncode
2503 self._proc = None
2505 def _open_extfile(self, parser, inf):
2506 super()._open_extfile(parser, inf)
2508 # stop old process
2509 self._close_proc()
2511 # launch new process
2512 self._returncode = 0
2513 self._proc = custom_popen(self._cmd)
2514 self._fd = self._proc.stdout
2516 def _read(self, cnt):
2517 """Read from pipe."""
2519 # normal read is usually enough
2520 data = self._fd.read(cnt)
2521 if len(data) == cnt or not data:
2522 return data
2524 # short read, try looping
2525 buf = [data]
2526 cnt -= len(data)
2527 while cnt > 0:
2528 data = self._fd.read(cnt)
2529 if not data:
2530 break
2531 cnt -= len(data)
2532 buf.append(data)
2533 return b"".join(buf)
2535 def close(self):
2536 """Close open resources."""
2538 self._close_proc()
2539 super().close()
2541 if self._tempfile:
2542 try:
2543 os.unlink(self._tempfile)
2544 except OSError:
2545 pass
2546 self._tempfile = None
2548 def readinto(self, buf):
2549 """Zero-copy read directly into buffer."""
2550 cnt = len(buf)
2551 if cnt > self._remain:
2552 cnt = self._remain
2553 vbuf = memoryview(buf)
2554 res = got = 0
2555 while got < cnt:
2556 res = self._fd.readinto(vbuf[got: cnt])
2557 if not res:
2558 break
2559 self._md_context.update(vbuf[got: got + res])
2560 self._remain -= res
2561 got += res
2562 return got
2565class DirectReader(RarExtFile):
2566 """Read uncompressed data directly from archive.
2567 """
2568 _cur = None
2569 _cur_avail = None
2570 _volfile = None
2572 def __init__(self, parser, inf):
2573 super().__init__()
2574 self._open_extfile(parser, inf)
2576 def _open_extfile(self, parser, inf):
2577 super()._open_extfile(parser, inf)
2579 self._volfile = self._inf.volume_file
2580 self._fd = XFile(self._volfile, 0)
2581 self._fd.seek(self._inf.header_offset, 0)
2582 self._cur = self._parser._parse_header(self._fd)
2583 self._cur_avail = self._cur.add_size
2585 def _skip(self, cnt):
2586 """RAR Seek, skipping through rar files to get to correct position
2587 """
2589 while cnt > 0:
2590 # next vol needed?
2591 if self._cur_avail == 0:
2592 if not self._open_next():
2593 break
2595 # fd is in read pos, do the read
2596 if cnt > self._cur_avail:
2597 cnt -= self._cur_avail
2598 self._remain -= self._cur_avail
2599 self._cur_avail = 0
2600 else:
2601 self._fd.seek(cnt, 1)
2602 self._cur_avail -= cnt
2603 self._remain -= cnt
2604 cnt = 0
2606 def _read(self, cnt):
2607 """Read from potentially multi-volume archive."""
2609 pos = self._fd.tell()
2610 need = self._cur.data_offset + self._cur.add_size - self._cur_avail
2611 if pos != need:
2612 self._fd.seek(need, 0)
2614 buf = []
2615 while cnt > 0:
2616 # next vol needed?
2617 if self._cur_avail == 0:
2618 if not self._open_next():
2619 break
2621 # fd is in read pos, do the read
2622 if cnt > self._cur_avail:
2623 data = self._fd.read(self._cur_avail)
2624 else:
2625 data = self._fd.read(cnt)
2626 if not data:
2627 break
2629 # got some data
2630 cnt -= len(data)
2631 self._cur_avail -= len(data)
2632 buf.append(data)
2634 if len(buf) == 1:
2635 return buf[0]
2636 return b"".join(buf)
2638 def _open_next(self):
2639 """Proceed to next volume."""
2641 # is the file split over archives?
2642 if (self._cur.flags & RAR_FILE_SPLIT_AFTER) == 0:
2643 return False
2645 if self._fd:
2646 self._fd.close()
2647 self._fd = None
2649 # open next part
2650 self._volfile = self._parser._next_volname(self._volfile)
2651 fd = open(self._volfile, "rb", 0)
2652 self._fd = fd
2653 sig = fd.read(len(self._parser._expect_sig))
2654 if sig != self._parser._expect_sig:
2655 raise BadRarFile("Invalid signature")
2657 # loop until first file header
2658 while True:
2659 cur = self._parser._parse_header(fd)
2660 if not cur:
2661 raise BadRarFile("Unexpected EOF")
2662 if cur.type in (RAR_BLOCK_MARK, RAR_BLOCK_MAIN):
2663 if cur.add_size:
2664 fd.seek(cur.add_size, 1)
2665 continue
2666 if cur.orig_filename != self._inf.orig_filename:
2667 raise BadRarFile("Did not found file entry")
2668 self._cur = cur
2669 self._cur_avail = cur.add_size
2670 return True
2672 def readinto(self, buf):
2673 """Zero-copy read directly into buffer."""
2674 got = 0
2675 vbuf = memoryview(buf)
2676 while got < len(buf):
2677 # next vol needed?
2678 if self._cur_avail == 0:
2679 if not self._open_next():
2680 break
2682 # length for next read
2683 cnt = len(buf) - got
2684 if cnt > self._cur_avail:
2685 cnt = self._cur_avail
2687 # read into temp view
2688 res = self._fd.readinto(vbuf[got: got + cnt])
2689 if not res:
2690 break
2691 self._md_context.update(vbuf[got: got + res])
2692 self._cur_avail -= res
2693 self._remain -= res
2694 got += res
2695 return got
2698class HeaderDecrypt:
2699 """File-like object that decrypts from another file"""
2700 def __init__(self, f, key, iv):
2701 self.f = f
2702 self.ciph = AES_CBC_Decrypt(key, iv)
2703 self.buf = b""
2705 def tell(self):
2706 """Current file pos - works only on block boundaries."""
2707 return self.f.tell()
2709 def read(self, cnt=None):
2710 """Read and decrypt."""
2711 if cnt > 8 * 1024:
2712 raise BadRarFile("Bad count to header decrypt - wrong password?")
2714 # consume old data
2715 if cnt <= len(self.buf):
2716 res = self.buf[:cnt]
2717 self.buf = self.buf[cnt:]
2718 return res
2719 res = self.buf
2720 self.buf = b""
2721 cnt -= len(res)
2723 # decrypt new data
2724 blklen = 16
2725 while cnt > 0:
2726 enc = self.f.read(blklen)
2727 if len(enc) < blklen:
2728 break
2729 dec = self.ciph.decrypt(enc)
2730 if cnt >= len(dec):
2731 res += dec
2732 cnt -= len(dec)
2733 else:
2734 res += dec[:cnt]
2735 self.buf = dec[cnt:]
2736 cnt = 0
2738 return res
2741class XFile:
2742 """Input may be filename or file object.
2743 """
2744 __slots__ = ("_fd", "_need_close", "_initial_pos")
2746 def __init__(self, xfile, bufsize=1024):
2747 if is_filelike(xfile):
2748 self._initial_pos = xfile.tell()
2749 self._need_close = False
2750 self._fd = xfile
2751 self._fd.seek(0)
2752 else:
2753 self._initial_pos = None
2754 self._need_close = True
2755 self._fd = open(xfile, "rb", bufsize)
2757 def restore_pos(self):
2758 if self._initial_pos is None:
2759 return
2760 try:
2761 self._fd.seek(self._initial_pos)
2762 except:
2763 pass
2765 def read(self, n=None):
2766 """Read from file."""
2767 return self._fd.read(n)
2769 def tell(self):
2770 """Return file pos."""
2771 return self._fd.tell()
2773 def seek(self, ofs, whence=0):
2774 """Move file pos."""
2775 return self._fd.seek(ofs, whence)
2777 def readinto(self, buf):
2778 """Read into buffer."""
2779 return self._fd.readinto(buf)
2781 def close(self):
2782 """Close file object."""
2783 if self._need_close:
2784 self._fd.close()
2786 def __enter__(self):
2787 return self
2789 def __exit__(self, typ, val, tb):
2790 self.close()
2793class NoHashContext:
2794 """No-op hash function."""
2795 def __init__(self, data=None):
2796 """Initialize"""
2797 def update(self, data):
2798 """Update data"""
2799 def digest(self):
2800 """Final hash"""
2801 def hexdigest(self):
2802 """Hexadecimal digest."""
2805class CRC32Context:
2806 """Hash context that uses CRC32."""
2807 __slots__ = ["_crc"]
2809 def __init__(self, data=None):
2810 self._crc = 0
2811 if data:
2812 self.update(data)
2814 def update(self, data):
2815 """Process data."""
2816 self._crc = crc32(data, self._crc)
2818 def digest(self):
2819 """Final hash."""
2820 return self._crc
2822 def hexdigest(self):
2823 """Hexadecimal digest."""
2824 return "%08x" % self.digest()
2827class Blake2SP:
2828 """Blake2sp hash context.
2829 """
2830 __slots__ = ["_thread", "_buf", "_cur", "_digest"]
2831 digest_size = 32
2832 block_size = 64
2833 parallelism = 8
2835 def __init__(self, data=None):
2836 self._buf = b""
2837 self._cur = 0
2838 self._digest = None
2839 self._thread = []
2841 for i in range(self.parallelism):
2842 ctx = self._blake2s(i, 0, i == (self.parallelism - 1))
2843 self._thread.append(ctx)
2845 if data:
2846 self.update(data)
2848 def _blake2s(self, ofs, depth, is_last):
2849 return blake2s(node_offset=ofs, node_depth=depth, last_node=is_last,
2850 depth=2, inner_size=32, fanout=self.parallelism)
2852 def _add_block(self, blk):
2853 self._thread[self._cur].update(blk)
2854 self._cur = (self._cur + 1) % self.parallelism
2856 def update(self, data):
2857 """Hash data.
2858 """
2859 view = memoryview(data)
2860 bs = self.block_size
2861 if self._buf:
2862 need = bs - len(self._buf)
2863 if len(view) < need:
2864 self._buf += view.tobytes()
2865 return
2866 self._add_block(self._buf + view[:need].tobytes())
2867 view = view[need:]
2868 while len(view) >= bs:
2869 self._add_block(view[:bs])
2870 view = view[bs:]
2871 self._buf = view.tobytes()
2873 def digest(self):
2874 """Return final digest value.
2875 """
2876 if self._digest is None:
2877 if self._buf:
2878 self._add_block(self._buf)
2879 self._buf = b""
2880 ctx = self._blake2s(0, 1, True)
2881 for t in self._thread:
2882 ctx.update(t.digest())
2883 self._digest = ctx.digest()
2884 return self._digest
2886 def hexdigest(self):
2887 """Hexadecimal digest."""
2888 return hexlify(self.digest()).decode("ascii")
2891class Rar3Sha1:
2892 """Emulate buggy SHA1 from RAR3.
2893 """
2894 digest_size = 20
2895 block_size = 64
2897 _BLK_BE = struct.Struct(b">16L")
2898 _BLK_LE = struct.Struct(b"<16L")
2900 __slots__ = ("_nbytes", "_md", "_rarbug")
2902 def __init__(self, data=b"", rarbug=False):
2903 self._md = sha1()
2904 self._nbytes = 0
2905 self._rarbug = rarbug
2906 self.update(data)
2908 def update(self, data):
2909 """Process more data."""
2910 self._md.update(data)
2911 bufpos = self._nbytes & 63
2912 self._nbytes += len(data)
2914 if self._rarbug and len(data) > 64:
2915 dpos = self.block_size - bufpos
2916 while dpos + self.block_size <= len(data):
2917 self._corrupt(data, dpos)
2918 dpos += self.block_size
2920 def digest(self):
2921 """Return final state."""
2922 return self._md.digest()
2924 def hexdigest(self):
2925 """Return final state as hex string."""
2926 return self._md.hexdigest()
2928 def _corrupt(self, data, dpos):
2929 """Corruption from SHA1 core."""
2930 ws = list(self._BLK_BE.unpack_from(data, dpos))
2931 for t in range(16, 80):
2932 tmp = ws[(t - 3) & 15] ^ ws[(t - 8) & 15] ^ ws[(t - 14) & 15] ^ ws[(t - 16) & 15]
2933 ws[t & 15] = ((tmp << 1) | (tmp >> (32 - 1))) & 0xFFFFFFFF
2934 self._BLK_LE.pack_into(data, dpos, *ws)
2937##
2938## Utility functions
2939##
2941S_LONG = Struct("<L")
2942S_SHORT = Struct("<H")
2943S_BYTE = Struct("<B")
2945S_BLK_HDR = Struct("<HBHH")
2946S_FILE_HDR = Struct("<LLBLLBBHL")
2947S_COMMENT_HDR = Struct("<HBBH")
2948S_OLD_SUBBLOCK_HDR = Struct("<HB")
2950def load_vint(buf, pos):
2951 """Load RAR5 variable-size int."""
2952 limit = min(pos + 11, len(buf))
2953 res = ofs = 0
2954 while pos < limit:
2955 b = buf[pos]
2956 res += ((b & 0x7F) << ofs)
2957 pos += 1
2958 ofs += 7
2959 if b < 0x80:
2960 return res, pos
2961 raise BadRarFile("cannot load vint")
2964def load_byte(buf, pos):
2965 """Load single byte"""
2966 end = pos + 1
2967 if end > len(buf):
2968 raise BadRarFile("cannot load byte")
2969 return S_BYTE.unpack_from(buf, pos)[0], end
2972def load_le32(buf, pos):
2973 """Load little-endian 32-bit integer"""
2974 end = pos + 4
2975 if end > len(buf):
2976 raise BadRarFile("cannot load le32")
2977 return S_LONG.unpack_from(buf, pos)[0], end
2980def load_bytes(buf, num, pos):
2981 """Load sequence of bytes"""
2982 end = pos + num
2983 if end > len(buf):
2984 raise BadRarFile("cannot load bytes")
2985 return buf[pos: end], end
2988def load_vstr(buf, pos):
2989 """Load bytes prefixed by vint length"""
2990 slen, pos = load_vint(buf, pos)
2991 return load_bytes(buf, slen, pos)
2994def load_dostime(buf, pos):
2995 """Load LE32 dos timestamp"""
2996 stamp, pos = load_le32(buf, pos)
2997 tup = parse_dos_time(stamp)
2998 return to_datetime(tup), pos
3001def load_unixtime(buf, pos):
3002 """Load LE32 unix timestamp"""
3003 secs, pos = load_le32(buf, pos)
3004 dt = datetime.fromtimestamp(secs, timezone.utc)
3005 return dt, pos
3008def load_windowstime(buf, pos):
3009 """Load LE64 windows timestamp"""
3010 # unix epoch (1970) in seconds from windows epoch (1601)
3011 unix_epoch = 11644473600
3012 val1, pos = load_le32(buf, pos)
3013 val2, pos = load_le32(buf, pos)
3014 secs, n1secs = divmod((val2 << 32) | val1, 10000000)
3015 dt = datetime.fromtimestamp(secs - unix_epoch, timezone.utc)
3016 dt = to_nsdatetime(dt, n1secs * 100)
3017 return dt, pos
3020#
3021# volume numbering
3022#
3024_rc_num = re.compile('^[0-9]+$')
3027def _next_newvol(volfile):
3028 """New-style next volume
3029 """
3030 name, ext = os.path.splitext(volfile)
3031 if ext.lower() in ("", ".exe", ".sfx"):
3032 volfile = name + ".rar"
3033 i = len(volfile) - 1
3034 while i >= 0:
3035 if "0" <= volfile[i] <= "9":
3036 return _inc_volname(volfile, i, False)
3037 if volfile[i] in ("/", os.sep):
3038 break
3039 i -= 1
3040 raise BadRarName("Cannot construct volume name: " + volfile)
3044def _next_oldvol(volfile):
3045 """Old-style next volume
3046 """
3047 name, ext = os.path.splitext(volfile)
3048 if ext.lower() in ("", ".exe", ".sfx"):
3049 ext = ".rar"
3050 sfx = ext[2:]
3051 if _rc_num.match(sfx):
3052 ext = _inc_volname(ext, len(ext) - 1, True)
3053 else:
3054 # .rar -> .r00
3055 ext = ext[:2] + "00"
3056 return name + ext
3059def _inc_volname(volfile, i, inc_chars):
3060 """increase digits with carry, otherwise just increment char
3061 """
3062 fn = list(volfile)
3063 while i >= 0:
3064 if fn[i] == "9":
3065 fn[i] = "0"
3066 i -= 1
3067 if i < 0:
3068 fn.insert(0, "1")
3069 elif "0" <= fn[i] < "9" or inc_chars:
3070 fn[i] = chr(ord(fn[i]) + 1)
3071 break
3072 else:
3073 fn.insert(i + 1, "1")
3074 break
3075 return "".join(fn)
3078def _parse_ext_time(h, data, pos):
3079 """Parse all RAR3 extended time fields
3080 """
3081 # flags and rest of data can be missing
3082 flags = 0
3083 if pos + 2 <= len(data):
3084 flags = S_SHORT.unpack_from(data, pos)[0]
3085 pos += 2
3087 mtime, pos = _parse_xtime(flags >> 3 * 4, data, pos, h.mtime)
3088 h.ctime, pos = _parse_xtime(flags >> 2 * 4, data, pos)
3089 h.atime, pos = _parse_xtime(flags >> 1 * 4, data, pos)
3090 h.arctime, pos = _parse_xtime(flags >> 0 * 4, data, pos)
3091 if mtime:
3092 h.mtime = mtime
3093 h.date_time = mtime.timetuple()[:6]
3094 return pos
3097def _parse_xtime(flag, data, pos, basetime=None):
3098 """Parse one RAR3 extended time field
3099 """
3100 res = None
3101 if flag & 8:
3102 if not basetime:
3103 basetime, pos = load_dostime(data, pos)
3105 # load second fractions of 100ns units
3106 rem = 0
3107 cnt = flag & 3
3108 for _ in range(cnt):
3109 b, pos = load_byte(data, pos)
3110 rem = (b << 16) | (rem >> 8)
3112 # dostime has room for 30 seconds only, correct if needed
3113 if flag & 4 and basetime.second < 59:
3114 basetime = basetime.replace(second=basetime.second + 1)
3116 res = to_nsdatetime(basetime, rem * 100)
3117 return res, pos
3120def is_filelike(obj):
3121 """Filename or file object?
3122 """
3123 if isinstance(obj, (bytes, str, Path)):
3124 return False
3125 res = True
3126 for a in ("read", "tell", "seek"):
3127 res = res and hasattr(obj, a)
3128 if not res:
3129 raise ValueError("Invalid object passed as file")
3130 return True
3133def rar5_s2k(pwd, salt, kdf_count):
3134 """String-to-key hash for RAR5.
3135 """
3136 if not isinstance(pwd, str):
3137 pwd = pwd.decode("utf8")
3138 wstr = pwd.encode("utf-16le")[:RAR_MAX_PASSWORD*2]
3139 ustr = wstr.decode("utf-16le").encode("utf8")
3140 return pbkdf2_hmac("sha256", ustr, salt, kdf_count)
3143def rar3_s2k(pwd, salt):
3144 """String-to-key hash for RAR3.
3145 """
3146 if not isinstance(pwd, str):
3147 pwd = pwd.decode("utf8")
3148 wstr = pwd.encode("utf-16le")[:RAR_MAX_PASSWORD*2]
3149 seed = bytearray(wstr + salt)
3150 h = Rar3Sha1(rarbug=True)
3151 iv = b""
3152 for i in range(16):
3153 for j in range(0x4000):
3154 cnt = S_LONG.pack(i * 0x4000 + j)
3155 h.update(seed)
3156 h.update(cnt[:3])
3157 if j == 0:
3158 iv += h.digest()[19:20]
3159 key_be = h.digest()[:16]
3160 key_le = pack("<LLLL", *unpack(">LLLL", key_be))
3161 return key_le, iv
3164def rar3_decompress(vers, meth, data, declen=0, flags=0, crc=0, pwd=None, salt=None):
3165 """Decompress blob of compressed data.
3167 Used for data with non-standard header - eg. comments.
3168 """
3169 # already uncompressed?
3170 if meth == RAR_M0 and (flags & RAR_FILE_PASSWORD) == 0:
3171 return data
3173 # take only necessary flags
3174 flags = flags & (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK)
3175 flags |= RAR_LONG_BLOCK
3177 # file header
3178 fname = b"data"
3179 date = ((2010 - 1980) << 25) + (12 << 21) + (31 << 16)
3180 mode = DOS_MODE_ARCHIVE
3181 fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc,
3182 date, vers, meth, len(fname), mode)
3183 fhdr += fname
3184 if salt:
3185 fhdr += salt
3187 # full header
3188 hlen = S_BLK_HDR.size + len(fhdr)
3189 hdr = S_BLK_HDR.pack(0, RAR_BLOCK_FILE, flags, hlen) + fhdr
3190 hcrc = crc32(hdr[2:]) & 0xFFFF
3191 hdr = S_BLK_HDR.pack(hcrc, RAR_BLOCK_FILE, flags, hlen) + fhdr
3193 # archive main header
3194 mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + b"\0" * (2 + 4)
3196 # decompress via temp rar
3197 setup = tool_setup()
3198 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR)
3199 tmpf = os.fdopen(tmpfd, "wb")
3200 try:
3201 tmpf.write(RAR_ID + mh + hdr + data)
3202 tmpf.close()
3204 curpwd = (flags & RAR_FILE_PASSWORD) and pwd or None
3205 cmd = setup.open_cmdline(curpwd, tmpname)
3206 p = custom_popen(cmd)
3207 return p.communicate()[0]
3208 finally:
3209 tmpf.close()
3210 os.unlink(tmpname)
3213def sanitize_filename(fname, pathsep, is_win32):
3214 """Make filename safe for write access.
3215 """
3216 if is_win32:
3217 if len(fname) > 1 and fname[1] == ":":
3218 fname = fname[2:]
3219 rc = RC_BAD_CHARS_WIN32
3220 else:
3221 rc = RC_BAD_CHARS_UNIX
3222 if rc.search(fname):
3223 fname = rc.sub("_", fname)
3225 parts = []
3226 for seg in fname.split("/"):
3227 if seg in ("", ".", ".."):
3228 continue
3229 if is_win32 and seg[-1] in (" ", "."):
3230 seg = seg[:-1] + "_"
3231 parts.append(seg)
3232 return pathsep.join(parts)
3235def empty_read(src, size, blklen):
3236 """Read and drop fixed amount of data.
3237 """
3238 while size > 0:
3239 if size > blklen:
3240 res = src.read(blklen)
3241 else:
3242 res = src.read(size)
3243 if not res:
3244 raise BadRarFile("cannot load data")
3245 size -= len(res)
3248def to_datetime(t):
3249 """Convert 6-part time tuple into datetime object.
3250 """
3251 # extract values
3252 year, mon, day, h, m, s = t
3254 # assume the values are valid
3255 try:
3256 return datetime(year, mon, day, h, m, s)
3257 except ValueError:
3258 pass
3260 # sanitize invalid values
3261 mday = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
3262 mon = max(1, min(mon, 12))
3263 day = max(1, min(day, mday[mon]))
3264 h = min(h, 23)
3265 m = min(m, 59)
3266 s = min(s, 59)
3267 return datetime(year, mon, day, h, m, s)
3270def parse_dos_time(stamp):
3271 """Parse standard 32-bit DOS timestamp.
3272 """
3273 sec, stamp = stamp & 0x1F, stamp >> 5
3274 mn, stamp = stamp & 0x3F, stamp >> 6
3275 hr, stamp = stamp & 0x1F, stamp >> 5
3276 day, stamp = stamp & 0x1F, stamp >> 5
3277 mon, stamp = stamp & 0x0F, stamp >> 4
3278 yr = (stamp & 0x7F) + 1980
3279 return (yr, mon, day, hr, mn, sec * 2)
3282# pylint: disable=arguments-differ,signature-differs
3283class nsdatetime(datetime):
3284 """Datetime that carries nanoseconds.
3286 Arithmetic operations will lose nanoseconds.
3288 .. versionadded:: 4.0
3289 """
3290 __slots__ = ("nanosecond",)
3291 nanosecond: int #: Number of nanoseconds, 0 <= nanosecond <= 999999999
3293 def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0,
3294 microsecond=0, tzinfo=None, *, fold=0, nanosecond=0):
3295 usec, mod = divmod(nanosecond, 1000) if nanosecond else (microsecond, 0)
3296 if mod == 0:
3297 return datetime(year, month, day, hour, minute, second, usec, tzinfo, fold=fold)
3298 self = super().__new__(cls, year, month, day, hour, minute, second, usec, tzinfo, fold=fold)
3299 self.nanosecond = nanosecond
3300 return self
3302 def isoformat(self, sep="T", timespec="auto"):
3303 """Formats with nanosecond precision by default.
3304 """
3305 if timespec == "auto":
3306 pre, post = super().isoformat(sep, "microseconds").split(".", 1)
3307 return f"{pre}.{self.nanosecond:09d}{post[6:]}"
3308 return super().isoformat(sep, timespec)
3310 def astimezone(self, tz=None):
3311 """Convert to new timezone.
3312 """
3313 tmp = super().astimezone(tz)
3314 return self.__class__(tmp.year, tmp.month, tmp.day, tmp.hour, tmp.minute, tmp.second,
3315 nanosecond=self.nanosecond, tzinfo=tmp.tzinfo, fold=tmp.fold)
3317 def replace(self, year=None, month=None, day=None, hour=None, minute=None, second=None,
3318 microsecond=None, tzinfo=None, *, fold=None, nanosecond=None):
3319 """Return new timestamp with specified fields replaced.
3320 """
3321 return self.__class__(
3322 self.year if year is None else year,
3323 self.month if month is None else month,
3324 self.day if day is None else day,
3325 self.hour if hour is None else hour,
3326 self.minute if minute is None else minute,
3327 self.second if second is None else second,
3328 nanosecond=((self.nanosecond if microsecond is None else microsecond * 1000)
3329 if nanosecond is None else nanosecond),
3330 tzinfo=self.tzinfo if tzinfo is None else tzinfo,
3331 fold=self.fold if fold is None else fold)
3333 def __hash__(self):
3334 return hash((super().__hash__(), self.nanosecond)) if self.nanosecond else super().__hash__()
3336 def __eq__(self, other):
3337 return super().__eq__(other) and self.nanosecond == (
3338 other.nanosecond if isinstance(other, nsdatetime) else other.microsecond * 1000)
3340 def __gt__(self, other):
3341 return super().__gt__(other) or (super().__eq__(other) and self.nanosecond > (
3342 other.nanosecond if isinstance(other, nsdatetime) else other.microsecond * 1000))
3344 def __lt__(self, other):
3345 return not (self > other or self == other)
3347 def __ge__(self, other):
3348 return not self < other
3350 def __le__(self, other):
3351 return not self > other
3353 def __ne__(self, other):
3354 return not self == other
3357def to_nsdatetime(dt, nsec):
3358 """Apply nanoseconds to datetime.
3359 """
3360 if not nsec:
3361 return dt
3362 return nsdatetime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
3363 tzinfo=dt.tzinfo, fold=dt.fold, nanosecond=nsec)
3366def to_nsecs(dt):
3367 """Convert datatime instance to nanoseconds.
3368 """
3369 secs = int(dt.timestamp())
3370 nsecs = dt.nanosecond if isinstance(dt, nsdatetime) else dt.microsecond * 1000
3371 return secs * 1000000000 + nsecs
3374def custom_popen(cmd):
3375 """Disconnect cmd from parent fds, read only from stdout.
3376 """
3377 creationflags = 0x08000000 if WIN32 else 0 # CREATE_NO_WINDOW
3378 try:
3379 p = Popen(cmd, bufsize=0, stdout=PIPE, stderr=STDOUT, stdin=DEVNULL,
3380 creationflags=creationflags)
3381 except OSError as ex:
3382 if ex.errno == errno.ENOENT:
3383 raise RarCannotExec("Unrar not installed?") from None
3384 if ex.errno == errno.EACCES or ex.errno == errno.EPERM:
3385 raise RarCannotExec("Cannot execute unrar") from None
3386 raise
3387 return p
3390def check_returncode(code, out, errmap):
3391 """Raise exception according to unrar exit code.
3392 """
3393 if code == 0:
3394 return
3396 if code > 0 and code < len(errmap):
3397 exc = errmap[code]
3398 elif code == 255:
3399 exc = RarUserBreak
3400 elif code < 0:
3401 exc = RarSignalExit
3402 else:
3403 exc = RarUnknownError
3405 # format message
3406 if out:
3407 msg = "%s [%d]: %s" % (exc.__doc__, code, out)
3408 else:
3409 msg = "%s [%d]" % (exc.__doc__, code)
3411 raise exc(msg)
3414def membuf_tempfile(memfile):
3415 """Write in-memory file object to real file.
3416 """
3417 memfile.seek(0, 0)
3419 tmpfd, tmpname = mkstemp(suffix=".rar", dir=HACK_TMP_DIR)
3420 tmpf = os.fdopen(tmpfd, "wb")
3422 try:
3423 shutil.copyfileobj(memfile, tmpf, BSIZE)
3424 tmpf.close()
3425 except BaseException:
3426 tmpf.close()
3427 os.unlink(tmpname)
3428 raise
3429 return tmpname
3432#
3433# Find working command-line tool
3434#
3436class ToolSetup:
3437 def __init__(self, setup):
3438 self.setup = setup
3440 def check(self):
3441 cmdline = self.get_cmdline("check_cmd", None)
3442 try:
3443 p = custom_popen(cmdline)
3444 out, _ = p.communicate()
3445 return p.returncode == 0
3446 except RarCannotExec:
3447 return False
3449 def open_cmdline(self, pwd, rarfn, filefn=None):
3450 cmdline = self.get_cmdline("open_cmd", pwd)
3451 cmdline.append(rarfn)
3452 if filefn:
3453 self.add_file_arg(cmdline, filefn)
3454 return cmdline
3456 def get_errmap(self):
3457 return self.setup["errmap"]
3459 def get_cmdline(self, key, pwd, nodash=False):
3460 cmdline = list(self.setup[key])
3461 cmdline[0] = globals()[cmdline[0]]
3462 if key == "check_cmd":
3463 return cmdline
3464 self.add_password_arg(cmdline, pwd)
3465 if not nodash:
3466 cmdline.append("--")
3467 return cmdline
3469 def add_file_arg(self, cmdline, filename):
3470 cmdline.append(filename)
3472 def add_password_arg(self, cmdline, pwd):
3473 """Append password switch to commandline.
3474 """
3475 if pwd is not None:
3476 if not isinstance(pwd, str):
3477 pwd = pwd.decode("utf8")
3478 args = self.setup["password"]
3479 if args is None:
3480 tool = self.setup["open_cmd"][0]
3481 raise RarCannotExec(f"{tool} does not support passwords")
3482 elif isinstance(args, str):
3483 cmdline.append(args + pwd)
3484 else:
3485 cmdline.extend(args)
3486 cmdline.append(pwd)
3487 else:
3488 cmdline.extend(self.setup["no_password"])
3491UNRAR_CONFIG = {
3492 "open_cmd": ("UNRAR_TOOL", "p", "-inul"),
3493 "check_cmd": ("UNRAR_TOOL", "-inul", "-?"),
3494 "password": "-p",
3495 "no_password": ("-p-",),
3496 # map return code to exception class, codes from rar.txt
3497 "errmap": [None,
3498 RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError, # 1..4
3499 RarWriteError, RarOpenError, RarUserError, RarMemoryError, # 5..8
3500 RarCreateError, RarNoFilesError, RarWrongPassword] # 9..11
3501}
3503# Problems with unar RAR backend:
3504# - Does not support RAR2 locked files [fails to read]
3505# - Does not support RAR5 Blake2sp hash [reading works]
3506UNAR_CONFIG = {
3507 "open_cmd": ("UNAR_TOOL", "-q", "-o", "-"),
3508 "check_cmd": ("UNAR_TOOL", "-version"),
3509 "password": ("-p",),
3510 "no_password": ("-p", ""),
3511 "errmap": [None],
3512}
3514# Problems with libarchive RAR backend:
3515# - Does not support solid archives.
3516# - Does not support password-protected archives.
3517# - Does not support RARVM-based compression filters.
3518BSDTAR_CONFIG = {
3519 "open_cmd": ("BSDTAR_TOOL", "-x", "--to-stdout", "-f"),
3520 "check_cmd": ("BSDTAR_TOOL", "--version"),
3521 "password": None,
3522 "no_password": (),
3523 "errmap": [None],
3524}
3526SEVENZIP_CONFIG = {
3527 "open_cmd": ("SEVENZIP_TOOL", "e", "-so", "-bb0"),
3528 "check_cmd": ("SEVENZIP_TOOL", "i"),
3529 "password": "-p",
3530 "no_password": ("-p",),
3531 "errmap": [None,
3532 RarWarning, RarFatalError, None, None, # 1..4
3533 None, None, RarUserError, RarMemoryError] # 5..8
3534}
3536SEVENZIP2_CONFIG = {
3537 "open_cmd": ("SEVENZIP2_TOOL", "e", "-so", "-bb0"),
3538 "check_cmd": ("SEVENZIP2_TOOL", "i"),
3539 "password": "-p",
3540 "no_password": ("-p",),
3541 "errmap": [None,
3542 RarWarning, RarFatalError, None, None, # 1..4
3543 None, None, RarUserError, RarMemoryError] # 5..8
3544}
3546CURRENT_SETUP = None
3549def tool_setup(unrar=True, unar=True, bsdtar=True, sevenzip=True, sevenzip2=True, force=False):
3550 """Pick a tool, return cached ToolSetup.
3551 """
3552 global CURRENT_SETUP
3553 if force:
3554 CURRENT_SETUP = None
3555 if CURRENT_SETUP is not None:
3556 return CURRENT_SETUP
3557 lst = []
3558 if unrar:
3559 lst.append(UNRAR_CONFIG)
3560 if unar:
3561 lst.append(UNAR_CONFIG)
3562 if sevenzip:
3563 lst.append(SEVENZIP_CONFIG)
3564 if sevenzip2:
3565 lst.append(SEVENZIP2_CONFIG)
3566 if bsdtar:
3567 lst.append(BSDTAR_CONFIG)
3569 for conf in lst:
3570 setup = ToolSetup(conf)
3571 if setup.check():
3572 CURRENT_SETUP = setup
3573 break
3574 if CURRENT_SETUP is None:
3575 raise RarCannotExec("Cannot find working tool")
3576 return CURRENT_SETUP
3579def main(args):
3580 """Minimal command-line interface for rarfile module.
3581 """
3582 import argparse
3583 p = argparse.ArgumentParser(description=main.__doc__)
3584 g = p.add_mutually_exclusive_group(required=True)
3585 g.add_argument("-l", "--list", metavar="<rarfile>",
3586 help="Show archive listing")
3587 g.add_argument("-e", "--extract", nargs=2,
3588 metavar=("<rarfile>", "<output_dir>"),
3589 help="Extract archive into target dir")
3590 g.add_argument("-t", "--test", metavar="<rarfile>",
3591 help="Test if a archive is valid")
3592 cmd = p.parse_args(args)
3594 if cmd.list:
3595 with RarFile(cmd.list) as rf:
3596 rf.printdir()
3597 elif cmd.test:
3598 with RarFile(cmd.test) as rf:
3599 rf.testrar()
3600 elif cmd.extract:
3601 with RarFile(cmd.extract[0]) as rf:
3602 rf.extractall(cmd.extract[1])
3605if __name__ == "__main__":
3606 main(sys.argv[1:])