From 8bfffc74e2e9204fb08d82466a9875db9b91dddd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 5 Jan 2010 17:16:46 -0700 Subject: [PATCH] Fix #4426 (Error opening epub for view) --- src/calibre/utils/zipfile.py | 163 ++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 79 deletions(-) diff --git a/src/calibre/utils/zipfile.py b/src/calibre/utils/zipfile.py index 351e033f1b..56e96ee8eb 100644 --- a/src/calibre/utils/zipfile.py +++ b/src/calibre/utils/zipfile.py @@ -1,9 +1,10 @@ """ -Read and write ZIP files. Modified by Kovid Goyal to support replacing files in +Read and write ZIP files. Modified by Kovid Goyal to support replacing files in a zip archive. """ from __future__ import with_statement from calibre.ptempfile import TemporaryDirectory +from calibre import sanitize_file_name import struct, os, time, sys, shutil import binascii, cStringIO @@ -789,80 +790,80 @@ class ZipFile: if self.debug > 2: print "total", total - - def _calculate_file_offsets(self): - for zip_info in self.filelist: - self.fp.seek(zip_info.header_offset, 0) - fheader = self.fp.read(30) - if fheader[0:4] != stringFileHeader: - raise BadZipfile, "Bad magic number for file header" - fheader = struct.unpack(structFileHeader, fheader) - # file_offset is computed here, since the extra field for - # the central directory and for the local file header - # refer to different fields, and they can have different - # lengths - file_offset = (zip_info.header_offset + 30 - + fheader[_FH_FILENAME_LENGTH] - + fheader[_FH_EXTRA_FIELD_LENGTH]) - fname = self.fp.read(fheader[_FH_FILENAME_LENGTH]) - if fname != zip_info.orig_filename: - raise RuntimeError( - 'File name in directory "%s" and header "%s" differ.' % ( - zip_info.orig_filename, fname)) - - zip_info.file_offset = file_offset - - def replace(self, filename, arcname=None, compress_type=None): - """Delete arcname, and put the bytes from filename into the - archive under the name arcname.""" - deleteName = arcname - if deleteName is None: - deleteName = filename - self.delete(deleteName) - self.write(filename, arcname, compress_type) - - def replacestr(self, zinfo, bytes): - """Delete zinfo.filename, and write a new file into the archive. The - contents is the string 'bytes'.""" - self.delete(zinfo.filename) - self.writestr(zinfo, bytes) - def delete(self, name): - """Delete the file from the archive. If it appears multiple - times only the first instance will be deleted.""" - for i in range (0, len(self.filelist)): - if self.filelist[i].filename == name: - if self.debug: - print "Removing", name - deleted_offset = self.filelist[i].header_offset + def _calculate_file_offsets(self): + for zip_info in self.filelist: + self.fp.seek(zip_info.header_offset, 0) + fheader = self.fp.read(30) + if fheader[0:4] != stringFileHeader: + raise BadZipfile, "Bad magic number for file header" + fheader = struct.unpack(structFileHeader, fheader) + # file_offset is computed here, since the extra field for + # the central directory and for the local file header + # refer to different fields, and they can have different + # lengths + file_offset = (zip_info.header_offset + 30 + + fheader[_FH_FILENAME_LENGTH] + + fheader[_FH_EXTRA_FIELD_LENGTH]) + fname = self.fp.read(fheader[_FH_FILENAME_LENGTH]) + if fname != zip_info.orig_filename: + raise RuntimeError( + 'File name in directory "%s" and header "%s" differ.' % ( + zip_info.orig_filename, fname)) + + zip_info.file_offset = file_offset + + def replace(self, filename, arcname=None, compress_type=None): + """Delete arcname, and put the bytes from filename into the + archive under the name arcname.""" + deleteName = arcname + if deleteName is None: + deleteName = filename + self.delete(deleteName) + self.write(filename, arcname, compress_type) + + def replacestr(self, zinfo, bytes): + """Delete zinfo.filename, and write a new file into the archive. The + contents is the string 'bytes'.""" + self.delete(zinfo.filename) + self.writestr(zinfo, bytes) + + def delete(self, name): + """Delete the file from the archive. If it appears multiple + times only the first instance will be deleted.""" + for i in range (0, len(self.filelist)): + if self.filelist[i].filename == name: + if self.debug: + print "Removing", name + deleted_offset = self.filelist[i].header_offset deleted_size = (self.filelist[i].file_offset - self.filelist[i].header_offset) + self.filelist[i].compress_size - zinfo_size = struct.calcsize(structCentralDir) + len(self.filelist[i].filename) + len(self.filelist[i].extra) - # Remove the file's data from the archive. - current_offset = self.fp.tell() - self.fp.seek(0, 2) - archive_size = self.fp.tell() - self.fp.seek(deleted_offset + deleted_size) - buf = self.fp.read() - self.fp.seek(deleted_offset) - self.fp.write(buf) - self.fp.truncate(archive_size - deleted_size - zinfo_size) - if current_offset > deleted_offset + deleted_size: - current_offset -= deleted_size - elif current_offset > deleted_offset: - current_offset = deleted_offset - self.fp.seek(current_offset, 0) - # Remove file from central directory. - del self.filelist[i] - # Adjust the remaining offsets in the central directory. - for j in range (i, len(self.filelist)): - if self.filelist[j].header_offset > deleted_offset: - self.filelist[j].header_offset -= deleted_size - if self.filelist[j].file_offset > deleted_offset: + zinfo_size = struct.calcsize(structCentralDir) + len(self.filelist[i].filename) + len(self.filelist[i].extra) + # Remove the file's data from the archive. + current_offset = self.fp.tell() + self.fp.seek(0, 2) + archive_size = self.fp.tell() + self.fp.seek(deleted_offset + deleted_size) + buf = self.fp.read() + self.fp.seek(deleted_offset) + self.fp.write(buf) + self.fp.truncate(archive_size - deleted_size - zinfo_size) + if current_offset > deleted_offset + deleted_size: + current_offset -= deleted_size + elif current_offset > deleted_offset: + current_offset = deleted_offset + self.fp.seek(current_offset, 0) + # Remove file from central directory. + del self.filelist[i] + # Adjust the remaining offsets in the central directory. + for j in range (i, len(self.filelist)): + if self.filelist[j].header_offset > deleted_offset: + self.filelist[j].header_offset -= deleted_size + if self.filelist[j].file_offset > deleted_offset: self.filelist[j].file_offset -= deleted_size - self._didModify = True - return - if self.debug: - print name, "not in archive" + self._didModify = True + return + if self.debug: + print name, "not in archive" def namelist(self): """Return a list of file names in the archive.""" @@ -1035,10 +1036,14 @@ class ZipFile: os.unlink(upperdirs) if upperdirs and not os.path.exists(upperdirs): os.makedirs(upperdirs) - + source = self.open(member, pwd=pwd) if not os.path.exists(targetpath): # Could be a previously automatically created directory - target = open(targetpath, "wb") + try: + target = open(targetpath, "wb") + except IOError: + targetpath = sanitize_file_name(targetpath) + target = open(targetpath, "wb") shutil.copyfileobj(source, target) source.close() target.close() @@ -1179,7 +1184,7 @@ class ZipFile: zinfo.file_size)) self.filelist.append(zinfo) self.NameToInfo[zinfo.filename] = zinfo - + def add_dir(self, path, prefix=''): ''' Add a directory recursively to the zip file with an optional prefix. @@ -1195,10 +1200,10 @@ class ZipFile: if os.path.isdir(f): self.add_dir(f, prefix=arcname) else: - self.write(f, arcname) + self.write(f, arcname) finally: os.chdir(cwd) - + def __del__(self): """Call the "close()" method in case the user forgot.""" @@ -1294,7 +1299,7 @@ class ZipFile: if self.debug > 0: msg = 'Archive comment is too long; truncating to %d bytes' \ % ZIP_MAX_COMMENT - print msg + print msg self.comment = self.comment[:ZIP_MAX_COMMENT] endrec = struct.pack(structEndArchive, stringEndArchive, @@ -1314,7 +1319,7 @@ def safe_replace(zipstream, name, datastream): Replace a file in a zip file in a safe manner. This proceeds by extracting and re-creating the zipfile. This is neccessary because :method:`ZipFile.replace` sometimes created corrupted zip files. - + :param zipstream: Stream from a zip file :param name: The name of the file to replace :param datastream: The data to replace the file with.