diff options
author | Paweł Jastrzębski <pawelj@iosphe.re> | 2018-07-10 08:09:04 +0200 |
---|---|---|
committer | Paweł Jastrzębski <pawelj@iosphe.re> | 2018-07-10 08:09:04 +0200 |
commit | 7904662f25997ba1466c9f21c564f1f3f066fb9f (patch) | |
tree | 7b8ffec37720bde09c90ca932a4d8b2bc5879e75 /kindlecomicconverter | |
parent | Bump MAX_IMAGE_PIXELS (close #273) (diff) | |
download | kcc-7904662f25997ba1466c9f21c564f1f3f066fb9f.tar.gz kcc-7904662f25997ba1466c9f21c564f1f3f066fb9f.tar.bz2 kcc-7904662f25997ba1466c9f21c564f1f3f066fb9f.zip |
Let 7-Zip handle all archive operations
Diffstat (limited to 'kindlecomicconverter')
-rw-r--r-- | kindlecomicconverter/KCC_gui.py | 77 | ||||
-rw-r--r-- | kindlecomicconverter/cbxarchive.py | 89 | ||||
-rwxr-xr-x | kindlecomicconverter/comic2ebook.py | 40 | ||||
-rw-r--r-- | kindlecomicconverter/comicarchive.py | 81 | ||||
-rw-r--r-- | kindlecomicconverter/metadata.py | 71 | ||||
-rw-r--r-- | kindlecomicconverter/rarfile.py | 1990 | ||||
-rw-r--r-- | kindlecomicconverter/shared.py | 21 | ||||
-rw-r--r-- | kindlecomicconverter/startup.py | 4 |
8 files changed, 140 insertions, 2233 deletions
diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py index d3e6f10..d46353c 100644 --- a/kindlecomicconverter/KCC_gui.py +++ b/kindlecomicconverter/KCC_gui.py @@ -173,7 +173,7 @@ class VersionThread(QtCore.QThread): move(path[0], path[0] + '.exe') MW.hideProgressBar.emit() MW.modeConvert.emit(1) - Popen(path[0] + '.exe /SP- /silent /noicons', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) + Popen(path[0] + '.exe /SP- /silent /noicons', stdout=PIPE, stderr=STDOUT, shell=True) MW.forceShutdown.emit() except Exception: MW.addMessage.emit('Failed to download the update!', 'warning', False) @@ -238,6 +238,7 @@ class WorkerThread(QtCore.QThread): MW.addTrayMessage.emit('Conversion interrupted.', 'Critical') MW.modeConvert.emit(1) + # noinspection PyUnboundLocalVariable def run(self): MW.modeConvert.emit(0) @@ -477,20 +478,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow): if self.needClean: self.needClean = False GUI.jobList.clear() - if self.UnRAR: - if self.sevenza: - fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, - 'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf)') - else: - fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, - 'Comic (*.cbz *.cbr *.zip *.rar *.pdf)') + if self.sevenzip: + fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, + 'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf)') else: - if self.sevenza: - fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, - 'Comic (*.cbz *.cb7 *.zip *.7z *.pdf)') - else: - fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, - 'Comic (*.cbz *.zip *.pdf)') + fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, 'Comic (*.pdf)') for fname in fnames[0]: if fname != '': if sys.platform.startswith('win'): @@ -509,20 +501,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow): sname = sname.replace('/', '\\') self.lastPath = os.path.abspath(sname) else: - if self.UnRAR: - if self.sevenza: - fname = QtWidgets.QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath, - 'Comic (*.cbz *.cbr *.cb7)') - else: - fname = QtWidgets.QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath, - 'Comic (*.cbz *.cbr)') + if self.sevenzip: + fname = QtWidgets.QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath, + 'Comic (*.cbz *.cbr *.cb7)') else: - if self.sevenza: - fname = QtWidgets.QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath, - 'Comic (*.cbz *.cb7)') - else: - fname = QtWidgets.QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath, - 'Comic (*.cbz)') + fname = [''] + self.showDialog("Editor is disabled due to a lack of 7z.", 'error') if fname[0] != '': if sys.platform.startswith('win'): sname = fname[0].replace('/', '\\') @@ -812,11 +796,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow): if self.needClean: self.needClean = False GUI.jobList.clear() - formats = ['.cbz', '.zip', '.pdf'] - if self.UnRAR: - formats.extend(['.cbr', '.rar']) - if self.sevenza: - formats.extend(['.cb7', '.7z']) + formats = ['.pdf'] + if self.sevenzip: + formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar']) if os.path.isdir(message): GUI.jobList.addItem(message) GUI.jobList.scrollToBottom() @@ -852,10 +834,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow): os.chmod('/usr/local/bin/kindlegen', 0o755) except Exception: pass - kindleGenExitCode = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - if kindleGenExitCode.wait() == 0: + kindleGenExitCode = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True) + kindleGenExitCode.communicate() + if kindleGenExitCode.returncode == 0: self.kindleGen = True - versionCheck = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) + versionCheck = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True) for line in versionCheck.stdout: line = line.decode("utf-8") if 'Amazon kindlegen' in line: @@ -1002,22 +985,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow): self.addMessage('Since you are a new user of <b>KCC</b> please see few ' '<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.', 'info') - rarExitCode = Popen('unrar', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - rarExitCode = rarExitCode.wait() - if rarExitCode == 0 or rarExitCode == 1 or rarExitCode == 7: - self.UnRAR = True - else: - self.UnRAR = False - self.addMessage('Cannot find <a href="http://www.rarlab.com/rar_add.htm">UnRAR</a>!' - ' Processing of CBR/RAR files will be disabled.', 'warning') - sevenzaExitCode = Popen('7za', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - sevenzaExitCode = sevenzaExitCode.wait() - if sevenzaExitCode == 0 or sevenzaExitCode == 7: - self.sevenza = True + process = Popen('7z', stdout=PIPE, stderr=STDOUT, shell=True) + process.communicate() + if process.returncode == 0 or process.returncode == 7: + self.sevenzip = True else: - self.sevenza = False - self.addMessage('Cannot find <a href="http://www.7-zip.org/download.html">7za</a>!' - ' Processing of CB7/7Z files will be disabled.', 'warning') + self.sevenzip = False + self.addMessage('Cannot find <a href="http://www.7-zip.org/download.html">7z</a>!' + ' Processing of archives will be disabled.', 'warning') self.detectKindleGen(True) APP.messageFromOtherInstance.connect(self.handleMessage) @@ -1098,7 +1073,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow): class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): def loadData(self, file): self.parser = metadata.MetadataParser(file) - if self.parser.compressor == 'rar': + if self.parser.format == 'RAR': self.editorWidget.setEnabled(False) self.okButton.setEnabled(False) self.statusLabel.setText('CBR metadata are read-only.') diff --git a/kindlecomicconverter/cbxarchive.py b/kindlecomicconverter/cbxarchive.py deleted file mode 100644 index 94545ae..0000000 --- a/kindlecomicconverter/cbxarchive.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com> -# Copyright (c) 2013-2018 Pawel Jastrzebski <pawelj@iosphe.re> -# -# 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. -# - -import os -from zipfile import is_zipfile, ZipFile -from subprocess import STDOUT, PIPE -from psutil import Popen -from shutil import move -from . import rarfile -from .shared import check7ZFile as is_7zfile - - -class CBxArchive: - def __init__(self, fname): - self.fname = fname - if is_zipfile(fname): - self.compressor = 'zip' - elif rarfile.is_rarfile(fname): - self.compressor = 'rar' - elif is_7zfile(fname): - self.compressor = '7z' - else: - self.compressor = None - - def isCbxFile(self): - return self.compressor is not None - - def extractCBZ(self, targetdir): - cbzFile = ZipFile(self.fname) - filelist = [] - for f in cbzFile.namelist(): - if f.startswith('__MACOSX') or f.endswith('.DS_Store') or f.endswith('humbs.db'): - pass - elif f.endswith('/'): - os.makedirs(os.path.join(targetdir, f), exist_ok=True) - else: - filelist.append(f) - cbzFile.extractall(targetdir, filelist) - - def extractCBR(self, targetdir): - cbrFile = rarfile.RarFile(self.fname) - cbrFile.extractall(targetdir) - for root, _, filenames in os.walk(targetdir): - for filename in filenames: - if filename.startswith('__MACOSX') or filename.endswith('.DS_Store') or filename.endswith('humbs.db'): - os.remove(os.path.join(root, filename)) - - def extractCB7(self, targetdir): - output = Popen('7za x "' + self.fname + '" -xr!__MACOSX -xr!.DS_Store -xr!thumbs.db -xr!Thumbs.db -o"' + - targetdir + '"', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - extracted = False - for line in output.stdout: - if b"Everything is Ok" in line: - extracted = True - if not extracted: - raise OSError - - def extract(self, targetdir): - if self.compressor == 'rar': - self.extractCBR(targetdir) - elif self.compressor == 'zip': - self.extractCBZ(targetdir) - elif self.compressor == '7z': - self.extractCB7(targetdir) - adir = os.listdir(targetdir) - if 'ComicInfo.xml' in adir: - adir.remove('ComicInfo.xml') - if len(adir) == 1 and os.path.isdir(os.path.join(targetdir, adir[0])): - for f in os.listdir(os.path.join(targetdir, adir[0])): - move(os.path.join(targetdir, adir[0], f), targetdir) - os.rmdir(os.path.join(targetdir, adir[0])) - return targetdir diff --git a/kindlecomicconverter/comic2ebook.py b/kindlecomicconverter/comic2ebook.py index e118b7e..37e39d7 100755 --- a/kindlecomicconverter/comic2ebook.py +++ b/kindlecomicconverter/comic2ebook.py @@ -45,7 +45,7 @@ except ImportError: from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, sanitizeTrace from . import comic2panel from . import image -from . import cbxarchive +from . import comicarchive from . import pdfjpgextract from . import dualmetafix from . import metadata @@ -597,16 +597,12 @@ def getWorkFolder(afile): raise UserWarning("Failed to extract images from PDF file.") else: workdir = mkdtemp('', 'KCC-') - cbx = cbxarchive.CBxArchive(afile) - if cbx.isCbxFile(): - try: - path = cbx.extract(workdir) - except Exception: - rmtree(workdir, True) - raise UserWarning("Failed to extract archive.") - else: + try: + cbx = comicarchive.ComicArchive(afile) + path = cbx.extract(workdir) + except OSError as e: rmtree(workdir, True) - raise UserWarning("Failed to detect archive format.") + raise UserWarning(e.strerror) else: raise UserWarning("Failed to open source file/directory.") sanitizePermissions(path) @@ -1054,21 +1050,17 @@ def checkOptions(): def checkTools(source): source = source.upper() - if source.endswith('.CBR') or source.endswith('.RAR'): - rarExitCode = Popen('unrar', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - rarExitCode = rarExitCode.wait() - if rarExitCode != 0 and rarExitCode != 1 and rarExitCode != 7: - print('ERROR: UnRAR is missing!') - exit(1) - elif source.endswith('.CB7') or source.endswith('.7Z'): - sevenzaExitCode = Popen('7za', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - sevenzaExitCode = sevenzaExitCode.wait() - if sevenzaExitCode != 0 and sevenzaExitCode != 7: - print('ERROR: 7za is missing!') + if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \ + source.endswith('.ZIP') or source.endswith('.CBZ'): + process = Popen('7z', stdout=PIPE, stderr=STDOUT, shell=True) + process.communicate() + if process.returncode != 0 and process.returncode != 7: + print('ERROR: 7z is missing!') exit(1) if options.format == 'MOBI': - kindleGenExitCode = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - if kindleGenExitCode.wait() != 0: + kindleGenExitCode = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True) + kindleGenExitCode.communicate() + if kindleGenExitCode.returncode != 0: print('ERROR: KindleGen is missing!') exit(1) @@ -1215,7 +1207,7 @@ def makeMOBIWorker(item): try: if os.path.getsize(item) < 629145600: output = Popen('kindlegen -dont_append_source -locale en "' + item + '"', - stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) + stdout=PIPE, stderr=STDOUT, shell=True) for line in output.stdout: line = line.decode('utf-8') # ERROR: Generic error diff --git a/kindlecomicconverter/comicarchive.py b/kindlecomicconverter/comicarchive.py new file mode 100644 index 0000000..df9029c --- /dev/null +++ b/kindlecomicconverter/comicarchive.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com> +# Copyright (c) 2013-2018 Pawel Jastrzebski <pawelj@iosphe.re> +# +# 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. +# + +import os +from psutil import Popen +from shutil import move +from subprocess import STDOUT, PIPE +from xml.dom.minidom import parseString +from xml.parsers.expat import ExpatError + + +class ComicArchive: + def __init__(self, filepath): + self.filepath = filepath + self.type = None + if not os.path.isfile(self.filepath): + raise OSError('File not found.') + process = Popen('7z l -y -p1 "' + self.filepath + '"', stderr=STDOUT, stdout=PIPE, shell=True) + for line in process.stdout: + if b'Type =' in line: + self.type = line.rstrip().decode().split(' = ')[1].upper() + break + process.communicate() + if process.returncode != 0: + raise OSError('Archive is corrupted or encrypted.') + elif self.type not in ['7Z', 'RAR', 'ZIP']: + raise OSError('Unsupported archive format.') + + def extract(self, targetdir): + if not os.path.isdir(targetdir): + raise OSError('Target directory don\'t exist.') + process = Popen('7z x -y -xr!__MACOSX -xr!.DS_Store -xr!thumbs.db -xr!Thumbs.db -o"' + targetdir + '" "' + + self.filepath + '"', stdout=PIPE, stderr=STDOUT, shell=True) + process.communicate() + if process.returncode != 0: + raise OSError('Failed to extract archive.') + tdir = os.listdir(targetdir) + if 'ComicInfo.xml' in tdir: + tdir.remove('ComicInfo.xml') + if len(tdir) == 1 and os.path.isdir(os.path.join(targetdir, tdir[0])): + for f in os.listdir(os.path.join(targetdir, tdir[0])): + move(os.path.join(targetdir, tdir[0], f), targetdir) + os.rmdir(os.path.join(targetdir, tdir[0])) + return targetdir + + def addFile(self, sourcefile): + if self.type == 'RAR': + raise NotImplementedError + process = Popen('7z a -y "' + self.filepath + '" "' + sourcefile + '"', + stdout=PIPE, stderr=STDOUT, shell=True) + process.communicate() + if process.returncode != 0: + raise OSError('Failed to add the file.') + + def extractMetadata(self): + process = Popen('7z x -y -so "' + self.filepath + '" ComicInfo.xml', + stdout=PIPE, stderr=STDOUT, shell=True) + xml = process.communicate() + if process.returncode != 0: + raise OSError('Failed to extract archive.') + try: + return parseString(xml[0]) + except ExpatError: + return None diff --git a/kindlecomicconverter/metadata.py b/kindlecomicconverter/metadata.py index da16718..df87340 100644 --- a/kindlecomicconverter/metadata.py +++ b/kindlecomicconverter/metadata.py @@ -19,13 +19,11 @@ import os from xml.dom.minidom import parse, Document from re import compile -from zipfile import is_zipfile, ZipFile, ZIP_DEFLATED from subprocess import STDOUT, PIPE from psutil import Popen from tempfile import mkdtemp from shutil import rmtree -from .shared import removeFromZIP, check7ZFile as is_7zfile -from . import rarfile +from . import comicarchive class MetadataParser: @@ -42,47 +40,19 @@ class MetadataParser: 'MUid': '', 'Bookmarks': []} self.rawdata = None - self.compressor = None + self.format = None if self.source.endswith('.xml') and os.path.exists(self.source): self.rawdata = parse(self.source) self.parseXML() elif not self.source.endswith('.xml'): - if is_zipfile(self.source): - self.compressor = 'zip' - with ZipFile(self.source) as zip_file: - for member in zip_file.namelist(): - if member != 'ComicInfo.xml': - continue - with zip_file.open(member) as xml_file: - self.rawdata = parse(xml_file) - elif rarfile.is_rarfile(self.source): - self.compressor = 'rar' - with rarfile.RarFile(self.source) as rar_file: - for member in rar_file.namelist(): - if member != 'ComicInfo.xml': - continue - with rar_file.open(member) as xml_file: - self.rawdata = parse(xml_file) - elif is_7zfile(self.source): - self.compressor = '7z' - workdir = mkdtemp('', 'KCC-') - tmpXML = os.path.join(workdir, 'ComicInfo.xml') - output = Popen('7za e "' + self.source + '" ComicInfo.xml -o"' + workdir + '"', - stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - extracted = False - for line in output.stdout: - if b"Everything is Ok" in line or b"No files to process" in line: - extracted = True - if not extracted: - rmtree(workdir) - raise OSError('Failed to extract 7ZIP file.') - if os.path.isfile(tmpXML): - self.rawdata = parse(tmpXML) - rmtree(workdir) - else: - raise OSError('Failed to detect archive format.') - if self.rawdata: - self.parseXML() + try: + cbx = comicarchive.ComicArchive(self.source) + self.rawdata = cbx.extractMetadata() + self.format = cbx.type + except OSError as e: + raise UserWarning(e.strerror) + if self.rawdata: + self.parseXML() def parseXML(self): if len(self.rawdata.getElementsByTagName('Series')) != 0: @@ -154,20 +124,9 @@ class MetadataParser: tmpXML = os.path.join(workdir, 'ComicInfo.xml') with open(tmpXML, 'w', encoding='utf-8') as f: self.rawdata.writexml(f, encoding='utf-8') - if is_zipfile(self.source): - removeFromZIP(self.source, 'ComicInfo.xml') - with ZipFile(self.source, mode='a', compression=ZIP_DEFLATED) as zip_file: - zip_file.write(tmpXML, arcname=tmpXML.split(os.sep)[-1]) - elif rarfile.is_rarfile(self.source): - raise NotImplementedError - elif is_7zfile(self.source): - output = Popen('7za a "' + self.source + '" "' + tmpXML + '"', - stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) - extracted = False - for line in output.stdout: - if b"Everything is Ok" in line: - extracted = True - if not extracted: - rmtree(workdir) - raise OSError('Failed to modify 7ZIP file.') + try: + cbx = comicarchive.ComicArchive(self.source) + cbx.addFile(tmpXML) + except OSError as e: + raise UserWarning(e.strerror) rmtree(workdir) diff --git a/kindlecomicconverter/rarfile.py b/kindlecomicconverter/rarfile.py deleted file mode 100644 index afb19a7..0000000 --- a/kindlecomicconverter/rarfile.py +++ /dev/null @@ -1,1990 +0,0 @@ -# rarfile.py -# -# Copyright (c) 2005-2014 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 0 if you don't look at comments and want to - # avoid wasting time for parsing them - rarfile.NEED_COMMENTS = 1 - - # Set up to 1 if you don't want to deal with decoding comments - # from unknown encoding. rarfile will try couple of common - # encodings in sequence. - rarfile.UNICODE_COMMENTS = 0 - - # Set to 1 if you prefer timestamps to be datetime objects - # instead tuples - rarfile.USE_DATETIME = 0 - - # Set to '/' to be more compatible with zipfile - rarfile.PATH_SEP = '\\' - -For more details, refer to source. - -""" - -__version__ = '2.7-kcc' - -# export only interesting items -__all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile'] - -## -## Imports and compat - support both Python 2.x and 3.x -## - -import sys, os, struct, errno -from struct import pack, unpack -from binascii import crc32 -from tempfile import mkstemp -from subprocess import Popen, PIPE, STDOUT -from datetime import datetime - -# only needed for encryped headers -try: - from Crypto.Cipher import AES - try: - from hashlib import sha1 - except ImportError: - from sha import new as sha1 - _have_crypto = 1 -except ImportError: - _have_crypto = 0 - -# compat with 2.x -if sys.hexversion < 0x3000000: - # prefer 3.x behaviour - range = xrange - # py2.6 has broken bytes() - def bytes(s, enc): - return str(s) -else: - unicode = str - -# see if compat bytearray() is needed -try: - bytearray -except NameError: - import array - class bytearray: - def __init__(self, val = ''): - self.arr = array.array('B', val) - self.append = self.arr.append - self.__getitem__ = self.arr.__getitem__ - self.__len__ = self.arr.__len__ - def decode(self, *args): - return self.arr.tostring().decode(*args) - -# Optimized .readinto() requires memoryview -try: - memoryview - have_memoryview = 1 -except NameError: - have_memoryview = 0 - -# Struct() for older python -try: - from struct import Struct -except ImportError: - class Struct: - def __init__(self, fmt): - self.format = fmt - self.size = struct.calcsize(fmt) - def unpack(self, buf): - return unpack(self.format, buf) - def unpack_from(self, buf, ofs = 0): - return unpack(self.format, buf[ofs : ofs + self.size]) - def pack(self, *args): - return pack(self.format, *args) - -# file object superclass -try: - from io import RawIOBase -except ImportError: - class RawIOBase(object): - def close(self): - pass - - -## -## 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',) - -#: whether to speed up decompression by using tmp archive -USE_EXTRACT_HACK = 0 - -#: limit the filesize for tmp archive usage -HACK_SIZE_LIMIT = 20*1024*1024 - -#: whether to parse file/archive comments. -NEED_COMMENTS = 1 - -#: whether to convert comments to unicode strings -UNICODE_COMMENTS = 0 - -#: Convert RAR time tuple into datetime() object -USE_DATETIME = 0 - -#: 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 - -## -## internal constants -## - -RAR_ID = bytes("Rar!\x1a\x07\x00", 'ascii') -RAR5_ID = bytes("Rar!\x1a\x07\x01", 'ascii') -ZERO = bytes("\0", 'ascii') -EMPTY = bytes("", 'ascii') - -S_BLK_HDR = Struct('<HBHH') -S_FILE_HDR = Struct('<LLBLLBBHL') -S_LONG = Struct('<L') -S_SHORT = Struct('<H') -S_BYTE = Struct('<B') -S_COMMENT_HDR = Struct('<HBBH') - -## -## Public interface -## - -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 RarUnknownError(RarExecError): - """Unknown exit code""" -class RarSignalExit(RarExecError): - """Unrar exited with signal""" -class RarCannotExec(RarExecError): - """Executable not found.""" - - -def is_rarfile(xfile): - '''Check quickly whether file is rar archive.''' - with open(xfile, 'rb') as fh: - buf = fh.read(len(RAR_ID)) - if buf == RAR_ID or buf == RAR5_ID: - return True - else: - return False - - -class RarInfo(object): - r'''An entry in rar archive. - - :mod:`zipfile`-compatible fields: - - filename - File name with relative path. - Default path separator is '\\', to change set rarfile.PATH_SEP. - Always unicode string. - date_time - Modification time, tuple of (year, month, day, hour, minute, second). - Or datetime() object if USE_DATETIME is set. - file_size - Uncompressed size. - compress_size - Compressed size. - CRC - CRC-32 of uncompressed file, unsigned int. - comment - File comment. Byte string or None. Use UNICODE_COMMENTS - to get automatic decoding to unicode. - volume - Volume nr, starting from 0. - - RAR-specific fields: - - compress_type - Compression method: 0x30 - 0x35. - extract_version - Minimal Rar version needed for decompressing. - host_os - Host OS type, one of RAR_OS_* constants. - mode - File attributes. May be either dos-style or unix-style, depending on host_os. - volume_file - Volume file name, where file starts. - mtime - Optional time field: Modification time, with float seconds. - Same as .date_time but with more precision. - ctime - Optional time field: creation time, with float seconds. - atime - Optional time field: last access time, with float seconds. - arctime - Optional time field: archival time, with float seconds. - - Internal fields: - - type - One of RAR_BLOCK_* types. Only entries with type==RAR_BLOCK_FILE are shown in .infolist(). - flags - For files, RAR_FILE_* bits. - ''' - - __slots__ = ( - # zipfile-compatible fields - 'filename', - 'file_size', - 'compress_size', - 'date_time', - 'comment', - 'CRC', - 'volume', - 'orig_filename', # bytes in unknown encoding - - # rar-specific fields - 'extract_version', - 'compress_type', - 'host_os', - 'mode', - 'type', - 'flags', - - # optional extended time fields - # tuple where the sec is float, or datetime(). - 'mtime', # same as .date_time - 'ctime', - 'atime', - 'arctime', - - # RAR internals - 'name_size', - 'header_size', - 'header_crc', - 'file_offset', - 'add_size', - 'header_data', - 'header_base', - 'header_offset', - 'salt', - 'volume_file', - ) - - def isdir(self): - '''Returns True if the 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): - return self.flags & RAR_FILE_PASSWORD - - -class RarFile(object): - '''Parse RAR structure, provide access to files in archive. - ''' - - #: Archive comment. Byte string or None. Use :data:`UNICODE_COMMENTS` - #: to get automatic decoding to unicode. - 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.comment = None - self._charset = charset or DEFAULT_CHARSET - self._info_callback = info_callback - - self._info_list = [] - self._info_map = {} - self._needs_password = False - self._password = None - self._crc_check = crc_check - self._vol_list = [] - - if errors == "stop": - self._strict = False - elif errors == "strict": - self._strict = True - else: - raise ValueError("Invalid value for 'errors' parameter.") - - self._main = None - - if mode != "r": - raise NotImplementedError("RarFile supports only mode=r") - - self._parse() - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def setpassword(self, password): - '''Sets the password to use when extracting.''' - self._password = password - if not self._main: - self._parse() - - def needs_password(self): - '''Returns True if any archive entries require password for extraction.''' - return self._needs_password - - def namelist(self): - '''Return list of filenames in archive.''' - return [f.filename for f in self._info_list] - - def infolist(self): - '''Return RarInfo objects for all files/directories in archive.''' - return self._info_list - - 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._vol_list - - def getinfo(self, fname): - '''Return RarInfo for file.''' - - if isinstance(fname, RarInfo): - return fname - - # 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: "+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) - - if inf.flags & RAR_FILE_SPLIT_BEFORE: - raise NeedFirstVolume("Partial file, please start from first volume: " + 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 - - # is temp write usable? - use_hack = 1 - if not self._main: - use_hack = 0 - elif self._main.flags & (RAR_MAIN_SOLID | RAR_MAIN_PASSWORD): - use_hack = 0 - elif inf.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): - 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: - return self._open_clear(inf) - elif use_hack: - return self._open_hack(inf, psw) - else: - return self._open_unrar(self.rarfile, 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. - """ - - f = self.open(fname, 'r', psw) - try: - return f.read() - finally: - f.close() - - def close(self): - """Release open resources.""" - pass - - def printdir(self): - """Print archive file list to stdout.""" - for f in self._info_list: - 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(self.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. - """ - return self._parse_error - - ## - ## private methods - ## - - def _set_error(self, msg, *args): - if args: - msg = msg % args - self._parse_error = msg - if self._strict: - raise BadRarFile(msg) - - # store entry - def _process_entry(self, 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) - # remember if any items require password - if item.needs_password(): - self._needs_password = True - elif len(self._info_list) > 0: - # final crc is in last block - old = self._info_list[-1] - old.CRC = item.CRC - old.compress_size += item.compress_size - - # parse new-style comment - if item.type == RAR_BLOCK_SUB and item.filename == 'CMT': - if not NEED_COMMENTS: - pass - elif 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 self._info_callback: - self._info_callback(item) - - # read rar - def _parse(self): - 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 - id = fd.read(len(RAR_ID)) - if id != RAR_ID and id != RAR5_ID: - raise NotRarFile("Not a Rar archive: "+self.rarfile) - - volume = 0 # first vol (.rar) is 0 - more_vols = 0 - endarc = 0 - 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 - more_vols = 0 - endarc = 0 - 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: - self._main = None - break - elif h.type == RAR_BLOCK_ENDARC: - more_vols = h.flags & RAR_ENDARC_NEXT_VOLUME - endarc = 1 - elif h.type == RAR_BLOCK_FILE: - # RAR 2.x does not write RAR_BLOCK_ENDARC - if h.flags & RAR_FILE_SPLIT_AFTER: - more_vols = 1 - # 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") - - # store it - self._process_entry(h) - - # go to next header - if h.add_size > 0: - fd.seek(h.file_offset + h.add_size, 0) - - # AES encrypted headers - _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) - - # read single header - def _parse_header(self, fd): - try: - # handle encrypted headers - if self._main and self._main.flags & RAR_MAIN_PASSWORD: - 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 - - # common header - def _parse_block_header(self, fd): - h = RarInfo() - h.header_offset = fd.tell() - h.comment = None - - # 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 - h.header_base = S_BLK_HDR.size - pos = S_BLK_HDR.size - - # read full header - if h.header_size > S_BLK_HDR.size: - h.header_data = buf + fd.read(h.header_size - S_BLK_HDR.size) - else: - h.header_data = buf - h.file_offset = fd.tell() - - # unexpected EOF? - if len(h.header_data) != h.header_size: - self._set_error('Unexpected EOF when reading header') - return None - - # block has data assiciated with it? - if h.flags & RAR_LONG_BLOCK: - h.add_size = S_LONG.unpack_from(h.header_data, pos)[0] - 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: - h.header_base += 6 - if h.flags & RAR_MAIN_ENCRYPTVER: - h.header_base += 1 - if h.flags & RAR_MAIN_COMMENT: - self._parse_subblocks(h, h.header_base) - self.comment = h.comment - elif h.type == RAR_BLOCK_FILE: - self._parse_file_header(h, pos) - elif h.type == RAR_BLOCK_SUB: - self._parse_file_header(h, pos) - h.header_base = h.header_size - elif h.type == RAR_BLOCK_OLD_AUTH: - h.header_base += 8 - elif h.type == RAR_BLOCK_OLD_EXTRA: - h.header_base += 7 - else: - h.header_base = h.header_size - - # check crc - if h.type == RAR_BLOCK_OLD_SUB: - crcdat = h.header_data[2:] + fd.read(h.add_size) - else: - crcdat = h.header_data[2:h.header_base] - - calc_crc = 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, pos): - fld = S_FILE_HDR.unpack_from(h.header_data, pos) - 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.extract_version = fld[5] - h.compress_type = fld[6] - h.name_size = fld[7] - h.mode = fld[8] - pos += S_FILE_HDR.size - - if h.flags & RAR_FILE_LARGE: - h1 = S_LONG.unpack_from(h.header_data, pos)[0] - h2 = S_LONG.unpack_from(h.header_data, pos + 4)[0] - h.compress_size |= h1 << 32 - h.file_size |= h2 << 32 - pos += 8 - h.add_size = h.compress_size - - name = h.header_data[pos : pos + h.name_size ] - pos += h.name_size - 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 = h.header_data[pos : pos + 8] - pos += 8 - else: - h.salt = None - - # optional extended time stamps - if h.flags & RAR_FILE_EXTTIME: - pos = self._parse_ext_time(h, pos) - else: - h.mtime = h.atime = h.ctime = h.arctime = None - - # base header end - h.header_base = pos - - if h.flags & RAR_FILE_COMMENT: - self._parse_subblocks(h, pos) - - # convert timestamps - if USE_DATETIME: - h.date_time = to_datetime(h.date_time) - h.mtime = to_datetime(h.mtime) - h.atime = to_datetime(h.atime) - h.ctime = to_datetime(h.ctime) - h.arctime = to_datetime(h.arctime) - - # .mtime is .date_time with more precision - if h.mtime: - if USE_DATETIME: - h.date_time = h.mtime - else: - # keep seconds int - h.date_time = h.mtime[:5] + (int(h.mtime[5]),) - - return pos - - # find old-style comment subblock - def _parse_subblocks(self, h, pos): - hdata = h.header_data - 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 = rar_decompress(ver, meth, data, declen, sflags, - crc, self._password) - if not self._crc_check: - h.comment = self._decode_comment(cmt) - elif crc32(cmt) & 0xFFFF == crc: - h.comment = self._decode_comment(cmt) - - pos = pos_next - - def _parse_ext_time(self, h, pos): - data = h.header_data - - # 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 - - h.mtime, pos = self._parse_xtime(flags >> 3*4, data, pos, h.date_time) - h.ctime, pos = self._parse_xtime(flags >> 2*4, data, pos) - h.atime, pos = self._parse_xtime(flags >> 1*4, data, pos) - h.arctime, pos = self._parse_xtime(flags >> 0*4, data, pos) - return pos - - def _parse_xtime(self, flag, data, pos, dostime = None): - unit = 10000000.0 # 100 ns units - if flag & 8: - if not dostime: - t = S_LONG.unpack_from(data, pos)[0] - dostime = parse_dos_time(t) - pos += 4 - rem = 0 - cnt = flag & 3 - for i in range(cnt): - b = S_BYTE.unpack_from(data, pos)[0] - rem = (b << 16) | (rem >> 8) - pos += 1 - sec = dostime[5] + rem / unit - if flag & 4: - sec += 1 - dostime = dostime[:5] + (sec,) - return dostime, pos - - # 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 self._next_newvol(volfile) - return self._next_oldvol(volfile) - - # new-style next volume - def _next_newvol(self, volfile): - i = len(volfile) - 1 - while i >= 0: - if volfile[i] >= '0' and volfile[i] <= '9': - return self._inc_volname(volfile, i) - i -= 1 - raise BadRarName("Cannot construct volume name: "+volfile) - - # old-style next volume - def _next_oldvol(self, volfile): - # rar -> r00 - if volfile[-4:].lower() == '.rar': - return volfile[:-2] + '00' - return self._inc_volname(volfile, len(volfile) - 1) - - # increase digits with carry, otherwise just increment char - def _inc_volname(self, 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) - - def _open_clear(self, inf): - return DirectReader(self, inf) - - # 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 = None): - BSIZE = 32*1024 - - 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: - # create main header: crc, type, flags, size, res1, res2 - mh = S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + ZERO * (2+4) - tmpf.write(RAR_ID + mh) - 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.close() - rf.close() - except: - rf.close() - tmpf.close() - os.unlink(tmpname) - raise - - return self._open_unrar(tmpname, inf, psw, tmpname) - - def _read_comment_v3(self, inf, psw=None): - - # read data - rf = XFile(inf.volume_file) - rf.seek(inf.file_offset) - data = rf.read(inf.compress_size) - rf.close() - - # decompress - cmt = rar_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 = crc32(cmt) - if crc < 0: - crc += (long(1) << 32) - if crc != inf.CRC: - return None - - return self._decode_comment(cmt) - - # extract using unrar - def _open_unrar(self, rarfile, inf, psw = None, tmpfile = None): - if is_filelike(rarfile): - raise ValueError("Cannot use unrar directly on memory buffer") - cmd = [UNRAR_TOOL] + list(OPEN_ARGS) - add_password_arg(cmd, psw) - cmd.append(rarfile) - - # not giving filename avoids encoding related problems - if not tmpfile: - 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) - - 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): - if UNICODE_COMMENTS: - return self._decode(val) - return val - - # 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) - - # rar file - cmd.append(self.rarfile) - - # 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) - -## -## Utility classes -## - -class UnicodeFilename: - """Handle 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): - try: - c = self.encdata[self.encpos] - self.encpos += 1 - return c - except IndexError: - self.failed = 1 - return 0 - - def std_byte(self): - try: - return self.std_name[self.pos] - except IndexError: - self.failed = 1 - return ord('?') - - def put(self, lo, hi): - self.buf.append(lo) - self.buf.append(hi) - self.pos += 1 - - def decode(self): - 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 i in range((n & 0x7f) + 2): - lo = (self.std_byte() + c) & 0xFF - self.put(lo, hi) - else: - for i 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. - - If :mod:`io` module is available (Python 2.6+, 3.x), then this calls - will inherit from :class:`io.RawIOBase` class. This makes line-based - access available: :meth:`RarExtFile.readline` and ``for ln in f``. - """ - - #: Filename of the archive entry - name = None - - def __init__(self, rf, inf): - RawIOBase.__init__(self) - - # standard io.* properties - self.name = inf.filename - self.mode = 'rb' - - self.rf = rf - self.inf = inf - self.crc_check = rf._crc_check - self.fd = None - self.CRC = 0 - self.remain = 0 - self.returncode = 0 - - self._open() - - def _open(self): - if self.fd: - self.fd.close() - self.fd = None - self.CRC = 0 - 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.CRC = crc32(data, self.CRC) - 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.""" - if not self.crc_check: - return - if self.returncode: - check_returncode(self, '') - if self.remain != 0: - raise BadRarFile("Failed the read enough data") - crc = self.CRC - if crc < 0: - crc += (long(1) << 32) - if crc != self.inf.CRC: - raise BadRarFile("Corrupt file - CRC check failed: " + self.inf.filename) - - def _read(self, cnt): - """Actual read that gets sanitized cnt.""" - - def close(self): - """Close open resources.""" - - RawIOBase.close(self) - - 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. - """ - - data = self.read(len(buf)) - n = len(data) - try: - buf[:n] = data - except TypeError: - import array - if not isinstance(buf, array.array): - raise - buf[:n] = array.array(buf.typecode, data) - return n - - 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.crc_check = 0 - - 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: - # process old data ? - #self._skip(fsize - cur_ofs) - # 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 - RarExtFile.__init__(self, 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): - RarExtFile._open(self) - - # 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() - RarExtFile.close(self) - - if self.tempfile: - try: - os.unlink(self.tempfile) - except OSError: - pass - self.tempfile = None - - if have_memoryview: - 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 - if self.crc_check: - self.CRC = crc32(vbuf[got : got + res], self.CRC) - self.remain -= res - got += res - return got - - -class DirectReader(RarExtFile): - """Read uncompressed data directly from archive.""" - - def _open(self): - RarExtFile._open(self) - - self.volfile = self.inf.volume_file - self.fd = XFile(self.volfile, 0) - self.fd.seek(self.inf.header_offset, 0) - self.cur = self.rf._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.rf._next_volname(self.volfile) - fd = open(self.volfile, "rb", 0) - self.fd = fd - - # loop until first file header - while 1: - cur = self.rf._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 - - if have_memoryview: - 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 - - # lenght 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 - if self.crc_check: - self.CRC = crc32(vbuf[got : got + res], self.CRC) - self.cur_avail -= res - self.remain -= res - got += res - return got - - -class HeaderDecrypt: - """File-like object that decrypts from another file""" - def __init__(self, f, key, iv): - self.f = f - self.ciph = AES.new(key, AES.MODE_CBC, iv) - self.buf = EMPTY - - def tell(self): - return self.f.tell() - - def read(self, cnt=None): - 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 - BLK = self.ciph.block_size - while cnt > 0: - enc = self.f.read(BLK) - if len(enc) < BLK: - 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): - __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): - return self._fd.read(n) - def tell(self): - return self._fd.tell() - def seek(self, ofs, whence=0): - return self._fd.seek(ofs, whence) - def readinto(self, dst): - return self._fd.readinto(dst) - def close(self): - if self._need_close: - self._fd.close() - def __enter__(self): - return self - def __exit__(self, typ, val, tb): - self.close() - -## -## Utility functions -## - -def is_filelike(obj): - if isinstance(obj, str) or isinstance(obj, 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.""" - - seed = psw.encode('utf-16le') + salt - iv = EMPTY - h = sha1() - for i in range(16): - for j in range(0x4000): - cnt = S_LONG.pack(i*0x4000 + j) - h.update(seed + 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 rar_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 = bytes('data', 'ascii') - 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 = 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, xs = t - s = int(xs) - us = int(1000000 * (xs - s)) - - # assume the values are valid - try: - return datetime(year, mon, day, h, m, s, us) - 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, us) - except ValueError: - day = 28 - return datetime(year, mon, day, h, m, s, us) - -def parse_dos_time(stamp): - """Parse standard 32-bit DOS timestamp.""" - - sec = stamp & 0x1F; stamp = stamp >> 5 - min = stamp & 0x3F; stamp = stamp >> 6 - hr = stamp & 0x1F; stamp = stamp >> 5 - day = stamp & 0x1F; stamp = stamp >> 5 - mon = stamp & 0x0F; stamp = stamp >> 4 - yr = (stamp & 0x7F) + 1980 - return (yr, mon, day, hr, min, 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: - ex = sys.exc_info()[1] - if ex.errno == errno.ENOENT: - raise RarCannotExec("Unrar not installed? (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, err = 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 - if code == 9: - return - - # map return code to exception class - errmap = [None, - RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError, - RarWriteError, RarOpenError, RarUserError, RarMemoryError, - RarCreateError, RarNoFilesError] # codes from rar.txt - 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) - -# -# Check if unrar works -# - -try: - # does UNRAR_TOOL work? - custom_check([UNRAR_TOOL], True) -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 - pass - diff --git a/kindlecomicconverter/shared.py b/kindlecomicconverter/shared.py index bdfd3e7..32f656c 100644 --- a/kindlecomicconverter/shared.py +++ b/kindlecomicconverter/shared.py @@ -24,7 +24,6 @@ from html.parser import HTMLParser from distutils.version import StrictVersion from shutil import rmtree, copy from tempfile import mkdtemp -from zipfile import ZipFile, ZIP_DEFLATED from re import split from traceback import format_tb @@ -87,26 +86,6 @@ def md5Checksum(fpath): return m.hexdigest() -def check7ZFile(fpath): - with open(fpath, 'rb') as fh: - header = fh.read(6) - return header == b"7z\xbc\xaf'\x1c" - - -def removeFromZIP(zipfname, *filenames): - tempdir = mkdtemp('', 'KCC-') - try: - tempname = os.path.join(tempdir, 'KCC.zip') - with ZipFile(zipfname, 'r') as zipread: - with ZipFile(tempname, 'w', compression=ZIP_DEFLATED) as zipwrite: - for item in zipread.infolist(): - if item.filename not in filenames: - zipwrite.writestr(item, zipread.read(item.filename)) - copy(tempname, zipfname) - finally: - rmtree(tempdir, True) - - def sanitizeTrace(traceback): return ''.join(format_tb(traceback))\ .replace('C:/projects/kcc/', '') \ diff --git a/kindlecomicconverter/startup.py b/kindlecomicconverter/startup.py index 158f520..4af160e 100644 --- a/kindlecomicconverter/startup.py +++ b/kindlecomicconverter/startup.py @@ -30,14 +30,14 @@ def start(): os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = "1" KCCAplication = KCC_gui.QApplicationMessaging(sys.argv) if KCCAplication.isRunning(): - for i in range (1, len(sys.argv)): + for i in range(1, len(sys.argv)): KCCAplication.sendMessage(sys.argv[i]) else: KCCAplication.sendMessage('ARISE') else: KCCWindow = KCC_gui.QMainWindowKCC() KCCUI = KCC_gui.KCCGUI(KCCAplication, KCCWindow) - for i in range (1, len(sys.argv)): + for i in range(1, len(sys.argv)): KCCUI.handleMessage(sys.argv[i]) sys.exit(KCCAplication.exec_()) |