3012 lines
		
	
	
		
			86 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			3012 lines
		
	
	
		
			86 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # rarfile.py
 | |
| #
 | |
| # Copyright (c) 2005-2016  Marko Kreen <markokr@gmail.com>
 | |
| #
 | |
| # Permission to use, copy, modify, and/or distribute this software for any
 | |
| # purpose with or without fee is hereby granted, provided that the above
 | |
| # copyright notice and this permission notice appear in all copies.
 | |
| #
 | |
| # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | |
| # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | |
| # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | |
| # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | |
| # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 | |
| # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | |
| # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | |
| 
 | |
| r"""RAR archive reader.
 | |
| 
 | |
| This is Python module for Rar archive reading.  The interface
 | |
| is made as :mod:`zipfile`-like as possible.
 | |
| 
 | |
| Basic logic:
 | |
|  - Parse archive structure with Python.
 | |
|  - Extract non-compressed files with Python
 | |
|  - Extract compressed files with unrar.
 | |
|  - Optionally write compressed data to temp file to speed up unrar,
 | |
|    otherwise it needs to scan whole archive on each execution.
 | |
| 
 | |
| Example::
 | |
| 
 | |
|     import rarfile
 | |
| 
 | |
|     rf = rarfile.RarFile('myarchive.rar')
 | |
|     for f in rf.infolist():
 | |
|         print f.filename, f.file_size
 | |
|         if f.filename == 'README':
 | |
|             print(rf.read(f))
 | |
| 
 | |
| Archive files can also be accessed via file-like object returned
 | |
| by :meth:`RarFile.open`::
 | |
| 
 | |
|     import rarfile
 | |
| 
 | |
|     with rarfile.RarFile('archive.rar') as rf:
 | |
|         with rf.open('README') as f:
 | |
|             for ln in f:
 | |
|                 print(ln.strip())
 | |
| 
 | |
| There are few module-level parameters to tune behaviour,
 | |
| here they are with defaults, and reason to change it::
 | |
| 
 | |
|     import rarfile
 | |
| 
 | |
|     # Set to full path of unrar.exe if it is not in PATH
 | |
|     rarfile.UNRAR_TOOL = "unrar"
 | |
| 
 | |
|     # Set to '\\' to be more compatible with old rarfile
 | |
|     rarfile.PATH_SEP = '/'
 | |
| 
 | |
| For more details, refer to source.
 | |
| 
 | |
| """
 | |
| 
 | |
| from __future__ import division, print_function
 | |
| 
 | |
| ##
 | |
| ## Imports and compat - support both Python 2.x and 3.x
 | |
| ##
 | |
| 
 | |
| import sys
 | |
| import os
 | |
| import errno
 | |
| import struct
 | |
| 
 | |
| from struct import pack, unpack, Struct
 | |
| from binascii import crc32, hexlify
 | |
| from tempfile import mkstemp
 | |
| from subprocess import Popen, PIPE, STDOUT
 | |
| from io import RawIOBase
 | |
| from hashlib import sha1, sha256
 | |
| from hmac import HMAC
 | |
| from datetime import datetime, timedelta, tzinfo
 | |
| 
 | |
| # fixed offset timezone, for UTC
 | |
| try:
 | |
|     from datetime import timezone
 | |
| except ImportError:
 | |
|     class timezone(tzinfo):
 | |
|         """Compat timezone."""
 | |
|         __slots__ = ('_ofs', '_name')
 | |
|         _DST = timedelta(0)
 | |
| 
 | |
|         def __init__(self, offset, name):
 | |
|             super(timezone, self).__init__()
 | |
|             self._ofs, self._name = offset, name
 | |
| 
 | |
|         def utcoffset(self, dt):
 | |
|             return self._ofs
 | |
| 
 | |
|         def tzname(self, dt):
 | |
|             return self._name
 | |
| 
 | |
|         def dst(self, dt):
 | |
|             return self._DST
 | |
| 
 | |
| # only needed for encryped headers
 | |
| try:
 | |
|     try:
 | |
|         from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher
 | |
|         from cryptography.hazmat.backends import default_backend
 | |
|         from cryptography.hazmat.primitives import hashes
 | |
|         from cryptography.hazmat.primitives.kdf import pbkdf2
 | |
| 
 | |
|         class AES_CBC_Decrypt(object):
 | |
|             """Decrypt API"""
 | |
|             def __init__(self, key, iv):
 | |
|                 ciph = Cipher(algorithms.AES(key), modes.CBC(iv), default_backend())
 | |
|                 self.decrypt = ciph.decryptor().update
 | |
| 
 | |
|         def pbkdf2_sha256(password, salt, iters):
 | |
|             """PBKDF2 with HMAC-SHA256"""
 | |
|             ctx = pbkdf2.PBKDF2HMAC(hashes.SHA256(), 32, salt, iters, default_backend())
 | |
|             return ctx.derive(password)
 | |
| 
 | |
|     except ImportError:
 | |
|         from Crypto.Cipher import AES
 | |
|         from Crypto.Protocol import KDF
 | |
| 
 | |
|         class AES_CBC_Decrypt(object):
 | |
|             """Decrypt API"""
 | |
|             def __init__(self, key, iv):
 | |
|                 self.decrypt = AES.new(key, AES.MODE_CBC, iv).decrypt
 | |
| 
 | |
|         def pbkdf2_sha256(password, salt, iters):
 | |
|             """PBKDF2 with HMAC-SHA256"""
 | |
|             return KDF.PBKDF2(password, salt, 32, iters, hmac_sha256)
 | |
| 
 | |
|     _have_crypto = 1
 | |
| except ImportError:
 | |
|     _have_crypto = 0
 | |
| 
 | |
| try:
 | |
|     try:
 | |
|         from hashlib import blake2s
 | |
|         _have_blake2 = True
 | |
|     except ImportError:
 | |
|         from pyblake2 import blake2s
 | |
|         _have_blake2 = True
 | |
| except ImportError:
 | |
|     _have_blake2 = False
 | |
| 
 | |
| # compat with 2.x
 | |
| if sys.hexversion < 0x3000000:
 | |
|     def rar_crc32(data, prev=0):
 | |
|         """CRC32 with unsigned values.
 | |
|         """
 | |
|         if (prev > 0) and (prev & 0x80000000):
 | |
|             prev -= (1 << 32)
 | |
|         res = crc32(data, prev)
 | |
|         if res < 0:
 | |
|             res += (1 << 32)
 | |
|         return res
 | |
|     tohex = hexlify
 | |
|     _byte_code = ord
 | |
| else:  # pragma: no cover
 | |
|     def tohex(data):
 | |
|         """Return hex string."""
 | |
|         return hexlify(data).decode('ascii')
 | |
|     rar_crc32 = crc32
 | |
|     unicode = str
 | |
|     _byte_code = int   # noqa
 | |
| 
 | |
| # don't break 2.6 completely
 | |
| if sys.hexversion < 0x2070000:
 | |
|     memoryview = lambda x: x  # noqa
 | |
| 
 | |
| __version__ = '3.0'
 | |
| 
 | |
| # export only interesting items
 | |
| __all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile']
 | |
| 
 | |
| ##
 | |
| ## Module configuration.  Can be tuned after importing.
 | |
| ##
 | |
| 
 | |
| #: default fallback charset
 | |
| DEFAULT_CHARSET = "windows-1252"
 | |
| 
 | |
| #: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
 | |
| TRY_ENCODINGS = ('utf8', 'utf-16le')
 | |
| 
 | |
| #: 'unrar', 'rar' or full path to either one
 | |
| UNRAR_TOOL = "unrar"
 | |
| 
 | |
| #: Command line args to use for opening file for reading.
 | |
| OPEN_ARGS = ('p', '-inul')
 | |
| 
 | |
| #: Command line args to use for extracting file to disk.
 | |
| EXTRACT_ARGS = ('x', '-y', '-idq')
 | |
| 
 | |
| #: args for testrar()
 | |
| TEST_ARGS = ('t', '-idq')
 | |
| 
 | |
| #
 | |
| # Allow use of tool that is not compatible with unrar.
 | |
| #
 | |
| # By default use 'bsdtar' which is 'tar' program that
 | |
| # sits on top of libarchive.
 | |
| #
 | |
| # Problems with libarchive RAR backend:
 | |
| # - Does not support solid archives.
 | |
| # - Does not support password-protected archives.
 | |
| #
 | |
| 
 | |
| ALT_TOOL = 'bsdtar'
 | |
| ALT_OPEN_ARGS = ('-x', '--to-stdout', '-f')
 | |
| ALT_EXTRACT_ARGS = ('-x', '-f')
 | |
| ALT_TEST_ARGS = ('-t', '-f')
 | |
| ALT_CHECK_ARGS = ('--help',)
 | |
| 
 | |
| #ALT_TOOL = 'unar'
 | |
| #ALT_OPEN_ARGS = ('-o', '-')
 | |
| #ALT_EXTRACT_ARGS = ()
 | |
| #ALT_TEST_ARGS = ('-test',) # does not work
 | |
| #ALT_CHECK_ARGS = ('-v',)
 | |
| 
 | |
| #: whether to speed up decompression by using tmp archive
 | |
| USE_EXTRACT_HACK = 1
 | |
| 
 | |
| #: limit the filesize for tmp archive usage
 | |
| HACK_SIZE_LIMIT = 20 * 1024 * 1024
 | |
| 
 | |
| #: Separator for path name components.  RAR internally uses '\\'.
 | |
| #: Use '/' to be similar with zipfile.
 | |
| PATH_SEP = '/'
 | |
| 
 | |
| ##
 | |
| ## rar constants
 | |
| ##
 | |
| 
 | |
| # block types
 | |
| RAR_BLOCK_MARK          = 0x72  # r
 | |
| RAR_BLOCK_MAIN          = 0x73  # s
 | |
| RAR_BLOCK_FILE          = 0x74  # t
 | |
| RAR_BLOCK_OLD_COMMENT   = 0x75  # u
 | |
| RAR_BLOCK_OLD_EXTRA     = 0x76  # v
 | |
| RAR_BLOCK_OLD_SUB       = 0x77  # w
 | |
| RAR_BLOCK_OLD_RECOVERY  = 0x78  # x
 | |
| RAR_BLOCK_OLD_AUTH      = 0x79  # y
 | |
| RAR_BLOCK_SUB           = 0x7a  # z
 | |
| RAR_BLOCK_ENDARC        = 0x7b  # {
 | |
| 
 | |
| # flags for RAR_BLOCK_MAIN
 | |
| RAR_MAIN_VOLUME         = 0x0001
 | |
| RAR_MAIN_COMMENT        = 0x0002
 | |
| RAR_MAIN_LOCK           = 0x0004
 | |
| RAR_MAIN_SOLID          = 0x0008
 | |
| RAR_MAIN_NEWNUMBERING   = 0x0010
 | |
| RAR_MAIN_AUTH           = 0x0020
 | |
| RAR_MAIN_RECOVERY       = 0x0040
 | |
| RAR_MAIN_PASSWORD       = 0x0080
 | |
| RAR_MAIN_FIRSTVOLUME    = 0x0100
 | |
| RAR_MAIN_ENCRYPTVER     = 0x0200
 | |
| 
 | |
| # flags for RAR_BLOCK_FILE
 | |
| RAR_FILE_SPLIT_BEFORE   = 0x0001
 | |
| RAR_FILE_SPLIT_AFTER    = 0x0002
 | |
| RAR_FILE_PASSWORD       = 0x0004
 | |
| RAR_FILE_COMMENT        = 0x0008
 | |
| RAR_FILE_SOLID          = 0x0010
 | |
| RAR_FILE_DICTMASK       = 0x00e0
 | |
| RAR_FILE_DICT64         = 0x0000
 | |
| RAR_FILE_DICT128        = 0x0020
 | |
| RAR_FILE_DICT256        = 0x0040
 | |
| RAR_FILE_DICT512        = 0x0060
 | |
| RAR_FILE_DICT1024       = 0x0080
 | |
| RAR_FILE_DICT2048       = 0x00a0
 | |
| RAR_FILE_DICT4096       = 0x00c0
 | |
| RAR_FILE_DIRECTORY      = 0x00e0
 | |
| RAR_FILE_LARGE          = 0x0100
 | |
| RAR_FILE_UNICODE        = 0x0200
 | |
| RAR_FILE_SALT           = 0x0400
 | |
| RAR_FILE_VERSION        = 0x0800
 | |
| RAR_FILE_EXTTIME        = 0x1000
 | |
| RAR_FILE_EXTFLAGS       = 0x2000
 | |
| 
 | |
| # flags for RAR_BLOCK_ENDARC
 | |
| RAR_ENDARC_NEXT_VOLUME  = 0x0001
 | |
| RAR_ENDARC_DATACRC      = 0x0002
 | |
| RAR_ENDARC_REVSPACE     = 0x0004
 | |
| RAR_ENDARC_VOLNR        = 0x0008
 | |
| 
 | |
| # flags common to all blocks
 | |
| RAR_SKIP_IF_UNKNOWN     = 0x4000
 | |
| RAR_LONG_BLOCK          = 0x8000
 | |
| 
 | |
| # Host OS types
 | |
| RAR_OS_MSDOS = 0
 | |
| RAR_OS_OS2   = 1
 | |
| RAR_OS_WIN32 = 2
 | |
| RAR_OS_UNIX  = 3
 | |
| RAR_OS_MACOS = 4
 | |
| RAR_OS_BEOS  = 5
 | |
| 
 | |
| # Compression methods - '0'..'5'
 | |
| RAR_M0 = 0x30
 | |
| RAR_M1 = 0x31
 | |
| RAR_M2 = 0x32
 | |
| RAR_M3 = 0x33
 | |
| RAR_M4 = 0x34
 | |
| RAR_M5 = 0x35
 | |
| 
 | |
| #
 | |
| # RAR5 constants
 | |
| #
 | |
| 
 | |
| RAR5_BLOCK_MAIN = 1
 | |
| RAR5_BLOCK_FILE = 2
 | |
| RAR5_BLOCK_SERVICE = 3
 | |
| RAR5_BLOCK_ENCRYPTION = 4
 | |
| RAR5_BLOCK_ENDARC = 5
 | |
| 
 | |
| RAR5_BLOCK_FLAG_EXTRA_DATA = 0x01
 | |
| RAR5_BLOCK_FLAG_DATA_AREA = 0x02
 | |
| RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN = 0x04
 | |
| RAR5_BLOCK_FLAG_SPLIT_BEFORE = 0x08
 | |
| RAR5_BLOCK_FLAG_SPLIT_AFTER = 0x10
 | |
| RAR5_BLOCK_FLAG_DEPENDS_PREV = 0x20
 | |
| RAR5_BLOCK_FLAG_KEEP_WITH_PARENT = 0x40
 | |
| 
 | |
| RAR5_MAIN_FLAG_ISVOL = 0x01
 | |
| RAR5_MAIN_FLAG_HAS_VOLNR = 0x02
 | |
| RAR5_MAIN_FLAG_SOLID = 0x04
 | |
| RAR5_MAIN_FLAG_RECOVERY = 0x08
 | |
| RAR5_MAIN_FLAG_LOCKED = 0x10
 | |
| 
 | |
| RAR5_FILE_FLAG_ISDIR = 0x01
 | |
| RAR5_FILE_FLAG_HAS_MTIME = 0x02
 | |
| RAR5_FILE_FLAG_HAS_CRC32 = 0x04
 | |
| RAR5_FILE_FLAG_UNKNOWN_SIZE = 0x08
 | |
| 
 | |
| RAR5_COMPR_SOLID = 0x40
 | |
| 
 | |
| RAR5_ENC_FLAG_HAS_CHECKVAL = 0x01
 | |
| 
 | |
| RAR5_ENDARC_FLAG_NEXT_VOL = 0x01
 | |
| 
 | |
| RAR5_XFILE_ENCRYPTION = 1
 | |
| RAR5_XFILE_HASH = 2
 | |
| RAR5_XFILE_TIME = 3
 | |
| RAR5_XFILE_VERSION = 4
 | |
| RAR5_XFILE_REDIR = 5
 | |
| RAR5_XFILE_OWNER = 6
 | |
| RAR5_XFILE_SERVICE = 7
 | |
| 
 | |
| RAR5_XTIME_UNIXTIME = 0x01
 | |
| RAR5_XTIME_HAS_MTIME = 0x02
 | |
| RAR5_XTIME_HAS_CTIME = 0x04
 | |
| RAR5_XTIME_HAS_ATIME = 0x08
 | |
| 
 | |
| RAR5_XENC_CIPHER_AES256 = 0
 | |
| 
 | |
| RAR5_XENC_CHECKVAL = 0x01
 | |
| RAR5_XENC_TWEAKED = 0x02
 | |
| 
 | |
| RAR5_XHASH_BLAKE2SP = 0
 | |
| 
 | |
| RAR5_XREDIR_UNIX_SYMLINK = 1
 | |
| RAR5_XREDIR_WINDOWS_SYMLINK = 2
 | |
| RAR5_XREDIR_WINDOWS_JUNCTION = 3
 | |
| RAR5_XREDIR_HARD_LINK = 4
 | |
| RAR5_XREDIR_FILE_COPY = 5
 | |
| 
 | |
| RAR5_XREDIR_ISDIR = 0x01
 | |
| 
 | |
| RAR5_XOWNER_UNAME = 0x01
 | |
| RAR5_XOWNER_GNAME = 0x02
 | |
| RAR5_XOWNER_UID = 0x04
 | |
| RAR5_XOWNER_GID = 0x08
 | |
| 
 | |
| RAR5_OS_WINDOWS = 0
 | |
| RAR5_OS_UNIX = 1
 | |
| 
 | |
| ##
 | |
| ## internal constants
 | |
| ##
 | |
| 
 | |
| RAR_ID = b"Rar!\x1a\x07\x00"
 | |
| RAR5_ID = b"Rar!\x1a\x07\x01\x00"
 | |
| ZERO = b'\0'
 | |
| EMPTY = b''
 | |
| UTC = timezone(timedelta(0), 'UTC')
 | |
| BSIZE = 32 * 1024
 | |
| 
 | |
| def _get_rar_version(xfile):
 | |
|     """Check quickly whether file is rar archive.
 | |
|     """
 | |
|     with XFile(xfile) as fd:
 | |
|         buf = fd.read(len(RAR5_ID))
 | |
|     if buf.startswith(RAR_ID):
 | |
|         return 3
 | |
|     elif buf.startswith(RAR5_ID):
 | |
|         return 5
 | |
|     return 0
 | |
| 
 | |
| ##
 | |
| ## Public interface
 | |
| ##
 | |
| 
 | |
| def is_rarfile(xfile):
 | |
|     """Check quickly whether file is rar archive.
 | |
|     """
 | |
|     return _get_rar_version(xfile) > 0
 | |
| 
 | |
| class Error(Exception):
 | |
|     """Base class for rarfile errors."""
 | |
| 
 | |
| class BadRarFile(Error):
 | |
|     """Incorrect data in archive."""
 | |
| 
 | |
| class NotRarFile(Error):
 | |
|     """The file is not RAR archive."""
 | |
| 
 | |
| class BadRarName(Error):
 | |
|     """Cannot guess multipart name components."""
 | |
| 
 | |
| class NoRarEntry(Error):
 | |
|     """File not found in RAR"""
 | |
| 
 | |
| class PasswordRequired(Error):
 | |
|     """File requires password"""
 | |
| 
 | |
| class NeedFirstVolume(Error):
 | |
|     """Need to start from first volume."""
 | |
| 
 | |
| class NoCrypto(Error):
 | |
|     """Cannot parse encrypted headers - no crypto available."""
 | |
| 
 | |
| class RarExecError(Error):
 | |
|     """Problem reported by unrar/rar."""
 | |
| 
 | |
| class RarWarning(RarExecError):
 | |
|     """Non-fatal error"""
 | |
| 
 | |
| class RarFatalError(RarExecError):
 | |
|     """Fatal error"""
 | |
| 
 | |
| class RarCRCError(RarExecError):
 | |
|     """CRC error during unpacking"""
 | |
| 
 | |
| class RarLockedArchiveError(RarExecError):
 | |
|     """Must not modify locked archive"""
 | |
| 
 | |
| class RarWriteError(RarExecError):
 | |
|     """Write error"""
 | |
| 
 | |
| class RarOpenError(RarExecError):
 | |
|     """Open error"""
 | |
| 
 | |
| class RarUserError(RarExecError):
 | |
|     """User error"""
 | |
| 
 | |
| class RarMemoryError(RarExecError):
 | |
|     """Memory error"""
 | |
| 
 | |
| class RarCreateError(RarExecError):
 | |
|     """Create error"""
 | |
| 
 | |
| class RarNoFilesError(RarExecError):
 | |
|     """No files that match pattern were found"""
 | |
| 
 | |
| class RarUserBreak(RarExecError):
 | |
|     """User stop"""
 | |
| 
 | |
| class RarWrongPassword(RarExecError):
 | |
|     """Incorrect password"""
 | |
| 
 | |
| class RarUnknownError(RarExecError):
 | |
|     """Unknown exit code"""
 | |
| 
 | |
| class RarSignalExit(RarExecError):
 | |
|     """Unrar exited with signal"""
 | |
| 
 | |
| class RarCannotExec(RarExecError):
 | |
|     """Executable not found."""
 | |
| 
 | |
| 
 | |
| class RarInfo(object):
 | |
|     r"""An entry in rar archive.
 | |
| 
 | |
|     RAR3 extended timestamps are :class:`datetime.datetime` objects without timezone.
 | |
|     RAR5 extended timestamps are :class:`datetime.datetime` objects with UTC timezone.
 | |
| 
 | |
|     Attributes:
 | |
| 
 | |
|         filename
 | |
|             File name with relative path.
 | |
|             Path separator is '/'.  Always unicode string.
 | |
| 
 | |
|         date_time
 | |
|             File modification timestamp.   As tuple of (year, month, day, hour, minute, second).
 | |
|             RAR5 allows archives where it is missing, it's None then.
 | |
| 
 | |
|         file_size
 | |
|             Uncompressed size.
 | |
| 
 | |
|         compress_size
 | |
|             Compressed size.
 | |
| 
 | |
|         compress_type
 | |
|             Compression method: one of :data:`RAR_M0` .. :data:`RAR_M5` constants.
 | |
| 
 | |
|         extract_version
 | |
|             Minimal Rar version needed for decompressing.  As (major*10 + minor),
 | |
|             so 2.9 is 29.
 | |
| 
 | |
|             RAR3: 10, 20, 29
 | |
| 
 | |
|             RAR5 does not have such field in archive, it's simply set to 50.
 | |
| 
 | |
|         host_os
 | |
|             Host OS type, one of RAR_OS_* constants.
 | |
| 
 | |
|             RAR3: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`, :data:`RAR_OS_MSDOS`,
 | |
|             :data:`RAR_OS_OS2`, :data:`RAR_OS_BEOS`.
 | |
| 
 | |
|             RAR5: :data:`RAR_OS_WIN32`, :data:`RAR_OS_UNIX`.
 | |
| 
 | |
|         mode
 | |
|             File attributes. May be either dos-style or unix-style, depending on host_os.
 | |
| 
 | |
|         mtime
 | |
|             File modification time.  Same value as :attr:`date_time`
 | |
|             but as :class:`datetime.datetime` object with extended precision.
 | |
| 
 | |
|         ctime
 | |
|             Optional time field: creation time.  As :class:`datetime.datetime` object.
 | |
| 
 | |
|         atime
 | |
|             Optional time field: last access time.  As :class:`datetime.datetime` object.
 | |
| 
 | |
|         arctime
 | |
|             Optional time field: archival time.  As :class:`datetime.datetime` object.
 | |
|             (RAR3-only)
 | |
| 
 | |
|         CRC
 | |
|             CRC-32 of uncompressed file, unsigned int.
 | |
| 
 | |
|             RAR5: may be None.
 | |
| 
 | |
|         blake2sp_hash
 | |
|             Blake2SP hash over decompressed data.  (RAR5-only)
 | |
| 
 | |
|         comment
 | |
|             Optional file comment field.  Unicode string.  (RAR3-only)
 | |
| 
 | |
|         file_redir
 | |
|             If not None, file is link of some sort.  Contains tuple of (type, flags, target).
 | |
|             (RAR5-only)
 | |
| 
 | |
|             Type is one of constants:
 | |
| 
 | |
|                 :data:`RAR5_XREDIR_UNIX_SYMLINK`
 | |
|                     unix symlink to target.
 | |
|                 :data:`RAR5_XREDIR_WINDOWS_SYMLINK`
 | |
|                     windows symlink to target.
 | |
|                 :data:`RAR5_XREDIR_WINDOWS_JUNCTION`
 | |
|                     windows junction.
 | |
|                 :data:`RAR5_XREDIR_HARD_LINK`
 | |
|                     hard link to target.
 | |
|                 :data:`RAR5_XREDIR_FILE_COPY`
 | |
|                     current file is copy of another archive entry.
 | |
| 
 | |
|             Flags may contain :data:`RAR5_XREDIR_ISDIR` bit.
 | |
| 
 | |
|         volume
 | |
|             Volume nr, starting from 0.
 | |
| 
 | |
|         volume_file
 | |
|             Volume file name, where file starts.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     # zipfile-compatible fields
 | |
|     filename = None
 | |
|     file_size = None
 | |
|     compress_size = None
 | |
|     date_time = None
 | |
|     comment = None
 | |
|     CRC = None
 | |
|     volume = None
 | |
|     orig_filename = None
 | |
| 
 | |
|     # optional extended time fields, datetime() objects.
 | |
|     mtime = None
 | |
|     ctime = None
 | |
|     atime = None
 | |
| 
 | |
|     extract_version = None
 | |
|     mode = None
 | |
|     host_os = None
 | |
|     compress_type = None
 | |
| 
 | |
|     # rar3-only fields
 | |
|     comment = None
 | |
|     arctime = None
 | |
| 
 | |
|     # rar5-only fields
 | |
|     blake2sp_hash = None
 | |
|     file_redir = None
 | |
| 
 | |
|     # internal fields
 | |
|     flags = 0
 | |
|     type = None
 | |
| 
 | |
|     def isdir(self):
 | |
|         """Returns True if entry is a directory.
 | |
|         """
 | |
|         if self.type == RAR_BLOCK_FILE:
 | |
|             return (self.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY
 | |
|         return False
 | |
| 
 | |
|     def needs_password(self):
 | |
|         """Returns True if data is stored password-protected.
 | |
|         """
 | |
|         if self.type == RAR_BLOCK_FILE:
 | |
|             return (self.flags & RAR_FILE_PASSWORD) > 0
 | |
|         return False
 | |
| 
 | |
| 
 | |
| class RarFile(object):
 | |
|     """Parse RAR structure, provide access to files in archive.
 | |
|     """
 | |
| 
 | |
|     #: Archive comment.  Unicode string or None.
 | |
|     comment = None
 | |
| 
 | |
|     def __init__(self, rarfile, mode="r", charset=None, info_callback=None,
 | |
|                  crc_check=True, errors="stop"):
 | |
|         """Open and parse a RAR archive.
 | |
| 
 | |
|         Parameters:
 | |
| 
 | |
|             rarfile
 | |
|                 archive file name
 | |
|             mode
 | |
|                 only 'r' is supported.
 | |
|             charset
 | |
|                 fallback charset to use, if filenames are not already Unicode-enabled.
 | |
|             info_callback
 | |
|                 debug callback, gets to see all archive entries.
 | |
|             crc_check
 | |
|                 set to False to disable CRC checks
 | |
|             errors
 | |
|                 Either "stop" to quietly stop parsing on errors,
 | |
|                 or "strict" to raise errors.  Default is "stop".
 | |
|         """
 | |
|         self._rarfile = rarfile
 | |
|         self._charset = charset or DEFAULT_CHARSET
 | |
|         self._info_callback = info_callback
 | |
|         self._crc_check = crc_check
 | |
|         self._password = None
 | |
|         self._file_parser = None
 | |
| 
 | |
|         if errors == "stop":
 | |
|             self._strict = False
 | |
|         elif errors == "strict":
 | |
|             self._strict = True
 | |
|         else:
 | |
|             raise ValueError("Invalid value for 'errors' parameter.")
 | |
| 
 | |
|         if mode != "r":
 | |
|             raise NotImplementedError("RarFile supports only mode=r")
 | |
| 
 | |
|         self._parse()
 | |
| 
 | |
|     def __enter__(self):
 | |
|         """Open context."""
 | |
|         return self
 | |
| 
 | |
|     def __exit__(self, typ, value, traceback):
 | |
|         """Exit context"""
 | |
|         self.close()
 | |
| 
 | |
|     def setpassword(self, password):
 | |
|         """Sets the password to use when extracting.
 | |
|         """
 | |
|         self._password = password
 | |
|         if self._file_parser:
 | |
|             if self._file_parser.has_header_encryption():
 | |
|                 self._file_parser = None
 | |
|         if not self._file_parser:
 | |
|             self._parse()
 | |
|         else:
 | |
|             self._file_parser.setpassword(self._password)
 | |
| 
 | |
|     def needs_password(self):
 | |
|         """Returns True if any archive entries require password for extraction.
 | |
|         """
 | |
|         return self._file_parser.needs_password()
 | |
| 
 | |
|     def namelist(self):
 | |
|         """Return list of filenames in archive.
 | |
|         """
 | |
|         return [f.filename for f in self.infolist()]
 | |
| 
 | |
|     def infolist(self):
 | |
|         """Return RarInfo objects for all files/directories in archive.
 | |
|         """
 | |
|         return self._file_parser.infolist()
 | |
| 
 | |
|     def volumelist(self):
 | |
|         """Returns filenames of archive volumes.
 | |
| 
 | |
|         In case of single-volume archive, the list contains
 | |
|         just the name of main archive file.
 | |
|         """
 | |
|         return self._file_parser.volumelist()
 | |
| 
 | |
|     def getinfo(self, fname):
 | |
|         """Return RarInfo for file.
 | |
|         """
 | |
|         return self._file_parser.getinfo(fname)
 | |
| 
 | |
|     def open(self, fname, mode='r', psw=None):
 | |
|         """Returns file-like object (:class:`RarExtFile`) from where the data can be read.
 | |
| 
 | |
|         The object implements :class:`io.RawIOBase` interface, so it can
 | |
|         be further wrapped with :class:`io.BufferedReader`
 | |
|         and :class:`io.TextIOWrapper`.
 | |
| 
 | |
|         On older Python where io module is not available, it implements
 | |
|         only .read(), .seek(), .tell() and .close() methods.
 | |
| 
 | |
|         The object is seekable, although the seeking is fast only on
 | |
|         uncompressed files, on compressed files the seeking is implemented
 | |
|         by reading ahead and/or restarting the decompression.
 | |
| 
 | |
|         Parameters:
 | |
| 
 | |
|             fname
 | |
|                 file name or RarInfo instance.
 | |
|             mode
 | |
|                 must be 'r'
 | |
|             psw
 | |
|                 password to use for extracting.
 | |
|         """
 | |
| 
 | |
|         if mode != 'r':
 | |
|             raise NotImplementedError("RarFile.open() supports only mode=r")
 | |
| 
 | |
|         # entry lookup
 | |
|         inf = self.getinfo(fname)
 | |
|         if inf.isdir():
 | |
|             raise TypeError("Directory does not have any data: " + inf.filename)
 | |
| 
 | |
|         # check password
 | |
|         if inf.needs_password():
 | |
|             psw = psw or self._password
 | |
|             if psw is None:
 | |
|                 raise PasswordRequired("File %s requires password" % inf.filename)
 | |
|         else:
 | |
|             psw = None
 | |
| 
 | |
|         return self._file_parser.open(inf, psw)
 | |
| 
 | |
|     def read(self, fname, psw=None):
 | |
|         """Return uncompressed data for archive entry.
 | |
| 
 | |
|         For longer files using :meth:`RarFile.open` may be better idea.
 | |
| 
 | |
|         Parameters:
 | |
| 
 | |
|             fname
 | |
|                 filename or RarInfo instance
 | |
|             psw
 | |
|                 password to use for extracting.
 | |
|         """
 | |
| 
 | |
|         with self.open(fname, 'r', psw) as f:
 | |
|             return f.read()
 | |
| 
 | |
|     def close(self):
 | |
|         """Release open resources."""
 | |
|         pass
 | |
| 
 | |
|     def printdir(self):
 | |
|         """Print archive file list to stdout."""
 | |
|         for f in self.infolist():
 | |
|             print(f.filename)
 | |
| 
 | |
|     def extract(self, member, path=None, pwd=None):
 | |
|         """Extract single file into current directory.
 | |
| 
 | |
|         Parameters:
 | |
| 
 | |
|             member
 | |
|                 filename or :class:`RarInfo` instance
 | |
|             path
 | |
|                 optional destination path
 | |
|             pwd
 | |
|                 optional password to use
 | |
|         """
 | |
|         if isinstance(member, RarInfo):
 | |
|             fname = member.filename
 | |
|         else:
 | |
|             fname = member
 | |
|         self._extract([fname], path, pwd)
 | |
| 
 | |
|     def extractall(self, path=None, members=None, pwd=None):
 | |
|         """Extract all files into current directory.
 | |
| 
 | |
|         Parameters:
 | |
| 
 | |
|             path
 | |
|                 optional destination path
 | |
|             members
 | |
|                 optional filename or :class:`RarInfo` instance list to extract
 | |
|             pwd
 | |
|                 optional password to use
 | |
|         """
 | |
|         fnlist = []
 | |
|         if members is not None:
 | |
|             for m in members:
 | |
|                 if isinstance(m, RarInfo):
 | |
|                     fnlist.append(m.filename)
 | |
|                 else:
 | |
|                     fnlist.append(m)
 | |
|         self._extract(fnlist, path, pwd)
 | |
| 
 | |
|     def testrar(self):
 | |
|         """Let 'unrar' test the archive.
 | |
|         """
 | |
|         cmd = [UNRAR_TOOL] + list(TEST_ARGS)
 | |
|         add_password_arg(cmd, self._password)
 | |
|         cmd.append('--')
 | |
|         with XTempFile(self._rarfile) as rarfile:
 | |
|             cmd.append(rarfile)
 | |
|             p = custom_popen(cmd)
 | |
|             output = p.communicate()[0]
 | |
|             check_returncode(p, output)
 | |
| 
 | |
|     def strerror(self):
 | |
|         """Return error string if parsing failed or None if no problems.
 | |
|         """
 | |
|         if not self._file_parser:
 | |
|             return "Not a RAR file"
 | |
|         return self._file_parser.strerror()
 | |
| 
 | |
|     ##
 | |
|     ## private methods
 | |
|     ##
 | |
| 
 | |
|     def _parse(self):
 | |
|         ver = _get_rar_version(self._rarfile)
 | |
|         if ver == 3:
 | |
|             p3 = RAR3Parser(self._rarfile, self._password, self._crc_check,
 | |
|                             self._charset, self._strict, self._info_callback)
 | |
|             self._file_parser = p3  # noqa
 | |
|         elif ver == 5:
 | |
|             p5 = RAR5Parser(self._rarfile, self._password, self._crc_check,
 | |
|                             self._charset, self._strict, self._info_callback)
 | |
|             self._file_parser = p5  # noqa
 | |
|         else:
 | |
|             raise BadRarFile("Not a RAR file")
 | |
| 
 | |
|         self._file_parser.parse()
 | |
|         self.comment = self._file_parser.comment
 | |
| 
 | |
|     # call unrar to extract a file
 | |
|     def _extract(self, fnlist, path=None, psw=None):
 | |
|         cmd = [UNRAR_TOOL] + list(EXTRACT_ARGS)
 | |
| 
 | |
|         # pasoword
 | |
|         psw = psw or self._password
 | |
|         add_password_arg(cmd, psw)
 | |
|         cmd.append('--')
 | |
| 
 | |
|         # rar file
 | |
|         with XTempFile(self._rarfile) as rarfn:
 | |
|             cmd.append(rarfn)
 | |
| 
 | |
|             # file list
 | |
|             for fn in fnlist:
 | |
|                 if os.sep != PATH_SEP:
 | |
|                     fn = fn.replace(PATH_SEP, os.sep)
 | |
|                 cmd.append(fn)
 | |
| 
 | |
|             # destination path
 | |
|             if path is not None:
 | |
|                 cmd.append(path + os.sep)
 | |
| 
 | |
|             # call
 | |
|             p = custom_popen(cmd)
 | |
|             output = p.communicate()[0]
 | |
|             check_returncode(p, output)
 | |
| 
 | |
| #
 | |
| # File format parsing
 | |
| #
 | |
| 
 | |
| class CommonParser(object):
 | |
|     """Shared parser parts."""
 | |
|     _main = None
 | |
|     _hdrenc_main = None
 | |
|     _needs_password = False
 | |
|     _fd = None
 | |
|     _expect_sig = None
 | |
|     _parse_error = None
 | |
|     _password = None
 | |
|     comment = None
 | |
| 
 | |
|     def __init__(self, rarfile, password, crc_check, charset, strict, info_cb):
 | |
|         self._rarfile = rarfile
 | |
|         self._password = password
 | |
|         self._crc_check = crc_check
 | |
|         self._charset = charset
 | |
|         self._strict = strict
 | |
|         self._info_callback = info_cb
 | |
|         self._info_list = []
 | |
|         self._info_map = {}
 | |
|         self._vol_list = []
 | |
| 
 | |
|     def has_header_encryption(self):
 | |
|         """Returns True if headers are encrypted
 | |
|         """
 | |
|         if self._hdrenc_main:
 | |
|             return True
 | |
|         if self._main:
 | |
|             if self._main.flags & RAR_MAIN_PASSWORD:
 | |
|                 return True
 | |
|         return False
 | |
| 
 | |
|     def setpassword(self, psw):
 | |
|         """Set cached password."""
 | |
|         self._password = psw
 | |
| 
 | |
|     def volumelist(self):
 | |
|         """Volume files"""
 | |
|         return self._vol_list
 | |
| 
 | |
|     def needs_password(self):
 | |
|         """Is password required"""
 | |
|         return self._needs_password
 | |
| 
 | |
|     def strerror(self):
 | |
|         """Last error"""
 | |
|         return self._parse_error
 | |
| 
 | |
|     def infolist(self):
 | |
|         """List of RarInfo records.
 | |
|         """
 | |
|         return self._info_list
 | |
| 
 | |
|     def getinfo(self, member):
 | |
|         """Return RarInfo for filename
 | |
|         """
 | |
|         if isinstance(member, RarInfo):
 | |
|             fname = member.filename
 | |
|         else:
 | |
|             fname = member
 | |
| 
 | |
|         # accept both ways here
 | |
|         if PATH_SEP == '/':
 | |
|             fname2 = fname.replace("\\", "/")
 | |
|         else:
 | |
|             fname2 = fname.replace("/", "\\")
 | |
| 
 | |
|         try:
 | |
|             return self._info_map[fname]
 | |
|         except KeyError:
 | |
|             try:
 | |
|                 return self._info_map[fname2]
 | |
|             except KeyError:
 | |
|                 raise NoRarEntry("No such file: %s" % fname)
 | |
| 
 | |
|     # read rar
 | |
|     def parse(self):
 | |
|         """Process file."""
 | |
|         self._fd = None
 | |
|         try:
 | |
|             self._parse_real()
 | |
|         finally:
 | |
|             if self._fd:
 | |
|                 self._fd.close()
 | |
|                 self._fd = None
 | |
| 
 | |
|     def _parse_real(self):
 | |
|         fd = XFile(self._rarfile)
 | |
|         self._fd = fd
 | |
|         sig = fd.read(len(self._expect_sig))
 | |
|         if sig != self._expect_sig:
 | |
|             if isinstance(self._rarfile, (str, unicode)):
 | |
|                 raise NotRarFile("Not a Rar archive: {}".format(self._rarfile))
 | |
|             raise NotRarFile("Not a Rar archive")
 | |
| 
 | |
|         volume = 0  # first vol (.rar) is 0
 | |
|         more_vols = False
 | |
|         endarc = False
 | |
|         volfile = self._rarfile
 | |
|         self._vol_list = [self._rarfile]
 | |
|         while 1:
 | |
|             if endarc:
 | |
|                 h = None    # don't read past ENDARC
 | |
|             else:
 | |
|                 h = self._parse_header(fd)
 | |
|             if not h:
 | |
|                 if more_vols:
 | |
|                     volume += 1
 | |
|                     fd.close()
 | |
|                     try:
 | |
|                         volfile = self._next_volname(volfile)
 | |
|                         fd = XFile(volfile)
 | |
|                     except IOError:
 | |
|                         self._set_error("Cannot open next volume: %s", volfile)
 | |
|                         break
 | |
|                     self._fd = fd
 | |
|                     sig = fd.read(len(self._expect_sig))
 | |
|                     if sig != self._expect_sig:
 | |
|                         self._set_error("Invalid volume sig: %s", volfile)
 | |
|                         break
 | |
|                     more_vols = False
 | |
|                     endarc = False
 | |
|                     self._vol_list.append(volfile)
 | |
|                     continue
 | |
|                 break
 | |
|             h.volume = volume
 | |
|             h.volume_file = volfile
 | |
| 
 | |
|             if h.type == RAR_BLOCK_MAIN and not self._main:
 | |
|                 self._main = h
 | |
|                 if h.flags & RAR_MAIN_NEWNUMBERING:
 | |
|                     # RAR 2.x does not set FIRSTVOLUME,
 | |
|                     # so check it only if NEWNUMBERING is used
 | |
|                     if (h.flags & RAR_MAIN_FIRSTVOLUME) == 0:
 | |
|                         raise NeedFirstVolume("Need to start from first volume")
 | |
|                 if h.flags & RAR_MAIN_PASSWORD:
 | |
|                     self._needs_password = True
 | |
|                     if not self._password:
 | |
|                         break
 | |
|             elif h.type == RAR_BLOCK_ENDARC:
 | |
|                 more_vols = (h.flags & RAR_ENDARC_NEXT_VOLUME) > 0
 | |
|                 endarc = True
 | |
|             elif h.type == RAR_BLOCK_FILE:
 | |
|                 # RAR 2.x does not write RAR_BLOCK_ENDARC
 | |
|                 if h.flags & RAR_FILE_SPLIT_AFTER:
 | |
|                     more_vols = True
 | |
|                 # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
 | |
|                 if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE:
 | |
|                     raise NeedFirstVolume("Need to start from first volume")
 | |
| 
 | |
|             if h.needs_password():
 | |
|                 self._needs_password = True
 | |
| 
 | |
|             # store it
 | |
|             self.process_entry(fd, h)
 | |
| 
 | |
|             if self._info_callback:
 | |
|                 self._info_callback(h)
 | |
| 
 | |
|             # go to next header
 | |
|             if h.add_size > 0:
 | |
|                 fd.seek(h.data_offset + h.add_size, 0)
 | |
| 
 | |
|     def process_entry(self, fd, item):
 | |
|         """Examine item, add into lookup cache."""
 | |
|         raise NotImplementedError()
 | |
| 
 | |
|     def _decrypt_header(self, fd):
 | |
|         raise NotImplementedError('_decrypt_header')
 | |
| 
 | |
|     def _parse_block_header(self, fd):
 | |
|         raise NotImplementedError('_parse_block_header')
 | |
| 
 | |
|     def _open_hack(self, inf, psw):
 | |
|         raise NotImplementedError('_open_hack')
 | |
| 
 | |
|     # read single header
 | |
|     def _parse_header(self, fd):
 | |
|         try:
 | |
|             # handle encrypted headers
 | |
|             if (self._main and self._main.flags & RAR_MAIN_PASSWORD) or self._hdrenc_main:
 | |
|                 if not self._password:
 | |
|                     return
 | |
|                 fd = self._decrypt_header(fd)
 | |
| 
 | |
|             # now read actual header
 | |
|             return self._parse_block_header(fd)
 | |
|         except struct.error:
 | |
|             self._set_error('Broken header in RAR file')
 | |
|             return None
 | |
| 
 | |
|     # given current vol name, construct next one
 | |
|     def _next_volname(self, volfile):
 | |
|         if is_filelike(volfile):
 | |
|             raise IOError("Working on single FD")
 | |
|         if self._main.flags & RAR_MAIN_NEWNUMBERING:
 | |
|             return _next_newvol(volfile)
 | |
|         return _next_oldvol(volfile)
 | |
| 
 | |
|     def _set_error(self, msg, *args):
 | |
|         if args:
 | |
|             msg = msg % args
 | |
|         self._parse_error = msg
 | |
|         if self._strict:
 | |
|             raise BadRarFile(msg)
 | |
| 
 | |
|     def open(self, inf, psw):
 | |
|         """Return stream object for file data."""
 | |
| 
 | |
|         if inf.file_redir:
 | |
|             # cannot leave to unrar as it expects copied file to exist
 | |
|             if inf.file_redir[0] in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK):
 | |
|                 inf = self.getinfo(inf.file_redir[2])
 | |
|                 if not inf:
 | |
|                     raise BadRarFile('cannot find copied file')
 | |
| 
 | |
|         if inf.flags & RAR_FILE_SPLIT_BEFORE:
 | |
|             raise NeedFirstVolume("Partial file, please start from first volume: " + inf.filename)
 | |
| 
 | |
|         # is temp write usable?
 | |
|         use_hack = 1
 | |
|         if not self._main:
 | |
|             use_hack = 0
 | |
|         elif self._main._must_disable_hack():
 | |
|             use_hack = 0
 | |
|         elif inf._must_disable_hack():
 | |
|             use_hack = 0
 | |
|         elif is_filelike(self._rarfile):
 | |
|             pass
 | |
|         elif inf.file_size > HACK_SIZE_LIMIT:
 | |
|             use_hack = 0
 | |
|         elif not USE_EXTRACT_HACK:
 | |
|             use_hack = 0
 | |
| 
 | |
|         # now extract
 | |
|         if inf.compress_type == RAR_M0 and (inf.flags & RAR_FILE_PASSWORD) == 0 and inf.file_redir is None:
 | |
|             return self._open_clear(inf)
 | |
|         elif use_hack:
 | |
|             return self._open_hack(inf, psw)
 | |
|         elif is_filelike(self._rarfile):
 | |
|             return self._open_unrar_membuf(self._rarfile, inf, psw)
 | |
|         else:
 | |
|             return self._open_unrar(self._rarfile, inf, psw)
 | |
| 
 | |
|     def _open_clear(self, inf):
 | |
|         return DirectReader(self, inf)
 | |
| 
 | |
|     def _open_hack_core(self, inf, psw, prefix, suffix):
 | |
| 
 | |
|         size = inf.compress_size + inf.header_size
 | |
|         rf = XFile(inf.volume_file, 0)
 | |
|         rf.seek(inf.header_offset)
 | |
| 
 | |
|         tmpfd, tmpname = mkstemp(suffix='.rar')
 | |
|         tmpf = os.fdopen(tmpfd, "wb")
 | |
| 
 | |
|         try:
 | |
|             tmpf.write(prefix)
 | |
|             while size > 0:
 | |
|                 if size > BSIZE:
 | |
|                     buf = rf.read(BSIZE)
 | |
|                 else:
 | |
|                     buf = rf.read(size)
 | |
|                 if not buf:
 | |
|                     raise BadRarFile('read failed: ' + inf.filename)
 | |
|                 tmpf.write(buf)
 | |
|                 size -= len(buf)
 | |
|             tmpf.write(suffix)
 | |
|             tmpf.close()
 | |
|             rf.close()
 | |
|         except:
 | |
|             rf.close()
 | |
|             tmpf.close()
 | |
|             os.unlink(tmpname)
 | |
|             raise
 | |
| 
 | |
|         return self._open_unrar(tmpname, inf, psw, tmpname)
 | |
| 
 | |
|     # write in-memory archive to temp file - needed for solid archives
 | |
|     def _open_unrar_membuf(self, memfile, inf, psw):
 | |
|         tmpname = membuf_tempfile(memfile)
 | |
|         return self._open_unrar(tmpname, inf, psw, tmpname, force_file=True)
 | |
| 
 | |
|     # extract using unrar
 | |
|     def _open_unrar(self, rarfile, inf, psw=None, tmpfile=None, force_file=False):
 | |
|         cmd = [UNRAR_TOOL] + list(OPEN_ARGS)
 | |
|         add_password_arg(cmd, psw)
 | |
|         cmd.append("--")
 | |
|         cmd.append(rarfile)
 | |
| 
 | |
|         # not giving filename avoids encoding related problems
 | |
|         if not tmpfile or force_file:
 | |
|             fn = inf.filename
 | |
|             if PATH_SEP != os.sep:
 | |
|                 fn = fn.replace(PATH_SEP, os.sep)
 | |
|             cmd.append(fn)
 | |
| 
 | |
|         # read from unrar pipe
 | |
|         return PipeReader(self, inf, cmd, tmpfile)
 | |
| 
 | |
| #
 | |
| # RAR3 format
 | |
| #
 | |
| 
 | |
| class Rar3Info(RarInfo):
 | |
|     """RAR3 specific fields."""
 | |
|     extract_version = 15
 | |
|     salt = None
 | |
|     add_size = 0
 | |
|     header_crc = None
 | |
|     header_size = None
 | |
|     header_offset = None
 | |
|     data_offset = None
 | |
|     _md_class = None
 | |
|     _md_expect = None
 | |
| 
 | |
|     # make sure some rar5 fields are always present
 | |
|     file_redir = None
 | |
|     blake2sp_hash = None
 | |
| 
 | |
|     def _must_disable_hack(self):
 | |
|         if self.type == RAR_BLOCK_FILE:
 | |
|             if self.flags & RAR_FILE_PASSWORD:
 | |
|                 return True
 | |
|             elif self.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
 | |
|                 return True
 | |
|         elif self.type == RAR_BLOCK_MAIN:
 | |
|             if self.flags & (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD):
 | |
|                 return True
 | |
|         return False
 | |
| 
 | |
| 
 | |
| class RAR3Parser(CommonParser):
 | |
|     """Parse RAR3 file format.
 | |
|     """
 | |
|     _expect_sig = RAR_ID
 | |
|     _last_aes_key = (None, None, None)   # (salt, key, iv)
 | |
| 
 | |
|     def _decrypt_header(self, fd):
 | |
|         if not _have_crypto:
 | |
|             raise NoCrypto('Cannot parse encrypted headers - no crypto')
 | |
|         salt = fd.read(8)
 | |
|         if self._last_aes_key[0] == salt:
 | |
|             key, iv = self._last_aes_key[1:]
 | |
|         else:
 | |
|             key, iv = rar3_s2k(self._password, salt)
 | |
|             self._last_aes_key = (salt, key, iv)
 | |
|         return HeaderDecrypt(fd, key, iv)
 | |
| 
 | |
|     # common header
 | |
|     def _parse_block_header(self, fd):
 | |
|         h = Rar3Info()
 | |
|         h.header_offset = fd.tell()
 | |
| 
 | |
|         # read and parse base header
 | |
|         buf = fd.read(S_BLK_HDR.size)
 | |
|         if not buf:
 | |
|             return None
 | |
|         t = S_BLK_HDR.unpack_from(buf)
 | |
|         h.header_crc, h.type, h.flags, h.header_size = t
 | |
| 
 | |
|         # read full header
 | |
|         if h.header_size > S_BLK_HDR.size:
 | |
|             hdata = buf + fd.read(h.header_size - S_BLK_HDR.size)
 | |
|         else:
 | |
|             hdata = buf
 | |
|         h.data_offset = fd.tell()
 | |
| 
 | |
|         # unexpected EOF?
 | |
|         if len(hdata) != h.header_size:
 | |
|             self._set_error('Unexpected EOF when reading header')
 | |
|             return None
 | |
| 
 | |
|         pos = S_BLK_HDR.size
 | |
| 
 | |
|         # block has data assiciated with it?
 | |
|         if h.flags & RAR_LONG_BLOCK:
 | |
|             h.add_size, pos = load_le32(hdata, pos)
 | |
|         else:
 | |
|             h.add_size = 0
 | |
| 
 | |
|         # parse interesting ones, decide header boundaries for crc
 | |
|         if h.type == RAR_BLOCK_MARK:
 | |
|             return h
 | |
|         elif h.type == RAR_BLOCK_MAIN:
 | |
|             pos += 6
 | |
|             if h.flags & RAR_MAIN_ENCRYPTVER:
 | |
|                 pos += 1
 | |
|             crc_pos = pos
 | |
|             if h.flags & RAR_MAIN_COMMENT:
 | |
|                 self._parse_subblocks(h, hdata, pos)
 | |
|         elif h.type == RAR_BLOCK_FILE:
 | |
|             pos = self._parse_file_header(h, hdata, pos - 4)
 | |
|             crc_pos = pos
 | |
|             if h.flags & RAR_FILE_COMMENT:
 | |
|                 pos = self._parse_subblocks(h, hdata, pos)
 | |
|         elif h.type == RAR_BLOCK_SUB:
 | |
|             pos = self._parse_file_header(h, hdata, pos - 4)
 | |
|             crc_pos = h.header_size
 | |
|         elif h.type == RAR_BLOCK_OLD_AUTH:
 | |
|             pos += 8
 | |
|             crc_pos = pos
 | |
|         elif h.type == RAR_BLOCK_OLD_EXTRA:
 | |
|             pos += 7
 | |
|             crc_pos = pos
 | |
|         else:
 | |
|             crc_pos = h.header_size
 | |
| 
 | |
|         # check crc
 | |
|         if h.type == RAR_BLOCK_OLD_SUB:
 | |
|             crcdat = hdata[2:] + fd.read(h.add_size)
 | |
|         else:
 | |
|             crcdat = hdata[2:crc_pos]
 | |
| 
 | |
|         calc_crc = rar_crc32(crcdat) & 0xFFFF
 | |
| 
 | |
|         # return good header
 | |
|         if h.header_crc == calc_crc:
 | |
|             return h
 | |
| 
 | |
|         # header parsing failed.
 | |
|         self._set_error('Header CRC error (%02x): exp=%x got=%x (xlen = %d)',
 | |
|                         h.type, h.header_crc, calc_crc, len(crcdat))
 | |
| 
 | |
|         # instead panicing, send eof
 | |
|         return None
 | |
| 
 | |
|     # read file-specific header
 | |
|     def _parse_file_header(self, h, hdata, pos):
 | |
|         fld = S_FILE_HDR.unpack_from(hdata, pos)
 | |
|         pos += S_FILE_HDR.size
 | |
| 
 | |
|         h.compress_size = fld[0]
 | |
|         h.file_size = fld[1]
 | |
|         h.host_os = fld[2]
 | |
|         h.CRC = fld[3]
 | |
|         h.date_time = parse_dos_time(fld[4])
 | |
|         h.mtime = to_datetime(h.date_time)
 | |
|         h.extract_version = fld[5]
 | |
|         h.compress_type = fld[6]
 | |
|         name_size = fld[7]
 | |
|         h.mode = fld[8]
 | |
| 
 | |
|         h._md_class = CRC32Context
 | |
|         h._md_expect = h.CRC
 | |
| 
 | |
|         if h.flags & RAR_FILE_LARGE:
 | |
|             h1, pos = load_le32(hdata, pos)
 | |
|             h2, pos = load_le32(hdata, pos)
 | |
|             h.compress_size |= h1 << 32
 | |
|             h.file_size |= h2 << 32
 | |
|             h.add_size = h.compress_size
 | |
| 
 | |
|         name, pos = load_bytes(hdata, name_size, pos)
 | |
|         if h.flags & RAR_FILE_UNICODE:
 | |
|             nul = name.find(ZERO)
 | |
|             h.orig_filename = name[:nul]
 | |
|             u = UnicodeFilename(h.orig_filename, name[nul + 1:])
 | |
|             h.filename = u.decode()
 | |
| 
 | |
|             # if parsing failed fall back to simple name
 | |
|             if u.failed:
 | |
|                 h.filename = self._decode(h.orig_filename)
 | |
|         else:
 | |
|             h.orig_filename = name
 | |
|             h.filename = self._decode(name)
 | |
| 
 | |
|         # change separator, if requested
 | |
|         if PATH_SEP != '\\':
 | |
|             h.filename = h.filename.replace('\\', PATH_SEP)
 | |
| 
 | |
|         if h.flags & RAR_FILE_SALT:
 | |
|             h.salt, pos = load_bytes(hdata, 8, pos)
 | |
|         else:
 | |
|             h.salt = None
 | |
| 
 | |
|         # optional extended time stamps
 | |
|         if h.flags & RAR_FILE_EXTTIME:
 | |
|             pos = _parse_ext_time(h, hdata, pos)
 | |
|         else:
 | |
|             h.mtime = h.atime = h.ctime = h.arctime = None
 | |
| 
 | |
|         return pos
 | |
| 
 | |
|     # find old-style comment subblock
 | |
|     def _parse_subblocks(self, h, hdata, pos):
 | |
|         while pos < len(hdata):
 | |
|             # ordinary block header
 | |
|             t = S_BLK_HDR.unpack_from(hdata, pos)
 | |
|             ___scrc, stype, sflags, slen = t
 | |
|             pos_next = pos + slen
 | |
|             pos += S_BLK_HDR.size
 | |
| 
 | |
|             # corrupt header
 | |
|             if pos_next < pos:
 | |
|                 break
 | |
| 
 | |
|             # followed by block-specific header
 | |
|             if stype == RAR_BLOCK_OLD_COMMENT and pos + S_COMMENT_HDR.size <= pos_next:
 | |
|                 declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos)
 | |
|                 pos += S_COMMENT_HDR.size
 | |
|                 data = hdata[pos : pos_next]
 | |
|                 cmt = rar3_decompress(ver, meth, data, declen, sflags,
 | |
|                                       crc, self._password)
 | |
|                 if not self._crc_check:
 | |
|                     h.comment = self._decode_comment(cmt)
 | |
|                 elif rar_crc32(cmt) & 0xFFFF == crc:
 | |
|                     h.comment = self._decode_comment(cmt)
 | |
| 
 | |
|             pos = pos_next
 | |
|         return pos
 | |
| 
 | |
|     def _read_comment_v3(self, inf, psw=None):
 | |
| 
 | |
|         # read data
 | |
|         with XFile(inf.volume_file) as rf:
 | |
|             rf.seek(inf.data_offset)
 | |
|             data = rf.read(inf.compress_size)
 | |
| 
 | |
|         # decompress
 | |
|         cmt = rar3_decompress(inf.extract_version, inf.compress_type, data,
 | |
|                               inf.file_size, inf.flags, inf.CRC, psw, inf.salt)
 | |
| 
 | |
|         # check crc
 | |
|         if self._crc_check:
 | |
|             crc = rar_crc32(cmt)
 | |
|             if crc != inf.CRC:
 | |
|                 return None
 | |
| 
 | |
|         return self._decode_comment(cmt)
 | |
| 
 | |
|     def _decode(self, val):
 | |
|         for c in TRY_ENCODINGS:
 | |
|             try:
 | |
|                 return val.decode(c)
 | |
|             except UnicodeError:
 | |
|                 pass
 | |
|         return val.decode(self._charset, 'replace')
 | |
| 
 | |
|     def _decode_comment(self, val):
 | |
|         return self._decode(val)
 | |
| 
 | |
|     def process_entry(self, fd, item):
 | |
|         if item.type == RAR_BLOCK_FILE:
 | |
|             # use only first part
 | |
|             if (item.flags & RAR_FILE_SPLIT_BEFORE) == 0:
 | |
|                 self._info_map[item.filename] = item
 | |
|                 self._info_list.append(item)
 | |
|             elif len(self._info_list) > 0:
 | |
|                 # final crc is in last block
 | |
|                 old = self._info_list[-1]
 | |
|                 old.CRC = item.CRC
 | |
|                 old._md_expect = item._md_expect
 | |
|                 old.compress_size += item.compress_size
 | |
| 
 | |
|         # parse new-style comment
 | |
|         if item.type == RAR_BLOCK_SUB and item.filename == 'CMT':
 | |
|             if item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
 | |
|                 pass
 | |
|             elif item.flags & RAR_FILE_SOLID:
 | |
|                 # file comment
 | |
|                 cmt = self._read_comment_v3(item, self._password)
 | |
|                 if len(self._info_list) > 0:
 | |
|                     old = self._info_list[-1]
 | |
|                     old.comment = cmt
 | |
|             else:
 | |
|                 # archive comment
 | |
|                 cmt = self._read_comment_v3(item, self._password)
 | |
|                 self.comment = cmt
 | |
| 
 | |
|         if item.type == RAR_BLOCK_MAIN:
 | |
|             if item.flags & RAR_MAIN_COMMENT:
 | |
|                 self.comment = item.comment
 | |
|             if item.flags & RAR_MAIN_PASSWORD:
 | |
|                 self._needs_password = True
 | |
| 
 | |
|     # put file compressed data into temporary .rar archive, and run
 | |
|     # unrar on that, thus avoiding unrar going over whole archive
 | |
|     def _open_hack(self, inf, psw):
 | |
|         # create main header: crc, type, flags, size, res1, res2
 | |
|         prefix = RAR_ID + S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + ZERO * (2 + 4)
 | |
|         return self._open_hack_core(inf, psw, prefix, EMPTY)
 | |
| 
 | |
| #
 | |
| # RAR5 format
 | |
| #
 | |
| 
 | |
| class Rar5Info(RarInfo):
 | |
|     """Shared fields for RAR5 records.
 | |
|     """
 | |
|     extract_version = 50
 | |
|     header_crc = None
 | |
|     header_size = None
 | |
|     header_offset = None
 | |
|     data_offset = None
 | |
| 
 | |
|     # type=all
 | |
|     block_type = None
 | |
|     block_flags = None
 | |
|     add_size = 0
 | |
|     block_extra_size = 0
 | |
| 
 | |
|     # type=MAIN
 | |
|     volume_number = None
 | |
|     _md_class = None
 | |
|     _md_expect = None
 | |
| 
 | |
|     def _must_disable_hack(self):
 | |
|         return False
 | |
| 
 | |
| 
 | |
| class Rar5BaseFile(Rar5Info):
 | |
|     """Shared sturct for file & service record.
 | |
|     """
 | |
|     type = -1
 | |
|     file_flags = None
 | |
|     file_encryption = (0, 0, 0, EMPTY, EMPTY, EMPTY)
 | |
|     file_compress_flags = None
 | |
|     file_redir = None
 | |
|     file_owner = None
 | |
|     file_version = None
 | |
|     blake2sp_hash = None
 | |
| 
 | |
|     def _must_disable_hack(self):
 | |
|         if self.flags & RAR_FILE_PASSWORD:
 | |
|             return True
 | |
|         if self.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER):
 | |
|             return True
 | |
|         if self.file_compress_flags & RAR5_COMPR_SOLID:
 | |
|             return True
 | |
|         if self.file_redir:
 | |
|             return True
 | |
|         return False
 | |
| 
 | |
| 
 | |
| class Rar5FileInfo(Rar5BaseFile):
 | |
|     """RAR5 file record.
 | |
|     """
 | |
|     type = RAR_BLOCK_FILE
 | |
| 
 | |
| 
 | |
| class Rar5ServiceInfo(Rar5BaseFile):
 | |
|     """RAR5 service record.
 | |
|     """
 | |
|     type = RAR_BLOCK_SUB
 | |
| 
 | |
| 
 | |
| class Rar5MainInfo(Rar5Info):
 | |
|     """RAR5 archive main record.
 | |
|     """
 | |
|     type = RAR_BLOCK_MAIN
 | |
|     main_flags = None
 | |
|     main_volume_number = None
 | |
| 
 | |
|     def _must_disable_hack(self):
 | |
|         if self.main_flags & RAR5_MAIN_FLAG_SOLID:
 | |
|             return True
 | |
|         return False
 | |
| 
 | |
| 
 | |
| class Rar5EncryptionInfo(Rar5Info):
 | |
|     """RAR5 archive header encryption record.
 | |
|     """
 | |
|     type = RAR5_BLOCK_ENCRYPTION
 | |
|     encryption_algo = None
 | |
|     encryption_flags = None
 | |
|     encryption_kdf_count = None
 | |
|     encryption_salt = None
 | |
|     encryption_check_value = None
 | |
| 
 | |
|     def needs_password(self):
 | |
|         return True
 | |
| 
 | |
| 
 | |
| class Rar5EndArcInfo(Rar5Info):
 | |
|     """RAR5 end of archive record.
 | |
|     """
 | |
|     type = RAR_BLOCK_ENDARC
 | |
|     endarc_flags = None
 | |
| 
 | |
| 
 | |
| class RAR5Parser(CommonParser):
 | |
|     """Parse RAR5 format.
 | |
|     """
 | |
|     _expect_sig = RAR5_ID
 | |
|     _hdrenc_main = None
 | |
| 
 | |
|     # AES encrypted headers
 | |
|     _last_aes256_key = (-1, None, None)   # (kdf_count, salt, key)
 | |
| 
 | |
|     def _gen_key(self, kdf_count, salt):
 | |
|         if self._last_aes256_key[:2] == (kdf_count, salt):
 | |
|             return self._last_aes256_key[2]
 | |
|         if kdf_count > 24:
 | |
|             raise BadRarFile('Too large kdf_count')
 | |
|         psw = self._password
 | |
|         if isinstance(psw, unicode):
 | |
|             psw = psw.encode('utf8')
 | |
|         key = pbkdf2_sha256(psw, salt, 1 << kdf_count)
 | |
|         self._last_aes256_key = (kdf_count, salt, key)
 | |
|         return key
 | |
| 
 | |
|     def _decrypt_header(self, fd):
 | |
|         if not _have_crypto:
 | |
|             raise NoCrypto('Cannot parse encrypted headers - no crypto')
 | |
|         h = self._hdrenc_main
 | |
|         key = self._gen_key(h.encryption_kdf_count, h.encryption_salt)
 | |
|         iv = fd.read(16)
 | |
|         return HeaderDecrypt(fd, key, iv)
 | |
| 
 | |
|     # common header
 | |
|     def _parse_block_header(self, fd):
 | |
|         header_offset = fd.tell()
 | |
| 
 | |
|         preload = 4 + 3
 | |
|         start_bytes = fd.read(preload)
 | |
|         header_crc, pos = load_le32(start_bytes, 0)
 | |
|         hdrlen, pos = load_vint(start_bytes, pos)
 | |
|         if hdrlen > 2 * 1024 * 1024:
 | |
|             return None
 | |
|         header_size = pos + hdrlen
 | |
| 
 | |
|         # read full header, check for EOF
 | |
|         hdata = start_bytes + fd.read(header_size - len(start_bytes))
 | |
|         if len(hdata) != header_size:
 | |
|             self._set_error('Unexpected EOF when reading header')
 | |
|             return None
 | |
|         data_offset = fd.tell()
 | |
| 
 | |
|         calc_crc = rar_crc32(memoryview(hdata)[4:])
 | |
|         if header_crc != calc_crc:
 | |
|             # header parsing failed.
 | |
|             self._set_error('Header CRC error: exp=%x got=%x (xlen = %d)',
 | |
|                             header_crc, calc_crc, len(hdata))
 | |
|             return None
 | |
| 
 | |
|         block_type, pos = load_vint(hdata, pos)
 | |
| 
 | |
|         if block_type == RAR5_BLOCK_MAIN:
 | |
|             h, pos = self._parse_block_common(Rar5MainInfo(), hdata)
 | |
|             h = self._parse_main_block(h, hdata, pos)
 | |
|         elif block_type == RAR5_BLOCK_FILE:
 | |
|             h, pos = self._parse_block_common(Rar5FileInfo(), hdata)
 | |
|             h = self._parse_file_block(h, hdata, pos)
 | |
|         elif block_type == RAR5_BLOCK_SERVICE:
 | |
|             h, pos = self._parse_block_common(Rar5ServiceInfo(), hdata)
 | |
|             h = self._parse_file_block(h, hdata, pos)
 | |
|         elif block_type == RAR5_BLOCK_ENCRYPTION:
 | |
|             h, pos = self._parse_block_common(Rar5EncryptionInfo(), hdata)
 | |
|             h = self._parse_encryption_block(h, hdata, pos)
 | |
|         elif block_type == RAR5_BLOCK_ENDARC:
 | |
|             h, pos = self._parse_block_common(Rar5EndArcInfo(), hdata)
 | |
|             h = self._parse_endarc_block(h, hdata, pos)
 | |
|         else:
 | |
|             h = None
 | |
|         if h:
 | |
|             h.header_offset = header_offset
 | |
|             h.data_offset = data_offset
 | |
|         return h
 | |
| 
 | |
|     def _parse_block_common(self, h, hdata):
 | |
|         h.header_crc, pos = load_le32(hdata, 0)
 | |
|         hdrlen, pos = load_vint(hdata, pos)
 | |
|         h.header_size = hdrlen + pos
 | |
|         h.block_type, pos = load_vint(hdata, pos)
 | |
|         h.block_flags, pos = load_vint(hdata, pos)
 | |
| 
 | |
|         if h.block_flags & RAR5_BLOCK_FLAG_EXTRA_DATA:
 | |
|             h.block_extra_size, pos = load_vint(hdata, pos)
 | |
|         if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA:
 | |
|             h.add_size, pos = load_vint(hdata, pos)
 | |
| 
 | |
|         h.compress_size = h.add_size
 | |
| 
 | |
|         if h.block_flags & RAR5_BLOCK_FLAG_SKIP_IF_UNKNOWN:
 | |
|             h.flags |= RAR_SKIP_IF_UNKNOWN
 | |
|         if h.block_flags & RAR5_BLOCK_FLAG_DATA_AREA:
 | |
|             h.flags |= RAR_LONG_BLOCK
 | |
|         return h, pos
 | |
| 
 | |
|     def _parse_main_block(self, h, hdata, pos):
 | |
|         h.main_flags, pos = load_vint(hdata, pos)
 | |
|         if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR:
 | |
|             h.main_volume_number = load_vint(hdata, pos)
 | |
| 
 | |
|         h.flags |= RAR_MAIN_NEWNUMBERING
 | |
|         if h.main_flags & RAR5_MAIN_FLAG_SOLID:
 | |
|             h.flags |= RAR_MAIN_SOLID
 | |
|         if h.main_flags & RAR5_MAIN_FLAG_ISVOL:
 | |
|             h.flags |= RAR_MAIN_VOLUME
 | |
|         if h.main_flags & RAR5_MAIN_FLAG_RECOVERY:
 | |
|             h.flags |= RAR_MAIN_RECOVERY
 | |
|         if self._hdrenc_main:
 | |
|             h.flags |= RAR_MAIN_PASSWORD
 | |
|         if h.main_flags & RAR5_MAIN_FLAG_HAS_VOLNR == 0:
 | |
|             h.flags |= RAR_MAIN_FIRSTVOLUME
 | |
| 
 | |
|         return h
 | |
| 
 | |
|     def _parse_file_block(self, h, hdata, pos):
 | |
|         h.file_flags, pos = load_vint(hdata, pos)
 | |
|         h.file_size, pos = load_vint(hdata, pos)
 | |
|         h.mode, pos = load_vint(hdata, pos)
 | |
| 
 | |
|         if h.file_flags & RAR5_FILE_FLAG_HAS_MTIME:
 | |
|             h.mtime, pos = load_unixtime(hdata, pos)
 | |
|             h.date_time = h.mtime.timetuple()[:6]
 | |
|         if h.file_flags & RAR5_FILE_FLAG_HAS_CRC32:
 | |
|             h.CRC, pos = load_le32(hdata, pos)
 | |
|             h._md_class = CRC32Context
 | |
|             h._md_expect = h.CRC
 | |
| 
 | |
|         h.file_compress_flags, pos = load_vint(hdata, pos)
 | |
|         h.file_host_os, pos = load_vint(hdata, pos)
 | |
|         h.orig_filename, pos = load_vstr(hdata, pos)
 | |
|         h.filename = h.orig_filename.decode('utf8', 'replace')
 | |
| 
 | |
|         # use compatible values
 | |
|         if h.file_host_os == RAR5_OS_WINDOWS:
 | |
|             h.host_os = RAR_OS_WIN32
 | |
|         else:
 | |
|             h.host_os = RAR_OS_UNIX
 | |
|         h.compress_type = RAR_M0 + ((h.file_compress_flags >> 7) & 7)
 | |
| 
 | |
|         if h.block_extra_size:
 | |
|             # allow 1 byte of garbage
 | |
|             while pos < len(hdata) - 1:
 | |
|                 xsize, pos = load_vint(hdata, pos)
 | |
|                 xdata, pos = load_bytes(hdata, xsize, pos)
 | |
|                 self._process_file_extra(h, xdata)
 | |
| 
 | |
|         if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE:
 | |
|             h.flags |= RAR_FILE_SPLIT_BEFORE
 | |
|         if h.block_flags & RAR5_BLOCK_FLAG_SPLIT_AFTER:
 | |
|             h.flags |= RAR_FILE_SPLIT_AFTER
 | |
|         if h.file_flags & RAR5_FILE_FLAG_ISDIR:
 | |
|             h.flags |= RAR_FILE_DIRECTORY
 | |
|         if h.file_compress_flags & RAR5_COMPR_SOLID:
 | |
|             h.flags |= RAR_FILE_SOLID
 | |
| 
 | |
|         return h
 | |
| 
 | |
|     def _parse_endarc_block(self, h, hdata, pos):
 | |
|         h.endarc_flags, pos = load_vint(hdata, pos)
 | |
|         if h.endarc_flags & RAR5_ENDARC_FLAG_NEXT_VOL:
 | |
|             h.flags |= RAR_ENDARC_NEXT_VOLUME
 | |
|         return h
 | |
| 
 | |
|     def _parse_encryption_block(self, h, hdata, pos):
 | |
|         h.encryption_algo, pos = load_vint(hdata, pos)
 | |
|         h.encryption_flags, pos = load_vint(hdata, pos)
 | |
|         h.encryption_kdf_count, pos = load_byte(hdata, pos)
 | |
|         h.encryption_salt, pos = load_bytes(hdata, 16, pos)
 | |
|         if h.encryption_flags & RAR5_ENC_FLAG_HAS_CHECKVAL:
 | |
|             h.encryption_check_value = load_bytes(hdata, 12, pos)
 | |
|         if h.encryption_algo != RAR5_XENC_CIPHER_AES256:
 | |
|             raise BadRarFile('Unsupported header encryption cipher')
 | |
|         self._hdrenc_main = h
 | |
|         return h
 | |
| 
 | |
|     # file extra record
 | |
|     def _process_file_extra(self, h, xdata):
 | |
|         xtype, pos = load_vint(xdata, 0)
 | |
|         if xtype == RAR5_XFILE_TIME:
 | |
|             self._parse_file_xtime(h, xdata, pos)
 | |
|         elif xtype == RAR5_XFILE_ENCRYPTION:
 | |
|             self._parse_file_encryption(h, xdata, pos)
 | |
|         elif xtype == RAR5_XFILE_HASH:
 | |
|             self._parse_file_hash(h, xdata, pos)
 | |
|         elif xtype == RAR5_XFILE_VERSION:
 | |
|             self._parse_file_version(h, xdata, pos)
 | |
|         elif xtype == RAR5_XFILE_REDIR:
 | |
|             self._parse_file_redir(h, xdata, pos)
 | |
|         elif xtype == RAR5_XFILE_OWNER:
 | |
|             self._parse_file_owner(h, xdata, pos)
 | |
|         elif xtype == RAR5_XFILE_SERVICE:
 | |
|             pass
 | |
|         else:
 | |
|             pass
 | |
| 
 | |
|     # extra block for file time record
 | |
|     def _parse_file_xtime(self, h, xdata, pos):
 | |
|         tflags, pos = load_vint(xdata, pos)
 | |
|         ldr = load_windowstime
 | |
|         if tflags & RAR5_XTIME_UNIXTIME:
 | |
|             ldr = load_unixtime
 | |
|         if tflags & RAR5_XTIME_HAS_MTIME:
 | |
|             h.mtime, pos = ldr(xdata, pos)
 | |
|             h.date_time = h.mtime.timetuple()[:6]
 | |
|         if tflags & RAR5_XTIME_HAS_CTIME:
 | |
|             h.ctime, pos = ldr(xdata, pos)
 | |
|         if tflags & RAR5_XTIME_HAS_ATIME:
 | |
|             h.atime, pos = ldr(xdata, pos)
 | |
| 
 | |
|     # just remember encryption info
 | |
|     def _parse_file_encryption(self, h, xdata, pos):
 | |
|         algo, pos = load_vint(xdata, pos)
 | |
|         flags, pos = load_vint(xdata, pos)
 | |
|         kdf_count, pos = load_byte(xdata, pos)
 | |
|         salt, pos = load_bytes(xdata, 16, pos)
 | |
|         iv, pos = load_bytes(xdata, 16, pos)
 | |
|         checkval = None
 | |
|         if flags & RAR5_XENC_CHECKVAL:
 | |
|             checkval, pos = load_bytes(xdata, 12, pos)
 | |
|         if flags & RAR5_XENC_TWEAKED:
 | |
|             h._md_expect = None
 | |
|             h._md_class = NoHashContext
 | |
| 
 | |
|         h.file_encryption = (algo, flags, kdf_count, salt, iv, checkval)
 | |
|         h.flags |= RAR_FILE_PASSWORD
 | |
| 
 | |
|     def _parse_file_hash(self, h, xdata, pos):
 | |
|         hash_type, pos = load_vint(xdata, pos)
 | |
|         if hash_type == RAR5_XHASH_BLAKE2SP:
 | |
|             h.blake2sp_hash, pos = load_bytes(xdata, 32, pos)
 | |
|             if _have_blake2 and (h.file_encryption[1] & RAR5_XENC_TWEAKED) == 0:
 | |
|                 h._md_class = Blake2SP
 | |
|                 h._md_expect = h.blake2sp_hash
 | |
| 
 | |
|     def _parse_file_version(self, h, xdata, pos):
 | |
|         flags, pos = load_vint(xdata, pos)
 | |
|         version, pos = load_vint(xdata, pos)
 | |
|         h.file_version = (flags, version)
 | |
| 
 | |
|     def _parse_file_redir(self, h, xdata, pos):
 | |
|         redir_type, pos = load_vint(xdata, pos)
 | |
|         redir_flags, pos = load_vint(xdata, pos)
 | |
|         redir_name, pos = load_vstr(xdata, pos)
 | |
|         redir_name = redir_name.decode('utf8', 'replace')
 | |
|         h.file_redir = (redir_type, redir_flags, redir_name)
 | |
| 
 | |
|     def _parse_file_owner(self, h, xdata, pos):
 | |
|         user_name = group_name = user_id = group_id = None
 | |
| 
 | |
|         flags, pos = load_vint(xdata, pos)
 | |
|         if flags & RAR5_XOWNER_UNAME:
 | |
|             user_name, pos = load_vstr(xdata, pos)
 | |
|         if flags & RAR5_XOWNER_GNAME:
 | |
|             group_name, pos = load_vstr(xdata, pos)
 | |
|         if flags & RAR5_XOWNER_UID:
 | |
|             user_id, pos = load_vint(xdata, pos)
 | |
|         if flags & RAR5_XOWNER_GID:
 | |
|             group_id, pos = load_vint(xdata, pos)
 | |
| 
 | |
|         h.file_owner = (user_name, group_name, user_id, group_id)
 | |
| 
 | |
|     def process_entry(self, fd, item):
 | |
|         if item.block_type == RAR5_BLOCK_FILE:
 | |
|             # use only first part
 | |
|             if (item.block_flags & RAR5_BLOCK_FLAG_SPLIT_BEFORE) == 0:
 | |
|                 self._info_map[item.filename] = item
 | |
|                 self._info_list.append(item)
 | |
|             elif len(self._info_list) > 0:
 | |
|                 # final crc is in last block
 | |
|                 old = self._info_list[-1]
 | |
|                 old.CRC = item.CRC
 | |
|                 old._md_expect = item._md_expect
 | |
|                 old.blake2sp_hash = item.blake2sp_hash
 | |
|                 old.compress_size += item.compress_size
 | |
|         elif item.block_type == RAR5_BLOCK_SERVICE:
 | |
|             if item.filename == 'CMT':
 | |
|                 self._load_comment(fd, item)
 | |
| 
 | |
|     def _load_comment(self, fd, item):
 | |
|         if item.block_flags & (RAR5_BLOCK_FLAG_SPLIT_BEFORE | RAR5_BLOCK_FLAG_SPLIT_AFTER):
 | |
|             return None
 | |
|         if item.compress_type != RAR_M0:
 | |
|             return None
 | |
| 
 | |
|         if item.flags & RAR_FILE_PASSWORD:
 | |
|             algo, ___flags, kdf_count, salt, iv, ___checkval = item.file_encryption
 | |
|             if algo != RAR5_XENC_CIPHER_AES256:
 | |
|                 return None
 | |
|             key = self._gen_key(kdf_count, salt)
 | |
|             f = HeaderDecrypt(fd, key, iv)
 | |
|             cmt = f.read(item.file_size)
 | |
|         else:
 | |
|             # archive comment
 | |
|             with self._open_clear(item) as cmtstream:
 | |
|                 cmt = cmtstream.read()
 | |
| 
 | |
|         # rar bug? - appends zero to comment
 | |
|         cmt = cmt.split(ZERO, 1)[0]
 | |
|         self.comment = cmt.decode('utf8')
 | |
| 
 | |
|     def _open_hack(self, inf, psw):
 | |
|         # len, type, blk_flags, flags
 | |
|         main_hdr = b'\x03\x01\x00\x00'
 | |
|         endarc_hdr = b'\x03\x05\x00\x00'
 | |
|         main_hdr = S_LONG.pack(rar_crc32(main_hdr)) + main_hdr
 | |
|         endarc_hdr = S_LONG.pack(rar_crc32(endarc_hdr)) + endarc_hdr
 | |
|         return self._open_hack_core(inf, psw, RAR5_ID + main_hdr, endarc_hdr)
 | |
| 
 | |
| ##
 | |
| ## Utility classes
 | |
| ##
 | |
| 
 | |
| class UnicodeFilename(object):
 | |
|     """Handle RAR3 unicode filename decompression.
 | |
|     """
 | |
|     def __init__(self, name, encdata):
 | |
|         self.std_name = bytearray(name)
 | |
|         self.encdata = bytearray(encdata)
 | |
|         self.pos = self.encpos = 0
 | |
|         self.buf = bytearray()
 | |
|         self.failed = 0
 | |
| 
 | |
|     def enc_byte(self):
 | |
|         """Copy encoded byte."""
 | |
|         try:
 | |
|             c = self.encdata[self.encpos]
 | |
|             self.encpos += 1
 | |
|             return c
 | |
|         except IndexError:
 | |
|             self.failed = 1
 | |
|             return 0
 | |
| 
 | |
|     def std_byte(self):
 | |
|         """Copy byte from 8-bit representation."""
 | |
|         try:
 | |
|             return self.std_name[self.pos]
 | |
|         except IndexError:
 | |
|             self.failed = 1
 | |
|             return ord('?')
 | |
| 
 | |
|     def put(self, lo, hi):
 | |
|         """Copy 16-bit value to result."""
 | |
|         self.buf.append(lo)
 | |
|         self.buf.append(hi)
 | |
|         self.pos += 1
 | |
| 
 | |
|     def decode(self):
 | |
|         """Decompress compressed UTF16 value."""
 | |
|         hi = self.enc_byte()
 | |
|         flagbits = 0
 | |
|         while self.encpos < len(self.encdata):
 | |
|             if flagbits == 0:
 | |
|                 flags = self.enc_byte()
 | |
|                 flagbits = 8
 | |
|             flagbits -= 2
 | |
|             t = (flags >> flagbits) & 3
 | |
|             if t == 0:
 | |
|                 self.put(self.enc_byte(), 0)
 | |
|             elif t == 1:
 | |
|                 self.put(self.enc_byte(), hi)
 | |
|             elif t == 2:
 | |
|                 self.put(self.enc_byte(), self.enc_byte())
 | |
|             else:
 | |
|                 n = self.enc_byte()
 | |
|                 if n & 0x80:
 | |
|                     c = self.enc_byte()
 | |
|                     for _ in range((n & 0x7f) + 2):
 | |
|                         lo = (self.std_byte() + c) & 0xFF
 | |
|                         self.put(lo, hi)
 | |
|                 else:
 | |
|                     for _ in range(n + 2):
 | |
|                         self.put(self.std_byte(), 0)
 | |
|         return self.buf.decode("utf-16le", "replace")
 | |
| 
 | |
| 
 | |
| class RarExtFile(RawIOBase):
 | |
|     """Base class for file-like object that :meth:`RarFile.open` returns.
 | |
| 
 | |
|     Provides public methods and common crc checking.
 | |
| 
 | |
|     Behaviour:
 | |
|      - no short reads - .read() and .readinfo() read as much as requested.
 | |
|      - no internal buffer, use io.BufferedReader for that.
 | |
|     """
 | |
| 
 | |
|     #: Filename of the archive entry
 | |
|     name = None
 | |
| 
 | |
|     def __init__(self, parser, inf):
 | |
|         """Open archive entry.
 | |
|         """
 | |
|         super(RarExtFile, self).__init__()
 | |
| 
 | |
|         # standard io.* properties
 | |
|         self.name = inf.filename
 | |
|         self.mode = 'rb'
 | |
| 
 | |
|         self._parser = parser
 | |
|         self._inf = inf
 | |
|         self._fd = None
 | |
|         self._remain = 0
 | |
|         self._returncode = 0
 | |
| 
 | |
|         self._md_context = None
 | |
| 
 | |
|         self._open()
 | |
| 
 | |
|     def _open(self):
 | |
|         if self._fd:
 | |
|             self._fd.close()
 | |
|         md_class = self._inf._md_class or NoHashContext
 | |
|         self._md_context = md_class()
 | |
|         self._fd = None
 | |
|         self._remain = self._inf.file_size
 | |
| 
 | |
|     def read(self, cnt=None):
 | |
|         """Read all or specified amount of data from archive entry."""
 | |
| 
 | |
|         # sanitize cnt
 | |
|         if cnt is None or cnt < 0:
 | |
|             cnt = self._remain
 | |
|         elif cnt > self._remain:
 | |
|             cnt = self._remain
 | |
|         if cnt == 0:
 | |
|             return EMPTY
 | |
| 
 | |
|         # actual read
 | |
|         data = self._read(cnt)
 | |
|         if data:
 | |
|             self._md_context.update(data)
 | |
|             self._remain -= len(data)
 | |
|         if len(data) != cnt:
 | |
|             raise BadRarFile("Failed the read enough data")
 | |
| 
 | |
|         # done?
 | |
|         if not data or self._remain == 0:
 | |
|             # self.close()
 | |
|             self._check()
 | |
|         return data
 | |
| 
 | |
|     def _check(self):
 | |
|         """Check final CRC."""
 | |
|         final = self._md_context.digest()
 | |
|         exp = self._inf._md_expect
 | |
|         if exp is None:
 | |
|             return
 | |
|         if final is None:
 | |
|             return
 | |
|         if self._returncode:
 | |
|             check_returncode(self, '')
 | |
|         if self._remain != 0:
 | |
|             raise BadRarFile("Failed the read enough data")
 | |
|         if final != exp:
 | |
|             raise BadRarFile("Corrupt file - CRC check failed: %s - exp=%r got=%r" % (
 | |
|                 self._inf.filename, exp, final))
 | |
| 
 | |
|     def _read(self, cnt):
 | |
|         """Actual read that gets sanitized cnt."""
 | |
| 
 | |
|     def close(self):
 | |
|         """Close open resources."""
 | |
| 
 | |
|         super(RarExtFile, self).close()
 | |
| 
 | |
|         if self._fd:
 | |
|             self._fd.close()
 | |
|             self._fd = None
 | |
| 
 | |
|     def __del__(self):
 | |
|         """Hook delete to make sure tempfile is removed."""
 | |
|         self.close()
 | |
| 
 | |
|     def readinto(self, buf):
 | |
|         """Zero-copy read directly into buffer.
 | |
| 
 | |
|         Returns bytes read.
 | |
|         """
 | |
|         raise NotImplementedError('readinto')
 | |
| 
 | |
|     def tell(self):
 | |
|         """Return current reading position in uncompressed data."""
 | |
|         return self._inf.file_size - self._remain
 | |
| 
 | |
|     def seek(self, ofs, whence=0):
 | |
|         """Seek in data.
 | |
| 
 | |
|         On uncompressed files, the seeking works by actual
 | |
|         seeks so it's fast.  On compresses files its slow
 | |
|         - forward seeking happends by reading ahead,
 | |
|         backwards by re-opening and decompressing from the start.
 | |
|         """
 | |
| 
 | |
|         # disable crc check when seeking
 | |
|         self._md_context = NoHashContext()
 | |
| 
 | |
|         fsize = self._inf.file_size
 | |
|         cur_ofs = self.tell()
 | |
| 
 | |
|         if whence == 0:     # seek from beginning of file
 | |
|             new_ofs = ofs
 | |
|         elif whence == 1:   # seek from current position
 | |
|             new_ofs = cur_ofs + ofs
 | |
|         elif whence == 2:   # seek from end of file
 | |
|             new_ofs = fsize + ofs
 | |
|         else:
 | |
|             raise ValueError('Invalid value for whence')
 | |
| 
 | |
|         # sanity check
 | |
|         if new_ofs < 0:
 | |
|             new_ofs = 0
 | |
|         elif new_ofs > fsize:
 | |
|             new_ofs = fsize
 | |
| 
 | |
|         # do the actual seek
 | |
|         if new_ofs >= cur_ofs:
 | |
|             self._skip(new_ofs - cur_ofs)
 | |
|         else:
 | |
|             # reopen and seek
 | |
|             self._open()
 | |
|             self._skip(new_ofs)
 | |
|         return self.tell()
 | |
| 
 | |
|     def _skip(self, cnt):
 | |
|         """Read and discard data"""
 | |
|         while cnt > 0:
 | |
|             if cnt > 8192:
 | |
|                 buf = self.read(8192)
 | |
|             else:
 | |
|                 buf = self.read(cnt)
 | |
|             if not buf:
 | |
|                 break
 | |
|             cnt -= len(buf)
 | |
| 
 | |
|     def readable(self):
 | |
|         """Returns True"""
 | |
|         return True
 | |
| 
 | |
|     def writable(self):
 | |
|         """Returns False.
 | |
| 
 | |
|         Writing is not supported.
 | |
|         """
 | |
|         return False
 | |
| 
 | |
|     def seekable(self):
 | |
|         """Returns True.
 | |
| 
 | |
|         Seeking is supported, although it's slow on compressed files.
 | |
|         """
 | |
|         return True
 | |
| 
 | |
|     def readall(self):
 | |
|         """Read all remaining data"""
 | |
|         # avoid RawIOBase default impl
 | |
|         return self.read()
 | |
| 
 | |
| 
 | |
| class PipeReader(RarExtFile):
 | |
|     """Read data from pipe, handle tempfile cleanup."""
 | |
| 
 | |
|     def __init__(self, rf, inf, cmd, tempfile=None):
 | |
|         self._cmd = cmd
 | |
|         self._proc = None
 | |
|         self._tempfile = tempfile
 | |
|         super(PipeReader, self).__init__(rf, inf)
 | |
| 
 | |
|     def _close_proc(self):
 | |
|         if not self._proc:
 | |
|             return
 | |
|         if self._proc.stdout:
 | |
|             self._proc.stdout.close()
 | |
|         if self._proc.stdin:
 | |
|             self._proc.stdin.close()
 | |
|         if self._proc.stderr:
 | |
|             self._proc.stderr.close()
 | |
|         self._proc.wait()
 | |
|         self._returncode = self._proc.returncode
 | |
|         self._proc = None
 | |
| 
 | |
|     def _open(self):
 | |
|         super(PipeReader, self)._open()
 | |
| 
 | |
|         # stop old process
 | |
|         self._close_proc()
 | |
| 
 | |
|         # launch new process
 | |
|         self._returncode = 0
 | |
|         self._proc = custom_popen(self._cmd)
 | |
|         self._fd = self._proc.stdout
 | |
| 
 | |
|         # avoid situation where unrar waits on stdin
 | |
|         if self._proc.stdin:
 | |
|             self._proc.stdin.close()
 | |
| 
 | |
|     def _read(self, cnt):
 | |
|         """Read from pipe."""
 | |
| 
 | |
|         # normal read is usually enough
 | |
|         data = self._fd.read(cnt)
 | |
|         if len(data) == cnt or not data:
 | |
|             return data
 | |
| 
 | |
|         # short read, try looping
 | |
|         buf = [data]
 | |
|         cnt -= len(data)
 | |
|         while cnt > 0:
 | |
|             data = self._fd.read(cnt)
 | |
|             if not data:
 | |
|                 break
 | |
|             cnt -= len(data)
 | |
|             buf.append(data)
 | |
|         return EMPTY.join(buf)
 | |
| 
 | |
|     def close(self):
 | |
|         """Close open resources."""
 | |
| 
 | |
|         self._close_proc()
 | |
|         super(PipeReader, self).close()
 | |
| 
 | |
|         if self._tempfile:
 | |
|             try:
 | |
|                 os.unlink(self._tempfile)
 | |
|             except OSError:
 | |
|                 pass
 | |
|             self._tempfile = None
 | |
| 
 | |
|     def readinto(self, buf):
 | |
|         """Zero-copy read directly into buffer."""
 | |
|         cnt = len(buf)
 | |
|         if cnt > self._remain:
 | |
|             cnt = self._remain
 | |
|         vbuf = memoryview(buf)
 | |
|         res = got = 0
 | |
|         while got < cnt:
 | |
|             res = self._fd.readinto(vbuf[got : cnt])
 | |
|             if not res:
 | |
|                 break
 | |
|             self._md_context.update(vbuf[got : got + res])
 | |
|             self._remain -= res
 | |
|             got += res
 | |
|         return got
 | |
| 
 | |
| 
 | |
| class DirectReader(RarExtFile):
 | |
|     """Read uncompressed data directly from archive.
 | |
|     """
 | |
|     _cur = None
 | |
|     _cur_avail = None
 | |
|     _volfile = None
 | |
| 
 | |
|     def _open(self):
 | |
|         super(DirectReader, self)._open()
 | |
| 
 | |
|         self._volfile = self._inf.volume_file
 | |
|         self._fd = XFile(self._volfile, 0)
 | |
|         self._fd.seek(self._inf.header_offset, 0)
 | |
|         self._cur = self._parser._parse_header(self._fd)
 | |
|         self._cur_avail = self._cur.add_size
 | |
| 
 | |
|     def _skip(self, cnt):
 | |
|         """RAR Seek, skipping through rar files to get to correct position
 | |
|         """
 | |
| 
 | |
|         while cnt > 0:
 | |
|             # next vol needed?
 | |
|             if self._cur_avail == 0:
 | |
|                 if not self._open_next():
 | |
|                     break
 | |
| 
 | |
|             # fd is in read pos, do the read
 | |
|             if cnt > self._cur_avail:
 | |
|                 cnt -= self._cur_avail
 | |
|                 self._remain -= self._cur_avail
 | |
|                 self._cur_avail = 0
 | |
|             else:
 | |
|                 self._fd.seek(cnt, 1)
 | |
|                 self._cur_avail -= cnt
 | |
|                 self._remain -= cnt
 | |
|                 cnt = 0
 | |
| 
 | |
|     def _read(self, cnt):
 | |
|         """Read from potentially multi-volume archive."""
 | |
| 
 | |
|         buf = []
 | |
|         while cnt > 0:
 | |
|             # next vol needed?
 | |
|             if self._cur_avail == 0:
 | |
|                 if not self._open_next():
 | |
|                     break
 | |
| 
 | |
|             # fd is in read pos, do the read
 | |
|             if cnt > self._cur_avail:
 | |
|                 data = self._fd.read(self._cur_avail)
 | |
|             else:
 | |
|                 data = self._fd.read(cnt)
 | |
|             if not data:
 | |
|                 break
 | |
| 
 | |
|             # got some data
 | |
|             cnt -= len(data)
 | |
|             self._cur_avail -= len(data)
 | |
|             buf.append(data)
 | |
| 
 | |
|         if len(buf) == 1:
 | |
|             return buf[0]
 | |
|         return EMPTY.join(buf)
 | |
| 
 | |
|     def _open_next(self):
 | |
|         """Proceed to next volume."""
 | |
| 
 | |
|         # is the file split over archives?
 | |
|         if (self._cur.flags & RAR_FILE_SPLIT_AFTER) == 0:
 | |
|             return False
 | |
| 
 | |
|         if self._fd:
 | |
|             self._fd.close()
 | |
|             self._fd = None
 | |
| 
 | |
|         # open next part
 | |
|         self._volfile = self._parser._next_volname(self._volfile)
 | |
|         fd = open(self._volfile, "rb", 0)
 | |
|         self._fd = fd
 | |
|         sig = fd.read(len(self._parser._expect_sig))
 | |
|         if sig != self._parser._expect_sig:
 | |
|             raise BadRarFile("Invalid signature")
 | |
| 
 | |
|         # loop until first file header
 | |
|         while 1:
 | |
|             cur = self._parser._parse_header(fd)
 | |
|             if not cur:
 | |
|                 raise BadRarFile("Unexpected EOF")
 | |
|             if cur.type in (RAR_BLOCK_MARK, RAR_BLOCK_MAIN):
 | |
|                 if cur.add_size:
 | |
|                     fd.seek(cur.add_size, 1)
 | |
|                 continue
 | |
|             if cur.orig_filename != self._inf.orig_filename:
 | |
|                 raise BadRarFile("Did not found file entry")
 | |
|             self._cur = cur
 | |
|             self._cur_avail = cur.add_size
 | |
|             return True
 | |
| 
 | |
|     def readinto(self, buf):
 | |
|         """Zero-copy read directly into buffer."""
 | |
|         got = 0
 | |
|         vbuf = memoryview(buf)
 | |
|         while got < len(buf):
 | |
|             # next vol needed?
 | |
|             if self._cur_avail == 0:
 | |
|                 if not self._open_next():
 | |
|                     break
 | |
| 
 | |
|             # length for next read
 | |
|             cnt = len(buf) - got
 | |
|             if cnt > self._cur_avail:
 | |
|                 cnt = self._cur_avail
 | |
| 
 | |
|             # read into temp view
 | |
|             res = self._fd.readinto(vbuf[got : got + cnt])
 | |
|             if not res:
 | |
|                 break
 | |
|             self._md_context.update(vbuf[got : got + res])
 | |
|             self._cur_avail -= res
 | |
|             self._remain -= res
 | |
|             got += res
 | |
|         return got
 | |
| 
 | |
| 
 | |
| class HeaderDecrypt(object):
 | |
|     """File-like object that decrypts from another file"""
 | |
|     def __init__(self, f, key, iv):
 | |
|         self.f = f
 | |
|         self.ciph = AES_CBC_Decrypt(key, iv)
 | |
|         self.buf = EMPTY
 | |
| 
 | |
|     def tell(self):
 | |
|         """Current file pos - works only on block boundaries."""
 | |
|         return self.f.tell()
 | |
| 
 | |
|     def read(self, cnt=None):
 | |
|         """Read and decrypt."""
 | |
|         if cnt > 8 * 1024:
 | |
|             raise BadRarFile('Bad count to header decrypt - wrong password?')
 | |
| 
 | |
|         # consume old data
 | |
|         if cnt <= len(self.buf):
 | |
|             res = self.buf[:cnt]
 | |
|             self.buf = self.buf[cnt:]
 | |
|             return res
 | |
|         res = self.buf
 | |
|         self.buf = EMPTY
 | |
|         cnt -= len(res)
 | |
| 
 | |
|         # decrypt new data
 | |
|         blklen = 16
 | |
|         while cnt > 0:
 | |
|             enc = self.f.read(blklen)
 | |
|             if len(enc) < blklen:
 | |
|                 break
 | |
|             dec = self.ciph.decrypt(enc)
 | |
|             if cnt >= len(dec):
 | |
|                 res += dec
 | |
|                 cnt -= len(dec)
 | |
|             else:
 | |
|                 res += dec[:cnt]
 | |
|                 self.buf = dec[cnt:]
 | |
|                 cnt = 0
 | |
| 
 | |
|         return res
 | |
| 
 | |
| 
 | |
| # handle (filename|filelike) object
 | |
| class XFile(object):
 | |
|     """Input may be filename or file object.
 | |
|     """
 | |
|     __slots__ = ('_fd', '_need_close')
 | |
| 
 | |
|     def __init__(self, xfile, bufsize=1024):
 | |
|         if is_filelike(xfile):
 | |
|             self._need_close = False
 | |
|             self._fd = xfile
 | |
|             self._fd.seek(0)
 | |
|         else:
 | |
|             self._need_close = True
 | |
|             self._fd = open(xfile, 'rb', bufsize)
 | |
| 
 | |
|     def read(self, n=None):
 | |
|         """Read from file."""
 | |
|         return self._fd.read(n)
 | |
| 
 | |
|     def tell(self):
 | |
|         """Return file pos."""
 | |
|         return self._fd.tell()
 | |
| 
 | |
|     def seek(self, ofs, whence=0):
 | |
|         """Move file pos."""
 | |
|         return self._fd.seek(ofs, whence)
 | |
| 
 | |
|     def readinto(self, dst):
 | |
|         """Read into buffer."""
 | |
|         return self._fd.readinto(dst)
 | |
| 
 | |
|     def close(self):
 | |
|         """Close file object."""
 | |
|         if self._need_close:
 | |
|             self._fd.close()
 | |
| 
 | |
|     def __enter__(self):
 | |
|         return self
 | |
| 
 | |
|     def __exit__(self, typ, val, tb):
 | |
|         self.close()
 | |
| 
 | |
| 
 | |
| class NoHashContext(object):
 | |
|     """No-op hash function."""
 | |
|     def __init__(self, data=None):
 | |
|         """Initialize"""
 | |
|     def update(self, data):
 | |
|         """Update data"""
 | |
|     def digest(self):
 | |
|         """Final hash"""
 | |
|     def hexdigest(self):
 | |
|         """Hexadecimal digest."""
 | |
| 
 | |
| 
 | |
| class CRC32Context(object):
 | |
|     """Hash context that uses CRC32."""
 | |
|     __slots__ = ['_crc']
 | |
| 
 | |
|     def __init__(self, data=None):
 | |
|         self._crc = 0
 | |
|         if data:
 | |
|             self.update(data)
 | |
| 
 | |
|     def update(self, data):
 | |
|         """Process data."""
 | |
|         self._crc = rar_crc32(data, self._crc)
 | |
| 
 | |
|     def digest(self):
 | |
|         """Final hash."""
 | |
|         return self._crc
 | |
| 
 | |
|     def hexdigest(self):
 | |
|         """Hexadecimal digest."""
 | |
|         return '%08x' % self.digest()
 | |
| 
 | |
| 
 | |
| class Blake2SP(object):
 | |
|     """Blake2sp hash context.
 | |
|     """
 | |
|     __slots__ = ['_thread', '_buf', '_cur', '_digest']
 | |
|     digest_size = 32
 | |
|     block_size = 64
 | |
|     parallelism = 8
 | |
| 
 | |
|     def __init__(self, data=None):
 | |
|         self._buf = b''
 | |
|         self._cur = 0
 | |
|         self._digest = None
 | |
|         self._thread = []
 | |
| 
 | |
|         for i in range(self.parallelism):
 | |
|             ctx = self._blake2s(i, 0, i == (self.parallelism - 1))
 | |
|             self._thread.append(ctx)
 | |
| 
 | |
|         if data:
 | |
|             self.update(data)
 | |
| 
 | |
|     def _blake2s(self, ofs, depth, is_last):
 | |
|         return blake2s(node_offset=ofs, node_depth=depth, last_node=is_last,
 | |
|                        depth=2, inner_size=32, fanout=self.parallelism)
 | |
| 
 | |
|     def _add_block(self, blk):
 | |
|         self._thread[self._cur].update(blk)
 | |
|         self._cur = (self._cur + 1) % self.parallelism
 | |
| 
 | |
|     def update(self, data):
 | |
|         """Hash data.
 | |
|         """
 | |
|         view = memoryview(data)
 | |
|         bs = self.block_size
 | |
|         if self._buf:
 | |
|             need = bs - len(self._buf)
 | |
|             if len(view) < need:
 | |
|                 self._buf += view.tobytes()
 | |
|                 return
 | |
|             self._add_block(self._buf + view[:need].tobytes())
 | |
|             view = view[need:]
 | |
|         while len(view) >= bs:
 | |
|             self._add_block(view[:bs])
 | |
|             view = view[bs:]
 | |
|         self._buf = view.tobytes()
 | |
| 
 | |
|     def digest(self):
 | |
|         """Return final digest value.
 | |
|         """
 | |
|         if self._digest is None:
 | |
|             if self._buf:
 | |
|                 self._add_block(self._buf)
 | |
|                 self._buf = EMPTY
 | |
|             ctx = self._blake2s(0, 1, True)
 | |
|             for t in self._thread:
 | |
|                 ctx.update(t.digest())
 | |
|             self._digest = ctx.digest()
 | |
|         return self._digest
 | |
| 
 | |
|     def hexdigest(self):
 | |
|         """Hexadecimal digest."""
 | |
|         return tohex(self.digest())
 | |
| 
 | |
| 
 | |
| class Rar3Sha1(object):
 | |
|     """Bug-compat for SHA1
 | |
|     """
 | |
|     digest_size = 20
 | |
|     block_size = 64
 | |
| 
 | |
|     _BLK_BE = struct.Struct(b'>16L')
 | |
|     _BLK_LE = struct.Struct(b'<16L')
 | |
| 
 | |
|     __slots__ = ('_nbytes', '_md', '_rarbug')
 | |
| 
 | |
|     def __init__(self, data=b'', rarbug=False):
 | |
|         self._md = sha1()
 | |
|         self._nbytes = 0
 | |
|         self._rarbug = rarbug
 | |
|         self.update(data)
 | |
| 
 | |
|     def update(self, data):
 | |
|         """Process more data."""
 | |
|         self._md.update(data)
 | |
|         bufpos = self._nbytes & 63
 | |
|         self._nbytes += len(data)
 | |
| 
 | |
|         if self._rarbug and len(data) > 64:
 | |
|             dpos = self.block_size - bufpos
 | |
|             while dpos + self.block_size <= len(data):
 | |
|                 self._corrupt(data, dpos)
 | |
|                 dpos += self.block_size
 | |
| 
 | |
|     def digest(self):
 | |
|         """Return final state."""
 | |
|         return self._md.digest()
 | |
| 
 | |
|     def hexdigest(self):
 | |
|         """Return final state as hex string."""
 | |
|         return self._md.hexdigest()
 | |
| 
 | |
|     def _corrupt(self, data, dpos):
 | |
|         """Corruption from SHA1 core."""
 | |
|         ws = list(self._BLK_BE.unpack_from(data, dpos))
 | |
|         for t in range(16, 80):
 | |
|             tmp = ws[(t - 3) & 15] ^ ws[(t - 8) & 15] ^ ws[(t - 14) & 15] ^ ws[(t - 16) & 15]
 | |
|             ws[t & 15] = ((tmp << 1) | (tmp >> (32 - 1))) & 0xFFFFFFFF
 | |
|         self._BLK_LE.pack_into(data, dpos, *ws)
 | |
| 
 | |
| 
 | |
| ##
 | |
| ## Utility functions
 | |
| ##
 | |
| 
 | |
| S_LONG = Struct('<L')
 | |
| S_SHORT = Struct('<H')
 | |
| S_BYTE = Struct('<B')
 | |
| 
 | |
| S_BLK_HDR = Struct('<HBHH')
 | |
| S_FILE_HDR = Struct('<LLBLLBBHL')
 | |
| S_COMMENT_HDR = Struct('<HBBH')
 | |
| 
 | |
| def load_vint(buf, pos):
 | |
|     """Load variable-size int."""
 | |
|     limit = min(pos + 11, len(buf))
 | |
|     res = ofs = 0
 | |
|     while pos < limit:
 | |
|         b = _byte_code(buf[pos])
 | |
|         res += ((b & 0x7F) << ofs)
 | |
|         pos += 1
 | |
|         ofs += 7
 | |
|         if b < 0x80:
 | |
|             return res, pos
 | |
|     raise BadRarFile('cannot load vint')
 | |
| 
 | |
| def load_byte(buf, pos):
 | |
|     """Load single byte"""
 | |
|     end = pos + 1
 | |
|     if end > len(buf):
 | |
|         raise BadRarFile('cannot load byte')
 | |
|     return S_BYTE.unpack_from(buf, pos)[0], end
 | |
| 
 | |
| def load_le32(buf, pos):
 | |
|     """Load little-endian 32-bit integer"""
 | |
|     end = pos + 4
 | |
|     if end > len(buf):
 | |
|         raise BadRarFile('cannot load le32')
 | |
|     return S_LONG.unpack_from(buf, pos)[0], pos + 4
 | |
| 
 | |
| def load_bytes(buf, num, pos):
 | |
|     """Load sequence of bytes"""
 | |
|     end = pos + num
 | |
|     if end > len(buf):
 | |
|         raise BadRarFile('cannot load bytes')
 | |
|     return buf[pos : end], end
 | |
| 
 | |
| def load_vstr(buf, pos):
 | |
|     """Load bytes prefixed by vint length"""
 | |
|     slen, pos = load_vint(buf, pos)
 | |
|     return load_bytes(buf, slen, pos)
 | |
| 
 | |
| def load_dostime(buf, pos):
 | |
|     """Load LE32 dos timestamp"""
 | |
|     stamp, pos = load_le32(buf, pos)
 | |
|     tup = parse_dos_time(stamp)
 | |
|     return to_datetime(tup), pos
 | |
| 
 | |
| def load_unixtime(buf, pos):
 | |
|     """Load LE32 unix timestamp"""
 | |
|     secs, pos = load_le32(buf, pos)
 | |
|     dt = datetime.fromtimestamp(secs, UTC)
 | |
|     return dt, pos
 | |
| 
 | |
| def load_windowstime(buf, pos):
 | |
|     """Load LE64 windows timestamp"""
 | |
|     # unix epoch (1970) in seconds from windows epoch (1601)
 | |
|     unix_epoch = 11644473600
 | |
|     val1, pos = load_le32(buf, pos)
 | |
|     val2, pos = load_le32(buf, pos)
 | |
|     secs, n1secs = divmod((val2 << 32) | val1, 10000000)
 | |
|     dt = datetime.fromtimestamp(secs - unix_epoch, UTC)
 | |
|     dt = dt.replace(microsecond=n1secs // 10)
 | |
|     return dt, pos
 | |
| 
 | |
| # new-style next volume
 | |
| def _next_newvol(volfile):
 | |
|     i = len(volfile) - 1
 | |
|     while i >= 0:
 | |
|         if volfile[i] >= '0' and volfile[i] <= '9':
 | |
|             return _inc_volname(volfile, i)
 | |
|         i -= 1
 | |
|     raise BadRarName("Cannot construct volume name: " + volfile)
 | |
| 
 | |
| # old-style next volume
 | |
| def _next_oldvol(volfile):
 | |
|     # rar -> r00
 | |
|     if volfile[-4:].lower() == '.rar':
 | |
|         return volfile[:-2] + '00'
 | |
|     return _inc_volname(volfile, len(volfile) - 1)
 | |
| 
 | |
| # increase digits with carry, otherwise just increment char
 | |
| def _inc_volname(volfile, i):
 | |
|     fn = list(volfile)
 | |
|     while i >= 0:
 | |
|         if fn[i] != '9':
 | |
|             fn[i] = chr(ord(fn[i]) + 1)
 | |
|             break
 | |
|         fn[i] = '0'
 | |
|         i -= 1
 | |
|     return ''.join(fn)
 | |
| 
 | |
| # rar3 extended time fields
 | |
| def _parse_ext_time(h, data, pos):
 | |
|     # flags and rest of data can be missing
 | |
|     flags = 0
 | |
|     if pos + 2 <= len(data):
 | |
|         flags = S_SHORT.unpack_from(data, pos)[0]
 | |
|         pos += 2
 | |
| 
 | |
|     mtime, pos = _parse_xtime(flags >> 3 * 4, data, pos, h.mtime)
 | |
|     h.ctime, pos = _parse_xtime(flags >> 2 * 4, data, pos)
 | |
|     h.atime, pos = _parse_xtime(flags >> 1 * 4, data, pos)
 | |
|     h.arctime, pos = _parse_xtime(flags >> 0 * 4, data, pos)
 | |
|     if mtime:
 | |
|         h.mtime = mtime
 | |
|         h.date_time = mtime.timetuple()[:6]
 | |
|     return pos
 | |
| 
 | |
| # rar3 one extended time field
 | |
| def _parse_xtime(flag, data, pos, basetime=None):
 | |
|     res = None
 | |
|     if flag & 8:
 | |
|         if not basetime:
 | |
|             basetime, pos = load_dostime(data, pos)
 | |
| 
 | |
|         # load second fractions
 | |
|         rem = 0
 | |
|         cnt = flag & 3
 | |
|         for _ in range(cnt):
 | |
|             b, pos = load_byte(data, pos)
 | |
|             rem = (b << 16) | (rem >> 8)
 | |
| 
 | |
|         # convert 100ns units to microseconds
 | |
|         usec = rem // 10
 | |
|         if usec > 1000000:
 | |
|             usec = 999999
 | |
| 
 | |
|         # dostime has room for 30 seconds only, correct if needed
 | |
|         if flag & 4 and basetime.second < 59:
 | |
|             res = basetime.replace(microsecond=usec, second=basetime.second + 1)
 | |
|         else:
 | |
|             res = basetime.replace(microsecond=usec)
 | |
|     return res, pos
 | |
| 
 | |
| def is_filelike(obj):
 | |
|     """Filename or file object?
 | |
|     """
 | |
|     if isinstance(obj, (bytes, unicode)):
 | |
|         return False
 | |
|     res = True
 | |
|     for a in ('read', 'tell', 'seek'):
 | |
|         res = res and hasattr(obj, a)
 | |
|     if not res:
 | |
|         raise ValueError("Invalid object passed as file")
 | |
|     return True
 | |
| 
 | |
| def rar3_s2k(psw, salt):
 | |
|     """String-to-key hash for RAR3.
 | |
|     """
 | |
|     if not isinstance(psw, unicode):
 | |
|         psw = psw.decode('utf8')
 | |
|     seed = bytearray(psw.encode('utf-16le') + salt)
 | |
|     h = Rar3Sha1(rarbug=True)
 | |
|     iv = EMPTY
 | |
|     for i in range(16):
 | |
|         for j in range(0x4000):
 | |
|             cnt = S_LONG.pack(i * 0x4000 + j)
 | |
|             h.update(seed)
 | |
|             h.update(cnt[:3])
 | |
|             if j == 0:
 | |
|                 iv += h.digest()[19:20]
 | |
|     key_be = h.digest()[:16]
 | |
|     key_le = pack("<LLLL", *unpack(">LLLL", key_be))
 | |
|     return key_le, iv
 | |
| 
 | |
| def rar3_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=None):
 | |
|     """Decompress blob of compressed data.
 | |
| 
 | |
|     Used for data with non-standard header - eg. comments.
 | |
|     """
 | |
|     # already uncompressed?
 | |
|     if meth == RAR_M0 and (flags & RAR_FILE_PASSWORD) == 0:
 | |
|         return data
 | |
| 
 | |
|     # take only necessary flags
 | |
|     flags = flags & (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK)
 | |
|     flags |= RAR_LONG_BLOCK
 | |
| 
 | |
|     # file header
 | |
|     fname = b'data'
 | |
|     date = 0
 | |
|     mode = 0x20
 | |
|     fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc,
 | |
|                            date, vers, meth, len(fname), mode)
 | |
|     fhdr += fname
 | |
|     if flags & RAR_FILE_SALT:
 | |
|         if not salt:
 | |
|             return EMPTY
 | |
|         fhdr += salt
 | |
| 
 | |
|     # full header
 | |
|     hlen = S_BLK_HDR.size + len(fhdr)
 | |
|     hdr = S_BLK_HDR.pack(0, RAR_BLOCK_FILE, flags, hlen) + fhdr
 | |
|     hcrc = rar_crc32(hdr[2:]) & 0xFFFF
 | |
|     hdr = S_BLK_HDR.pack(hcrc, RAR_BLOCK_FILE, flags, hlen) + fhdr
 | |
| 
 | |
|     # archive main header
 | |
|     mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + ZERO * (2 + 4)
 | |
| 
 | |
|     # decompress via temp rar
 | |
|     tmpfd, tmpname = mkstemp(suffix='.rar')
 | |
|     tmpf = os.fdopen(tmpfd, "wb")
 | |
|     try:
 | |
|         tmpf.write(RAR_ID + mh + hdr + data)
 | |
|         tmpf.close()
 | |
| 
 | |
|         cmd = [UNRAR_TOOL] + list(OPEN_ARGS)
 | |
|         add_password_arg(cmd, psw, (flags & RAR_FILE_PASSWORD))
 | |
|         cmd.append(tmpname)
 | |
| 
 | |
|         p = custom_popen(cmd)
 | |
|         return p.communicate()[0]
 | |
|     finally:
 | |
|         tmpf.close()
 | |
|         os.unlink(tmpname)
 | |
| 
 | |
| def to_datetime(t):
 | |
|     """Convert 6-part time tuple into datetime object.
 | |
|     """
 | |
|     if t is None:
 | |
|         return None
 | |
| 
 | |
|     # extract values
 | |
|     year, mon, day, h, m, s = t
 | |
| 
 | |
|     # assume the values are valid
 | |
|     try:
 | |
|         return datetime(year, mon, day, h, m, s)
 | |
|     except ValueError:
 | |
|         pass
 | |
| 
 | |
|     # sanitize invalid values
 | |
|     mday = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
 | |
|     if mon < 1:
 | |
|         mon = 1
 | |
|     if mon > 12:
 | |
|         mon = 12
 | |
|     if day < 1:
 | |
|         day = 1
 | |
|     if day > mday[mon]:
 | |
|         day = mday[mon]
 | |
|     if h > 23:
 | |
|         h = 23
 | |
|     if m > 59:
 | |
|         m = 59
 | |
|     if s > 59:
 | |
|         s = 59
 | |
|     if mon == 2 and day == 29:
 | |
|         try:
 | |
|             return datetime(year, mon, day, h, m, s)
 | |
|         except ValueError:
 | |
|             day = 28
 | |
|     return datetime(year, mon, day, h, m, s)
 | |
| 
 | |
| def parse_dos_time(stamp):
 | |
|     """Parse standard 32-bit DOS timestamp.
 | |
|     """
 | |
|     sec, stamp = stamp & 0x1F, stamp >> 5
 | |
|     mn,  stamp = stamp & 0x3F, stamp >> 6
 | |
|     hr,  stamp = stamp & 0x1F, stamp >> 5
 | |
|     day, stamp = stamp & 0x1F, stamp >> 5
 | |
|     mon, stamp = stamp & 0x0F, stamp >> 4
 | |
|     yr = (stamp & 0x7F) + 1980
 | |
|     return (yr, mon, day, hr, mn, sec * 2)
 | |
| 
 | |
| def custom_popen(cmd):
 | |
|     """Disconnect cmd from parent fds, read only from stdout.
 | |
|     """
 | |
|     # needed for py2exe
 | |
|     creationflags = 0
 | |
|     if sys.platform == 'win32':
 | |
|         creationflags = 0x08000000   # CREATE_NO_WINDOW
 | |
| 
 | |
|     # run command
 | |
|     try:
 | |
|         p = Popen(cmd, bufsize=0, stdout=PIPE, stdin=PIPE, stderr=STDOUT,
 | |
|                   creationflags=creationflags)
 | |
|     except OSError as ex:
 | |
|         if ex.errno == errno.ENOENT:
 | |
|             raise RarCannotExec("Unrar not installed? (rarfile.UNRAR_TOOL=%r)" % UNRAR_TOOL)
 | |
|         if ex.errno == errno.EACCES or ex.errno == errno.EPERM:
 | |
|             raise RarCannotExec("Cannot execute unrar (rarfile.UNRAR_TOOL=%r)" % UNRAR_TOOL)
 | |
|         raise
 | |
|     return p
 | |
| 
 | |
| def custom_check(cmd, ignore_retcode=False):
 | |
|     """Run command, collect output, raise error if needed.
 | |
|     """
 | |
|     p = custom_popen(cmd)
 | |
|     out, _ = p.communicate()
 | |
|     if p.returncode and not ignore_retcode:
 | |
|         raise RarExecError("Check-run failed")
 | |
|     return out
 | |
| 
 | |
| def add_password_arg(cmd, psw, ___required=False):
 | |
|     """Append password switch to commandline.
 | |
|     """
 | |
|     if UNRAR_TOOL == ALT_TOOL:
 | |
|         return
 | |
|     if psw is not None:
 | |
|         cmd.append('-p' + psw)
 | |
|     else:
 | |
|         cmd.append('-p-')
 | |
| 
 | |
| def check_returncode(p, out):
 | |
|     """Raise exception according to unrar exit code.
 | |
|     """
 | |
|     code = p.returncode
 | |
|     if code == 0:
 | |
|         return
 | |
| 
 | |
|     # map return code to exception class, codes from rar.txt
 | |
|     errmap = [None,
 | |
|               RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError,    # 1..4
 | |
|               RarWriteError, RarOpenError, RarUserError, RarMemoryError,        # 5..8
 | |
|               RarCreateError, RarNoFilesError, RarWrongPassword]                # 9..11
 | |
|     if UNRAR_TOOL == ALT_TOOL:
 | |
|         errmap = [None]
 | |
|     if code > 0 and code < len(errmap):
 | |
|         exc = errmap[code]
 | |
|     elif code == 255:
 | |
|         exc = RarUserBreak
 | |
|     elif code < 0:
 | |
|         exc = RarSignalExit
 | |
|     else:
 | |
|         exc = RarUnknownError
 | |
| 
 | |
|     # format message
 | |
|     if out:
 | |
|         msg = "%s [%d]: %s" % (exc.__doc__, p.returncode, out)
 | |
|     else:
 | |
|         msg = "%s [%d]" % (exc.__doc__, p.returncode)
 | |
| 
 | |
|     raise exc(msg)
 | |
| 
 | |
| def hmac_sha256(key, data):
 | |
|     """HMAC-SHA256"""
 | |
|     return HMAC(key, data, sha256).digest()
 | |
| 
 | |
| def membuf_tempfile(memfile):
 | |
|     """Write in-memory file object to real file."""
 | |
|     memfile.seek(0, 0)
 | |
| 
 | |
|     tmpfd, tmpname = mkstemp(suffix='.rar')
 | |
|     tmpf = os.fdopen(tmpfd, "wb")
 | |
| 
 | |
|     try:
 | |
|         while True:
 | |
|             buf = memfile.read(BSIZE)
 | |
|             if not buf:
 | |
|                 break
 | |
|             tmpf.write(buf)
 | |
|         tmpf.close()
 | |
|     except:
 | |
|         tmpf.close()
 | |
|         os.unlink(tmpname)
 | |
|         raise
 | |
|     return tmpname
 | |
| 
 | |
| class XTempFile(object):
 | |
|     """Real file for archive.
 | |
|     """
 | |
|     __slots__ = ('_tmpfile', '_filename')
 | |
| 
 | |
|     def __init__(self, rarfile):
 | |
|         if is_filelike(rarfile):
 | |
|             self._tmpfile = membuf_tempfile(rarfile)
 | |
|             self._filename = self._tmpfile
 | |
|         else:
 | |
|             self._tmpfile = None
 | |
|             self._filename = rarfile
 | |
| 
 | |
|     def __enter__(self):
 | |
|         return self._filename
 | |
| 
 | |
|     def __exit__(self, exc_type, exc_value, tb):
 | |
|         if self._tmpfile:
 | |
|             try:
 | |
|                 os.unlink(self._tmpfile)
 | |
|             except OSError:
 | |
|                 pass
 | |
|             self._tmpfile = None
 | |
| 
 | |
| #
 | |
| # Check if unrar works
 | |
| #
 | |
| 
 | |
| ORIG_UNRAR_TOOL = UNRAR_TOOL
 | |
| ORIG_OPEN_ARGS = OPEN_ARGS
 | |
| ORIG_EXTRACT_ARGS = EXTRACT_ARGS
 | |
| ORIG_TEST_ARGS = TEST_ARGS
 | |
| 
 | |
| def _check_unrar_tool():
 | |
|     global UNRAR_TOOL, OPEN_ARGS, EXTRACT_ARGS, TEST_ARGS
 | |
|     try:
 | |
|         # does UNRAR_TOOL work?
 | |
|         custom_check([ORIG_UNRAR_TOOL], True)
 | |
| 
 | |
|         UNRAR_TOOL = ORIG_UNRAR_TOOL
 | |
|         OPEN_ARGS = ORIG_OPEN_ARGS
 | |
|         EXTRACT_ARGS = ORIG_EXTRACT_ARGS
 | |
|         TEST_ARGS = ORIG_TEST_ARGS
 | |
|     except RarCannotExec:
 | |
|         try:
 | |
|             # does ALT_TOOL work?
 | |
|             custom_check([ALT_TOOL] + list(ALT_CHECK_ARGS), True)
 | |
|             # replace config
 | |
|             UNRAR_TOOL = ALT_TOOL
 | |
|             OPEN_ARGS = ALT_OPEN_ARGS
 | |
|             EXTRACT_ARGS = ALT_EXTRACT_ARGS
 | |
|             TEST_ARGS = ALT_TEST_ARGS
 | |
|         except RarCannotExec:
 | |
|             # no usable tool, only uncompressed archives work
 | |
|             return False
 | |
|     return True
 | |
| 
 | |
| _check_unrar_tool()
 | |
| 
 |