mirror of https://github.com/morpheus65535/bazarr
Added support for 7z by upgrading rarfile dependency
- Bump rarfile.py from 4.0 to 4.1 - Add support for 7zip for archive extractions, since unar is a steaming pile of C# and unrar is non-free, having some easy-to-package software to handle archive makes it easier to run Bazarr on Linux distributions.
This commit is contained in:
parent
c97a98d4f2
commit
4b7cdbc5f3
|
@ -194,16 +194,28 @@ def init_binaries():
|
||||||
exe = get_binary("unar")
|
exe = get_binary("unar")
|
||||||
rarfile.UNAR_TOOL = exe
|
rarfile.UNAR_TOOL = exe
|
||||||
rarfile.UNRAR_TOOL = None
|
rarfile.UNRAR_TOOL = None
|
||||||
rarfile.tool_setup(unrar=False, unar=True, bsdtar=False, force=True)
|
rarfile.SEVENZIP_TOOL = None
|
||||||
|
rarfile.tool_setup(unrar=False, unar=True, bsdtar=False, sevenzip=False, force=True)
|
||||||
except (BinaryNotFound, rarfile.RarCannotExec):
|
except (BinaryNotFound, rarfile.RarCannotExec):
|
||||||
try:
|
try:
|
||||||
exe = get_binary("unrar")
|
exe = get_binary("unrar")
|
||||||
rarfile.UNRAR_TOOL = exe
|
rarfile.UNRAR_TOOL = exe
|
||||||
rarfile.UNAR_TOOL = None
|
rarfile.UNAR_TOOL = None
|
||||||
rarfile.tool_setup(unrar=True, unar=False, bsdtar=False, force=True)
|
rarfile.SEVENZIP_TOOL = None
|
||||||
|
rarfile.tool_setup(unrar=True, unar=False, bsdtar=False, sevenzip=False, force=True)
|
||||||
except (BinaryNotFound, rarfile.RarCannotExec):
|
except (BinaryNotFound, rarfile.RarCannotExec):
|
||||||
logging.exception("BAZARR requires a rar archive extraction utilities (unrar, unar) and it can't be found.")
|
try:
|
||||||
raise BinaryNotFound
|
exe = get_binary("7z")
|
||||||
|
rarfile.UNRAR_TOOL = None
|
||||||
|
rarfile.UNAR_TOOL = None
|
||||||
|
rarfile.SEVENZIP_TOOL = "7z"
|
||||||
|
rarfile.tool_setup(unrar=False, unar=False, bsdtar=False, sevenzip=True, force=True)
|
||||||
|
except (BinaryNotFound, rarfile.RarCannotExec):
|
||||||
|
logging.exception("BAZARR requires a rar archive extraction utilities (unrar, unar, 7zip) and it can't be found.")
|
||||||
|
raise BinaryNotFound
|
||||||
|
else:
|
||||||
|
logging.debug("Using 7zip from: %s", exe)
|
||||||
|
return exe
|
||||||
else:
|
else:
|
||||||
logging.debug("Using UnRAR from: %s", exe)
|
logging.debug("Using UnRAR from: %s", exe)
|
||||||
return exe
|
return exe
|
||||||
|
|
287
libs/rarfile.py
287
libs/rarfile.py
|
@ -59,12 +59,14 @@ import sys
|
||||||
import warnings
|
import warnings
|
||||||
from binascii import crc32, hexlify
|
from binascii import crc32, hexlify
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from hashlib import blake2s, pbkdf2_hmac, sha1
|
from hashlib import blake2s, pbkdf2_hmac, sha1, sha256
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from struct import Struct, pack, unpack
|
from struct import Struct, pack, unpack
|
||||||
from subprocess import DEVNULL, PIPE, STDOUT, Popen
|
from subprocess import DEVNULL, PIPE, STDOUT, Popen
|
||||||
from tempfile import mkstemp
|
from tempfile import mkstemp
|
||||||
|
|
||||||
|
AES = None
|
||||||
|
|
||||||
# only needed for encrypted headers
|
# only needed for encrypted headers
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
@ -90,10 +92,10 @@ class AES_CBC_Decrypt:
|
||||||
self.decrypt = ciph.decryptor().update
|
self.decrypt = ciph.decryptor().update
|
||||||
|
|
||||||
|
|
||||||
__version__ = "4.0"
|
__version__ = "4.1"
|
||||||
|
|
||||||
# export only interesting items
|
# export only interesting items
|
||||||
__all__ = ["is_rarfile", "is_rarfile_sfx", "RarInfo", "RarFile", "RarExtFile"]
|
__all__ = ["get_rar_version", "is_rarfile", "is_rarfile_sfx", "RarInfo", "RarFile", "RarExtFile"]
|
||||||
|
|
||||||
##
|
##
|
||||||
## Module configuration. Can be tuned after importing.
|
## Module configuration. Can be tuned after importing.
|
||||||
|
@ -108,6 +110,12 @@ UNAR_TOOL = "unar"
|
||||||
#: executable for bsdtar tool
|
#: executable for bsdtar tool
|
||||||
BSDTAR_TOOL = "bsdtar"
|
BSDTAR_TOOL = "bsdtar"
|
||||||
|
|
||||||
|
#: executable for p7zip/7z tool
|
||||||
|
SEVENZIP_TOOL = "7z"
|
||||||
|
|
||||||
|
#: executable for alternative 7z tool
|
||||||
|
SEVENZIP2_TOOL = "7zz"
|
||||||
|
|
||||||
#: default fallback charset
|
#: default fallback charset
|
||||||
DEFAULT_CHARSET = "windows-1252"
|
DEFAULT_CHARSET = "windows-1252"
|
||||||
|
|
||||||
|
@ -280,6 +288,9 @@ DOS_MODE_SYSTEM = 0x04
|
||||||
DOS_MODE_HIDDEN = 0x02
|
DOS_MODE_HIDDEN = 0x02
|
||||||
DOS_MODE_READONLY = 0x01
|
DOS_MODE_READONLY = 0x01
|
||||||
|
|
||||||
|
RAR5_PW_CHECK_SIZE = 8
|
||||||
|
RAR5_PW_SUM_SIZE = 4
|
||||||
|
|
||||||
##
|
##
|
||||||
## internal constants
|
## internal constants
|
||||||
##
|
##
|
||||||
|
@ -298,17 +309,7 @@ _BAD_CHARS = r"""\x00-\x1F<>|"?*"""
|
||||||
RC_BAD_CHARS_UNIX = re.compile(r"[%s]" % _BAD_CHARS)
|
RC_BAD_CHARS_UNIX = re.compile(r"[%s]" % _BAD_CHARS)
|
||||||
RC_BAD_CHARS_WIN32 = re.compile(r"[%s:^\\]" % _BAD_CHARS)
|
RC_BAD_CHARS_WIN32 = re.compile(r"[%s:^\\]" % _BAD_CHARS)
|
||||||
|
|
||||||
|
FORCE_TOOL = False
|
||||||
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 RAR_V3
|
|
||||||
elif buf.startswith(RAR5_ID):
|
|
||||||
return RAR_V5
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _find_sfx_header(xfile):
|
def _find_sfx_header(xfile):
|
||||||
|
@ -340,10 +341,27 @@ def _find_sfx_header(xfile):
|
||||||
## Public interface
|
## Public interface
|
||||||
##
|
##
|
||||||
|
|
||||||
|
|
||||||
|
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 RAR_V3
|
||||||
|
elif buf.startswith(RAR5_ID):
|
||||||
|
return RAR_V5
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def is_rarfile(xfile):
|
def is_rarfile(xfile):
|
||||||
"""Check quickly whether file is rar archive.
|
"""Check quickly whether file is rar archive.
|
||||||
"""
|
"""
|
||||||
return _get_rar_version(xfile) > 0
|
try:
|
||||||
|
return get_rar_version(xfile) > 0
|
||||||
|
except OSError:
|
||||||
|
# File not found or not accessible, ignore
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_rarfile_sfx(xfile):
|
def is_rarfile_sfx(xfile):
|
||||||
|
@ -634,6 +652,25 @@ class RarInfo:
|
||||||
|
|
||||||
class RarFile:
|
class RarFile:
|
||||||
"""Parse RAR structure, provide access to files in archive.
|
"""Parse RAR structure, provide access to files in archive.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
file
|
||||||
|
archive file name or file-like object.
|
||||||
|
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".
|
||||||
|
part_only
|
||||||
|
If True, read only single file and allow it to be middle-part
|
||||||
|
of multi-volume archive.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: File name, if available. Unicode string or None.
|
#: File name, if available. Unicode string or None.
|
||||||
|
@ -643,25 +680,7 @@ class RarFile:
|
||||||
comment = None
|
comment = None
|
||||||
|
|
||||||
def __init__(self, file, mode="r", charset=None, info_callback=None,
|
def __init__(self, file, mode="r", charset=None, info_callback=None,
|
||||||
crc_check=True, errors="stop"):
|
crc_check=True, errors="stop", part_only=False):
|
||||||
"""Open and parse a RAR archive.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
|
|
||||||
file
|
|
||||||
archive file name or file-like object.
|
|
||||||
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".
|
|
||||||
"""
|
|
||||||
if is_filelike(file):
|
if is_filelike(file):
|
||||||
self.filename = getattr(file, "name", None)
|
self.filename = getattr(file, "name", None)
|
||||||
else:
|
else:
|
||||||
|
@ -673,6 +692,7 @@ class RarFile:
|
||||||
self._charset = charset or DEFAULT_CHARSET
|
self._charset = charset or DEFAULT_CHARSET
|
||||||
self._info_callback = info_callback
|
self._info_callback = info_callback
|
||||||
self._crc_check = crc_check
|
self._crc_check = crc_check
|
||||||
|
self._part_only = part_only
|
||||||
self._password = None
|
self._password = None
|
||||||
self._file_parser = None
|
self._file_parser = None
|
||||||
|
|
||||||
|
@ -740,6 +760,14 @@ class RarFile:
|
||||||
"""
|
"""
|
||||||
return self._file_parser.getinfo(name)
|
return self._file_parser.getinfo(name)
|
||||||
|
|
||||||
|
def getinfo_orig(self, name):
|
||||||
|
"""Return RarInfo for file source.
|
||||||
|
|
||||||
|
RAR5: if name is hard-linked or copied file,
|
||||||
|
returns original entry with original filename.
|
||||||
|
"""
|
||||||
|
return self._file_parser.getinfo_orig(name)
|
||||||
|
|
||||||
def open(self, name, mode="r", pwd=None):
|
def open(self, name, mode="r", pwd=None):
|
||||||
"""Returns file-like object (:class:`RarExtFile`) from where the data can be read.
|
"""Returns file-like object (:class:`RarExtFile`) from where the data can be read.
|
||||||
|
|
||||||
|
@ -880,12 +908,12 @@ class RarFile:
|
||||||
if ver == RAR_V3:
|
if ver == RAR_V3:
|
||||||
p3 = RAR3Parser(self._rarfile, self._password, self._crc_check,
|
p3 = RAR3Parser(self._rarfile, self._password, self._crc_check,
|
||||||
self._charset, self._strict, self._info_callback,
|
self._charset, self._strict, self._info_callback,
|
||||||
sfx_ofs)
|
sfx_ofs, self._part_only)
|
||||||
self._file_parser = p3 # noqa
|
self._file_parser = p3 # noqa
|
||||||
elif ver == RAR_V5:
|
elif ver == RAR_V5:
|
||||||
p5 = RAR5Parser(self._rarfile, self._password, self._crc_check,
|
p5 = RAR5Parser(self._rarfile, self._password, self._crc_check,
|
||||||
self._charset, self._strict, self._info_callback,
|
self._charset, self._strict, self._info_callback,
|
||||||
sfx_ofs)
|
sfx_ofs, self._part_only)
|
||||||
self._file_parser = p5 # noqa
|
self._file_parser = p5 # noqa
|
||||||
else:
|
else:
|
||||||
raise NotRarFile("Not a RAR file")
|
raise NotRarFile("Not a RAR file")
|
||||||
|
@ -984,7 +1012,8 @@ class CommonParser:
|
||||||
_password = None
|
_password = None
|
||||||
comment = None
|
comment = None
|
||||||
|
|
||||||
def __init__(self, rarfile, password, crc_check, charset, strict, info_cb, sfx_offset):
|
def __init__(self, rarfile, password, crc_check, charset, strict,
|
||||||
|
info_cb, sfx_offset, part_only):
|
||||||
self._rarfile = rarfile
|
self._rarfile = rarfile
|
||||||
self._password = password
|
self._password = password
|
||||||
self._crc_check = crc_check
|
self._crc_check = crc_check
|
||||||
|
@ -995,6 +1024,7 @@ class CommonParser:
|
||||||
self._info_map = {}
|
self._info_map = {}
|
||||||
self._vol_list = []
|
self._vol_list = []
|
||||||
self._sfx_offset = sfx_offset
|
self._sfx_offset = sfx_offset
|
||||||
|
self._part_only = part_only
|
||||||
|
|
||||||
def has_header_encryption(self):
|
def has_header_encryption(self):
|
||||||
"""Returns True if headers are encrypted
|
"""Returns True if headers are encrypted
|
||||||
|
@ -1043,7 +1073,16 @@ class CommonParser:
|
||||||
try:
|
try:
|
||||||
return self._info_map[fname]
|
return self._info_map[fname]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise NoRarEntry("No such file: %s" % fname)
|
raise NoRarEntry("No such file: %s" % fname) from None
|
||||||
|
|
||||||
|
def getinfo_orig(self, member):
|
||||||
|
inf = self.getinfo(member)
|
||||||
|
if inf.file_redir:
|
||||||
|
redir_type, redir_flags, redir_name = inf.file_redir
|
||||||
|
# cannot leave to unrar as it expects copied file to exist
|
||||||
|
if redir_type in (RAR5_XREDIR_FILE_COPY, RAR5_XREDIR_HARD_LINK):
|
||||||
|
inf = self.getinfo(redir_name)
|
||||||
|
return inf
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
"""Process file."""
|
"""Process file."""
|
||||||
|
@ -1080,7 +1119,7 @@ class CommonParser:
|
||||||
if raise_need_first_vol:
|
if raise_need_first_vol:
|
||||||
# did not find ENDARC with VOLNR
|
# did not find ENDARC with VOLNR
|
||||||
raise NeedFirstVolume("Need to start from first volume", None)
|
raise NeedFirstVolume("Need to start from first volume", None)
|
||||||
if more_vols:
|
if more_vols and not self._part_only:
|
||||||
volume += 1
|
volume += 1
|
||||||
fd.close()
|
fd.close()
|
||||||
try:
|
try:
|
||||||
|
@ -1098,6 +1137,7 @@ class CommonParser:
|
||||||
endarc = False
|
endarc = False
|
||||||
self._vol_list.append(volfile)
|
self._vol_list.append(volfile)
|
||||||
self._main = None
|
self._main = None
|
||||||
|
self._hdrenc_main = None
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
h.volume = volume
|
h.volume = volume
|
||||||
|
@ -1105,7 +1145,7 @@ class CommonParser:
|
||||||
|
|
||||||
if h.type == RAR_BLOCK_MAIN and not self._main:
|
if h.type == RAR_BLOCK_MAIN and not self._main:
|
||||||
self._main = h
|
self._main = h
|
||||||
if volume == 0 and (h.flags & RAR_MAIN_NEWNUMBERING):
|
if volume == 0 and (h.flags & RAR_MAIN_NEWNUMBERING) and not self._part_only:
|
||||||
# RAR 2.x does not set FIRSTVOLUME,
|
# RAR 2.x does not set FIRSTVOLUME,
|
||||||
# so check it only if NEWNUMBERING is used
|
# so check it only if NEWNUMBERING is used
|
||||||
if (h.flags & RAR_MAIN_FIRSTVOLUME) == 0:
|
if (h.flags & RAR_MAIN_FIRSTVOLUME) == 0:
|
||||||
|
@ -1137,7 +1177,8 @@ class CommonParser:
|
||||||
more_vols = True
|
more_vols = True
|
||||||
# RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
|
# RAR 2.x does not set RAR_MAIN_FIRSTVOLUME
|
||||||
if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE:
|
if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE:
|
||||||
raise_need_first_vol = True
|
if not self._part_only:
|
||||||
|
raise_need_first_vol = True
|
||||||
|
|
||||||
if h.needs_password():
|
if h.needs_password():
|
||||||
self._needs_password = True
|
self._needs_password = True
|
||||||
|
@ -1241,6 +1282,8 @@ class CommonParser:
|
||||||
return self._open_unrar(self._rarfile, inf, pwd)
|
return self._open_unrar(self._rarfile, inf, pwd)
|
||||||
|
|
||||||
def _open_clear(self, inf):
|
def _open_clear(self, inf):
|
||||||
|
if FORCE_TOOL:
|
||||||
|
return self._open_unrar(self._rarfile, inf)
|
||||||
return DirectReader(self, inf)
|
return DirectReader(self, inf)
|
||||||
|
|
||||||
def _open_hack_core(self, inf, pwd, prefix, suffix):
|
def _open_hack_core(self, inf, pwd, prefix, suffix):
|
||||||
|
@ -1288,7 +1331,7 @@ class CommonParser:
|
||||||
# not giving filename avoids encoding related problems
|
# not giving filename avoids encoding related problems
|
||||||
fn = None
|
fn = None
|
||||||
if not tmpfile or force_file:
|
if not tmpfile or force_file:
|
||||||
fn = inf.filename
|
fn = inf.filename.replace("/", os.path.sep)
|
||||||
|
|
||||||
# read from unrar pipe
|
# read from unrar pipe
|
||||||
cmd = setup.open_cmdline(pwd, rarfile, fn)
|
cmd = setup.open_cmdline(pwd, rarfile, fn)
|
||||||
|
@ -1379,6 +1422,9 @@ class RAR3Parser(CommonParser):
|
||||||
buf = fd.read(S_BLK_HDR.size)
|
buf = fd.read(S_BLK_HDR.size)
|
||||||
if not buf:
|
if not buf:
|
||||||
return None
|
return None
|
||||||
|
if len(buf) < S_BLK_HDR.size:
|
||||||
|
self._set_error("Unexpected EOF when reading header")
|
||||||
|
return None
|
||||||
t = S_BLK_HDR.unpack_from(buf)
|
t = S_BLK_HDR.unpack_from(buf)
|
||||||
h.header_crc, h.type, h.flags, h.header_size = t
|
h.header_crc, h.type, h.flags, h.header_size = t
|
||||||
|
|
||||||
|
@ -1751,14 +1797,18 @@ class RAR5Parser(CommonParser):
|
||||||
# AES encrypted headers
|
# AES encrypted headers
|
||||||
_last_aes256_key = (-1, None, None) # (kdf_count, salt, key)
|
_last_aes256_key = (-1, None, None) # (kdf_count, salt, key)
|
||||||
|
|
||||||
|
def _get_utf8_password(self):
|
||||||
|
pwd = self._password
|
||||||
|
if isinstance(pwd, str):
|
||||||
|
return pwd.encode("utf8")
|
||||||
|
return pwd
|
||||||
|
|
||||||
def _gen_key(self, kdf_count, salt):
|
def _gen_key(self, kdf_count, salt):
|
||||||
if self._last_aes256_key[:2] == (kdf_count, salt):
|
if self._last_aes256_key[:2] == (kdf_count, salt):
|
||||||
return self._last_aes256_key[2]
|
return self._last_aes256_key[2]
|
||||||
if kdf_count > 24:
|
if kdf_count > 24:
|
||||||
raise BadRarFile("Too large kdf_count")
|
raise BadRarFile("Too large kdf_count")
|
||||||
pwd = self._password
|
pwd = self._get_utf8_password()
|
||||||
if isinstance(pwd, str):
|
|
||||||
pwd = pwd.encode("utf8")
|
|
||||||
key = pbkdf2_hmac("sha256", pwd, salt, 1 << kdf_count)
|
key = pbkdf2_hmac("sha256", pwd, salt, 1 << kdf_count)
|
||||||
self._last_aes256_key = (kdf_count, salt, key)
|
self._last_aes256_key = (kdf_count, salt, key)
|
||||||
return key
|
return key
|
||||||
|
@ -1776,8 +1826,17 @@ class RAR5Parser(CommonParser):
|
||||||
"""
|
"""
|
||||||
header_offset = fd.tell()
|
header_offset = fd.tell()
|
||||||
|
|
||||||
preload = 4 + 3
|
preload = 4 + 1
|
||||||
start_bytes = fd.read(preload)
|
start_bytes = fd.read(preload)
|
||||||
|
if len(start_bytes) < preload:
|
||||||
|
self._set_error("Unexpected EOF when reading header")
|
||||||
|
return None
|
||||||
|
while start_bytes[-1] & 0x80:
|
||||||
|
b = fd.read(1)
|
||||||
|
if not b:
|
||||||
|
self._set_error("Unexpected EOF when reading header")
|
||||||
|
return None
|
||||||
|
start_bytes += b
|
||||||
header_crc, pos = load_le32(start_bytes, 0)
|
header_crc, pos = load_le32(start_bytes, 0)
|
||||||
hdrlen, pos = load_vint(start_bytes, pos)
|
hdrlen, pos = load_vint(start_bytes, pos)
|
||||||
if hdrlen > 2 * 1024 * 1024:
|
if hdrlen > 2 * 1024 * 1024:
|
||||||
|
@ -1912,15 +1971,39 @@ class RAR5Parser(CommonParser):
|
||||||
h.flags |= RAR_ENDARC_NEXT_VOLUME
|
h.flags |= RAR_ENDARC_NEXT_VOLUME
|
||||||
return h
|
return h
|
||||||
|
|
||||||
|
def _check_password(self, check_value, kdf_count_shift, salt):
|
||||||
|
if len(check_value) != RAR5_PW_CHECK_SIZE + RAR5_PW_SUM_SIZE:
|
||||||
|
return
|
||||||
|
|
||||||
|
hdr_check = check_value[:RAR5_PW_CHECK_SIZE]
|
||||||
|
hdr_sum = check_value[RAR5_PW_CHECK_SIZE:]
|
||||||
|
sum_hash = sha256(hdr_check).digest()
|
||||||
|
if sum_hash[:RAR5_PW_SUM_SIZE] != hdr_sum:
|
||||||
|
return
|
||||||
|
|
||||||
|
kdf_count = (1 << kdf_count_shift) + 32
|
||||||
|
pwd = self._get_utf8_password()
|
||||||
|
pwd_hash = pbkdf2_hmac("sha256", pwd, salt, kdf_count)
|
||||||
|
|
||||||
|
pwd_check = bytearray(RAR5_PW_CHECK_SIZE)
|
||||||
|
len_mask = RAR5_PW_CHECK_SIZE - 1
|
||||||
|
for i, v in enumerate(pwd_hash):
|
||||||
|
pwd_check[i & len_mask] ^= v
|
||||||
|
|
||||||
|
if pwd_check != hdr_check:
|
||||||
|
raise RarWrongPassword()
|
||||||
|
|
||||||
def _parse_encryption_block(self, h, hdata, pos):
|
def _parse_encryption_block(self, h, hdata, pos):
|
||||||
h.encryption_algo, pos = load_vint(hdata, pos)
|
h.encryption_algo, pos = load_vint(hdata, pos)
|
||||||
h.encryption_flags, pos = load_vint(hdata, pos)
|
h.encryption_flags, pos = load_vint(hdata, pos)
|
||||||
h.encryption_kdf_count, pos = load_byte(hdata, pos)
|
h.encryption_kdf_count, pos = load_byte(hdata, pos)
|
||||||
h.encryption_salt, pos = load_bytes(hdata, 16, pos)
|
h.encryption_salt, pos = load_bytes(hdata, 16, pos)
|
||||||
if h.encryption_flags & RAR5_ENC_FLAG_HAS_CHECKVAL:
|
if h.encryption_flags & RAR5_ENC_FLAG_HAS_CHECKVAL:
|
||||||
h.encryption_check_value = load_bytes(hdata, 12, pos)
|
h.encryption_check_value, pos = load_bytes(hdata, 12, pos)
|
||||||
if h.encryption_algo != RAR5_XENC_CIPHER_AES256:
|
if h.encryption_algo != RAR5_XENC_CIPHER_AES256:
|
||||||
raise BadRarFile("Unsupported header encryption cipher")
|
raise BadRarFile("Unsupported header encryption cipher")
|
||||||
|
if h.encryption_check_value and self._password:
|
||||||
|
self._check_password(h.encryption_check_value, h.encryption_kdf_count, h.encryption_salt)
|
||||||
self._hdrenc_main = h
|
self._hdrenc_main = h
|
||||||
return h
|
return h
|
||||||
|
|
||||||
|
@ -2157,6 +2240,7 @@ class RarExtFile(io.RawIOBase):
|
||||||
_remain = 0
|
_remain = 0
|
||||||
_returncode = 0
|
_returncode = 0
|
||||||
_md_context = None
|
_md_context = None
|
||||||
|
_seeking = False
|
||||||
|
|
||||||
def _open_extfile(self, parser, inf):
|
def _open_extfile(self, parser, inf):
|
||||||
self.name = inf.filename
|
self.name = inf.filename
|
||||||
|
@ -2165,7 +2249,10 @@ class RarExtFile(io.RawIOBase):
|
||||||
|
|
||||||
if self._fd:
|
if self._fd:
|
||||||
self._fd.close()
|
self._fd.close()
|
||||||
md_class = self._inf._md_class or NoHashContext
|
if self._seeking:
|
||||||
|
md_class = NoHashContext
|
||||||
|
else:
|
||||||
|
md_class = self._inf._md_class or NoHashContext
|
||||||
self._md_context = md_class()
|
self._md_context = md_class()
|
||||||
self._fd = None
|
self._fd = None
|
||||||
self._remain = self._inf.file_size
|
self._remain = self._inf.file_size
|
||||||
|
@ -2256,7 +2343,9 @@ class RarExtFile(io.RawIOBase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# disable crc check when seeking
|
# disable crc check when seeking
|
||||||
self._md_context = NoHashContext()
|
if not self._seeking:
|
||||||
|
self._md_context = NoHashContext()
|
||||||
|
self._seeking = True
|
||||||
|
|
||||||
fsize = self._inf.file_size
|
fsize = self._inf.file_size
|
||||||
cur_ofs = self.tell()
|
cur_ofs = self.tell()
|
||||||
|
@ -2437,6 +2526,11 @@ class DirectReader(RarExtFile):
|
||||||
def _read(self, cnt):
|
def _read(self, cnt):
|
||||||
"""Read from potentially multi-volume archive."""
|
"""Read from potentially multi-volume archive."""
|
||||||
|
|
||||||
|
pos = self._fd.tell()
|
||||||
|
need = self._cur.data_offset + self._cur.add_size - self._cur_avail
|
||||||
|
if pos != need:
|
||||||
|
self._fd.seek(need, 0)
|
||||||
|
|
||||||
buf = []
|
buf = []
|
||||||
while cnt > 0:
|
while cnt > 0:
|
||||||
# next vol needed?
|
# next vol needed?
|
||||||
|
@ -2790,7 +2884,7 @@ def load_le32(buf, pos):
|
||||||
end = pos + 4
|
end = pos + 4
|
||||||
if end > len(buf):
|
if end > len(buf):
|
||||||
raise BadRarFile("cannot load le32")
|
raise BadRarFile("cannot load le32")
|
||||||
return S_LONG.unpack_from(buf, pos)[0], pos + 4
|
return S_LONG.unpack_from(buf, pos)[0], end
|
||||||
|
|
||||||
|
|
||||||
def load_bytes(buf, num, pos):
|
def load_bytes(buf, num, pos):
|
||||||
|
@ -2833,36 +2927,61 @@ def load_windowstime(buf, pos):
|
||||||
return dt, pos
|
return dt, pos
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# volume numbering
|
||||||
|
#
|
||||||
|
|
||||||
|
_rc_num = re.compile('^[0-9]+$')
|
||||||
|
|
||||||
|
|
||||||
def _next_newvol(volfile):
|
def _next_newvol(volfile):
|
||||||
"""New-style next volume
|
"""New-style next volume
|
||||||
"""
|
"""
|
||||||
|
name, ext = os.path.splitext(volfile)
|
||||||
|
if ext.lower() in ("", ".exe", ".sfx"):
|
||||||
|
volfile = name + ".rar"
|
||||||
i = len(volfile) - 1
|
i = len(volfile) - 1
|
||||||
while i >= 0:
|
while i >= 0:
|
||||||
if volfile[i] >= "0" and volfile[i] <= "9":
|
if "0" <= volfile[i] <= "9":
|
||||||
return _inc_volname(volfile, i)
|
return _inc_volname(volfile, i, False)
|
||||||
|
if volfile[i] in ("/", os.sep):
|
||||||
|
break
|
||||||
i -= 1
|
i -= 1
|
||||||
raise BadRarName("Cannot construct volume name: " + volfile)
|
raise BadRarName("Cannot construct volume name: " + volfile)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _next_oldvol(volfile):
|
def _next_oldvol(volfile):
|
||||||
"""Old-style next volume
|
"""Old-style next volume
|
||||||
"""
|
"""
|
||||||
# rar -> r00
|
name, ext = os.path.splitext(volfile)
|
||||||
if volfile[-4:].lower() == ".rar":
|
if ext.lower() in ("", ".exe", ".sfx"):
|
||||||
return volfile[:-2] + "00"
|
ext = ".rar"
|
||||||
return _inc_volname(volfile, len(volfile) - 1)
|
sfx = ext[2:]
|
||||||
|
if _rc_num.match(sfx):
|
||||||
|
ext = _inc_volname(ext, len(ext) - 1, True)
|
||||||
|
else:
|
||||||
|
# .rar -> .r00
|
||||||
|
ext = ext[:2] + "00"
|
||||||
|
return name + ext
|
||||||
|
|
||||||
|
|
||||||
def _inc_volname(volfile, i):
|
def _inc_volname(volfile, i, inc_chars):
|
||||||
"""increase digits with carry, otherwise just increment char
|
"""increase digits with carry, otherwise just increment char
|
||||||
"""
|
"""
|
||||||
fn = list(volfile)
|
fn = list(volfile)
|
||||||
while i >= 0:
|
while i >= 0:
|
||||||
if fn[i] != "9":
|
if fn[i] == "9":
|
||||||
|
fn[i] = "0"
|
||||||
|
i -= 1
|
||||||
|
if i < 0:
|
||||||
|
fn.insert(0, "1")
|
||||||
|
elif "0" <= fn[i] < "9" or inc_chars:
|
||||||
fn[i] = chr(ord(fn[i]) + 1)
|
fn[i] = chr(ord(fn[i]) + 1)
|
||||||
break
|
break
|
||||||
fn[i] = "0"
|
else:
|
||||||
i -= 1
|
fn.insert(i + 1, "1")
|
||||||
|
break
|
||||||
return "".join(fn)
|
return "".join(fn)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2957,7 +3076,7 @@ def rar3_decompress(vers, meth, data, declen=0, flags=0, crc=0, pwd=None, salt=N
|
||||||
# file header
|
# file header
|
||||||
fname = b"data"
|
fname = b"data"
|
||||||
date = ((2010 - 1980) << 25) + (12 << 21) + (31 << 16)
|
date = ((2010 - 1980) << 25) + (12 << 21) + (31 << 16)
|
||||||
mode = 0x20
|
mode = DOS_MODE_ARCHIVE
|
||||||
fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc,
|
fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc,
|
||||||
date, vers, meth, len(fname), mode)
|
date, vers, meth, len(fname), mode)
|
||||||
fhdr += fname
|
fhdr += fname
|
||||||
|
@ -3038,17 +3157,12 @@ def to_datetime(t):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# sanitize invalid values
|
# sanitize invalid values
|
||||||
mday = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
mday = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
||||||
mon = max(1, min(mon, 12))
|
mon = max(1, min(mon, 12))
|
||||||
day = max(1, min(day, mday[mon]))
|
day = max(1, min(day, mday[mon]))
|
||||||
h = min(h, 23)
|
h = min(h, 23)
|
||||||
m = min(m, 59)
|
m = min(m, 59)
|
||||||
s = min(s, 59)
|
s = min(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)
|
return datetime(year, mon, day, h, m, s)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3075,7 +3189,7 @@ class nsdatetime(datetime):
|
||||||
__slots__ = ("nanosecond",)
|
__slots__ = ("nanosecond",)
|
||||||
nanosecond: int #: Number of nanoseconds, 0 <= nanosecond < 999999999
|
nanosecond: int #: Number of nanoseconds, 0 <= nanosecond < 999999999
|
||||||
|
|
||||||
def __new__(cls, year, month, day, hour=0, minute=0, second=0,
|
def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0,
|
||||||
microsecond=0, tzinfo=None, *, fold=0, nanosecond=0):
|
microsecond=0, tzinfo=None, *, fold=0, nanosecond=0):
|
||||||
usec, mod = divmod(nanosecond, 1000) if nanosecond else (microsecond, 0)
|
usec, mod = divmod(nanosecond, 1000) if nanosecond else (microsecond, 0)
|
||||||
if mod == 0:
|
if mod == 0:
|
||||||
|
@ -3165,9 +3279,9 @@ def custom_popen(cmd):
|
||||||
creationflags=creationflags)
|
creationflags=creationflags)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
if ex.errno == errno.ENOENT:
|
if ex.errno == errno.ENOENT:
|
||||||
raise RarCannotExec("Unrar not installed?")
|
raise RarCannotExec("Unrar not installed?") from None
|
||||||
if ex.errno == errno.EACCES or ex.errno == errno.EPERM:
|
if ex.errno == errno.EACCES or ex.errno == errno.EPERM:
|
||||||
raise RarCannotExec("Cannot execute unrar")
|
raise RarCannotExec("Cannot execute unrar") from None
|
||||||
raise
|
raise
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
@ -3244,6 +3358,8 @@ class ToolSetup:
|
||||||
def get_cmdline(self, key, pwd, nodash=False):
|
def get_cmdline(self, key, pwd, nodash=False):
|
||||||
cmdline = list(self.setup[key])
|
cmdline = list(self.setup[key])
|
||||||
cmdline[0] = globals()[cmdline[0]]
|
cmdline[0] = globals()[cmdline[0]]
|
||||||
|
if key == "check_cmd":
|
||||||
|
return cmdline
|
||||||
self.add_password_arg(cmdline, pwd)
|
self.add_password_arg(cmdline, pwd)
|
||||||
if not nodash:
|
if not nodash:
|
||||||
cmdline.append("--")
|
cmdline.append("--")
|
||||||
|
@ -3259,7 +3375,10 @@ class ToolSetup:
|
||||||
if not isinstance(pwd, str):
|
if not isinstance(pwd, str):
|
||||||
pwd = pwd.decode("utf8")
|
pwd = pwd.decode("utf8")
|
||||||
args = self.setup["password"]
|
args = self.setup["password"]
|
||||||
if isinstance(args, str):
|
if args is None:
|
||||||
|
tool = self.setup["open_cmd"][0]
|
||||||
|
raise RarCannotExec(f"{tool} does not support passwords")
|
||||||
|
elif isinstance(args, str):
|
||||||
cmdline.append(args + pwd)
|
cmdline.append(args + pwd)
|
||||||
else:
|
else:
|
||||||
cmdline.extend(args)
|
cmdline.extend(args)
|
||||||
|
@ -3303,10 +3422,30 @@ BSDTAR_CONFIG = {
|
||||||
"errmap": [None],
|
"errmap": [None],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SEVENZIP_CONFIG = {
|
||||||
|
"open_cmd": ("SEVENZIP_TOOL", "e", "-so", "-bb0"),
|
||||||
|
"check_cmd": ("SEVENZIP_TOOL", "i"),
|
||||||
|
"password": "-p",
|
||||||
|
"no_password": ("-p",),
|
||||||
|
"errmap": [None,
|
||||||
|
RarWarning, RarFatalError, None, None, # 1..4
|
||||||
|
None, None, RarUserError, RarMemoryError] # 5..8
|
||||||
|
}
|
||||||
|
|
||||||
|
SEVENZIP2_CONFIG = {
|
||||||
|
"open_cmd": ("SEVENZIP2_TOOL", "e", "-so", "-bb0"),
|
||||||
|
"check_cmd": ("SEVENZIP2_TOOL", "i"),
|
||||||
|
"password": "-p",
|
||||||
|
"no_password": ("-p",),
|
||||||
|
"errmap": [None,
|
||||||
|
RarWarning, RarFatalError, None, None, # 1..4
|
||||||
|
None, None, RarUserError, RarMemoryError] # 5..8
|
||||||
|
}
|
||||||
|
|
||||||
CURRENT_SETUP = None
|
CURRENT_SETUP = None
|
||||||
|
|
||||||
|
|
||||||
def tool_setup(unrar=True, unar=True, bsdtar=True, force=False):
|
def tool_setup(unrar=True, unar=True, bsdtar=True, sevenzip=True, sevenzip2=True, force=False):
|
||||||
"""Pick a tool, return cached ToolSetup.
|
"""Pick a tool, return cached ToolSetup.
|
||||||
"""
|
"""
|
||||||
global CURRENT_SETUP
|
global CURRENT_SETUP
|
||||||
|
@ -3319,6 +3458,10 @@ def tool_setup(unrar=True, unar=True, bsdtar=True, force=False):
|
||||||
lst.append(UNRAR_CONFIG)
|
lst.append(UNRAR_CONFIG)
|
||||||
if unar:
|
if unar:
|
||||||
lst.append(UNAR_CONFIG)
|
lst.append(UNAR_CONFIG)
|
||||||
|
if sevenzip:
|
||||||
|
lst.append(SEVENZIP_CONFIG)
|
||||||
|
if sevenzip2:
|
||||||
|
lst.append(SEVENZIP2_CONFIG)
|
||||||
if bsdtar:
|
if bsdtar:
|
||||||
lst.append(BSDTAR_CONFIG)
|
lst.append(BSDTAR_CONFIG)
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ python-engineio==4.3.4
|
||||||
python-socketio==5.7.2
|
python-socketio==5.7.2
|
||||||
pytz==2023.3
|
pytz==2023.3
|
||||||
pytz_deprecation_shim==0.1.0.post0
|
pytz_deprecation_shim==0.1.0.post0
|
||||||
rarfile==4.0
|
rarfile==4.1
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
semver==2.13.0
|
semver==2.13.0
|
||||||
signalrcore==0.9.5
|
signalrcore==0.9.5
|
||||||
|
|
Loading…
Reference in New Issue