]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/zipfile.py
add hg and python
[plan9front.git] / sys / lib / python / zipfile.py
1 """
2 Read and write ZIP files.
3 """
4 import struct, os, time, sys
5 import binascii, cStringIO
6
7 try:
8     import zlib # We may need its compression method
9 except ImportError:
10     zlib = None
11
12 __all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile",
13            "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ]
14
15 class BadZipfile(Exception):
16     pass
17
18
19 class LargeZipFile(Exception):
20     """
21     Raised when writing a zipfile, the zipfile requires ZIP64 extensions
22     and those extensions are disabled.
23     """
24
25 error = BadZipfile      # The exception raised by this module
26
27 ZIP64_LIMIT= (1 << 31) - 1
28
29 # constants for Zip file compression methods
30 ZIP_STORED = 0
31 ZIP_DEFLATED = 8
32 # Other ZIP compression methods not supported
33
34 # Here are some struct module formats for reading headers
35 structEndArchive = "<4s4H2lH"     # 9 items, end of archive, 22 bytes
36 stringEndArchive = "PK\005\006"   # magic number for end of archive record
37 structCentralDir = "<4s4B4HlLL5HLl"# 19 items, central directory, 46 bytes
38 stringCentralDir = "PK\001\002"   # magic number for central directory
39 structFileHeader = "<4s2B4HlLL2H"  # 12 items, file header record, 30 bytes
40 stringFileHeader = "PK\003\004"   # magic number for file header
41 structEndArchive64Locator = "<4slql" # 4 items, locate Zip64 header, 20 bytes
42 stringEndArchive64Locator = "PK\x06\x07" # magic token for locator header
43 structEndArchive64 = "<4sqhhllqqqq" # 10 items, end of archive (Zip64), 56 bytes
44 stringEndArchive64 = "PK\x06\x06" # magic token for Zip64 header
45
46
47 # indexes of entries in the central directory structure
48 _CD_SIGNATURE = 0
49 _CD_CREATE_VERSION = 1
50 _CD_CREATE_SYSTEM = 2
51 _CD_EXTRACT_VERSION = 3
52 _CD_EXTRACT_SYSTEM = 4                  # is this meaningful?
53 _CD_FLAG_BITS = 5
54 _CD_COMPRESS_TYPE = 6
55 _CD_TIME = 7
56 _CD_DATE = 8
57 _CD_CRC = 9
58 _CD_COMPRESSED_SIZE = 10
59 _CD_UNCOMPRESSED_SIZE = 11
60 _CD_FILENAME_LENGTH = 12
61 _CD_EXTRA_FIELD_LENGTH = 13
62 _CD_COMMENT_LENGTH = 14
63 _CD_DISK_NUMBER_START = 15
64 _CD_INTERNAL_FILE_ATTRIBUTES = 16
65 _CD_EXTERNAL_FILE_ATTRIBUTES = 17
66 _CD_LOCAL_HEADER_OFFSET = 18
67
68 # indexes of entries in the local file header structure
69 _FH_SIGNATURE = 0
70 _FH_EXTRACT_VERSION = 1
71 _FH_EXTRACT_SYSTEM = 2                  # is this meaningful?
72 _FH_GENERAL_PURPOSE_FLAG_BITS = 3
73 _FH_COMPRESSION_METHOD = 4
74 _FH_LAST_MOD_TIME = 5
75 _FH_LAST_MOD_DATE = 6
76 _FH_CRC = 7
77 _FH_COMPRESSED_SIZE = 8
78 _FH_UNCOMPRESSED_SIZE = 9
79 _FH_FILENAME_LENGTH = 10
80 _FH_EXTRA_FIELD_LENGTH = 11
81
82 def is_zipfile(filename):
83     """Quickly see if file is a ZIP file by checking the magic number."""
84     try:
85         fpin = open(filename, "rb")
86         endrec = _EndRecData(fpin)
87         fpin.close()
88         if endrec:
89             return True                 # file has correct magic number
90     except IOError:
91         pass
92     return False
93
94 def _EndRecData64(fpin, offset, endrec):
95     """
96     Read the ZIP64 end-of-archive records and use that to update endrec
97     """
98     locatorSize = struct.calcsize(structEndArchive64Locator)
99     fpin.seek(offset - locatorSize, 2)
100     data = fpin.read(locatorSize)
101     sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data)
102     if sig != stringEndArchive64Locator:
103         return endrec
104
105     if diskno != 0 or disks != 1:
106         raise BadZipfile("zipfiles that span multiple disks are not supported")
107
108     # Assume no 'zip64 extensible data'
109     endArchiveSize = struct.calcsize(structEndArchive64)
110     fpin.seek(offset - locatorSize - endArchiveSize, 2)
111     data = fpin.read(endArchiveSize)
112     sig, sz, create_version, read_version, disk_num, disk_dir, \
113             dircount, dircount2, dirsize, diroffset = \
114             struct.unpack(structEndArchive64, data)
115     if sig != stringEndArchive64:
116         return endrec
117
118     # Update the original endrec using data from the ZIP64 record
119     endrec[1] = disk_num
120     endrec[2] = disk_dir
121     endrec[3] = dircount
122     endrec[4] = dircount2
123     endrec[5] = dirsize
124     endrec[6] = diroffset
125     return endrec
126
127
128 def _EndRecData(fpin):
129     """Return data from the "End of Central Directory" record, or None.
130
131     The data is a list of the nine items in the ZIP "End of central dir"
132     record followed by a tenth item, the file seek offset of this record."""
133     fpin.seek(-22, 2)               # Assume no archive comment.
134     filesize = fpin.tell() + 22     # Get file size
135     data = fpin.read()
136     if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
137         endrec = struct.unpack(structEndArchive, data)
138         endrec = list(endrec)
139         endrec.append("")               # Append the archive comment
140         endrec.append(filesize - 22)    # Append the record start offset
141         if endrec[-4] == -1 or endrec[-4] == 0xffffffff:
142             return _EndRecData64(fpin, -22, endrec)
143         return endrec
144     # Search the last END_BLOCK bytes of the file for the record signature.
145     # The comment is appended to the ZIP file and has a 16 bit length.
146     # So the comment may be up to 64K long.  We limit the search for the
147     # signature to a few Kbytes at the end of the file for efficiency.
148     # also, the signature must not appear in the comment.
149     END_BLOCK = min(filesize, 1024 * 4)
150     fpin.seek(filesize - END_BLOCK, 0)
151     data = fpin.read()
152     start = data.rfind(stringEndArchive)
153     if start >= 0:     # Correct signature string was found
154         endrec = struct.unpack(structEndArchive, data[start:start+22])
155         endrec = list(endrec)
156         comment = data[start+22:]
157         if endrec[7] == len(comment):     # Comment length checks out
158             # Append the archive comment and start offset
159             endrec.append(comment)
160             endrec.append(filesize - END_BLOCK + start)
161             if endrec[-4] == -1 or endrec[-4] == 0xffffffff:
162                 return _EndRecData64(fpin, - END_BLOCK + start, endrec)
163             return endrec
164     return      # Error, return None
165
166
167 class ZipInfo (object):
168     """Class with attributes describing each file in the ZIP archive."""
169
170     __slots__ = (
171             'orig_filename',
172             'filename',
173             'date_time',
174             'compress_type',
175             'comment',
176             'extra',
177             'create_system',
178             'create_version',
179             'extract_version',
180             'reserved',
181             'flag_bits',
182             'volume',
183             'internal_attr',
184             'external_attr',
185             'header_offset',
186             'CRC',
187             'compress_size',
188             'file_size',
189         )
190
191     def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
192         self.orig_filename = filename   # Original file name in archive
193
194         # Terminate the file name at the first null byte.  Null bytes in file
195         # names are used as tricks by viruses in archives.
196         null_byte = filename.find(chr(0))
197         if null_byte >= 0:
198             filename = filename[0:null_byte]
199         # This is used to ensure paths in generated ZIP files always use
200         # forward slashes as the directory separator, as required by the
201         # ZIP format specification.
202         if os.sep != "/" and os.sep in filename:
203             filename = filename.replace(os.sep, "/")
204
205         self.filename = filename        # Normalized file name
206         self.date_time = date_time      # year, month, day, hour, min, sec
207         # Standard values:
208         self.compress_type = ZIP_STORED # Type of compression for the file
209         self.comment = ""               # Comment for each file
210         self.extra = ""                 # ZIP extra data
211         if sys.platform == 'win32':
212             self.create_system = 0          # System which created ZIP archive
213         else:
214             # Assume everything else is unix-y
215             self.create_system = 3          # System which created ZIP archive
216         self.create_version = 20        # Version which created ZIP archive
217         self.extract_version = 20       # Version needed to extract archive
218         self.reserved = 0               # Must be zero
219         self.flag_bits = 0              # ZIP flag bits
220         self.volume = 0                 # Volume number of file header
221         self.internal_attr = 0          # Internal attributes
222         self.external_attr = 0          # External file attributes
223         # Other attributes are set by class ZipFile:
224         # header_offset         Byte offset to the file header
225         # CRC                   CRC-32 of the uncompressed file
226         # compress_size         Size of the compressed file
227         # file_size             Size of the uncompressed file
228
229     def FileHeader(self):
230         """Return the per-file header as a string."""
231         dt = self.date_time
232         dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
233         dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
234         if self.flag_bits & 0x08:
235             # Set these to zero because we write them after the file data
236             CRC = compress_size = file_size = 0
237         else:
238             CRC = self.CRC
239             compress_size = self.compress_size
240             file_size = self.file_size
241
242         extra = self.extra
243
244         if file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT:
245             # File is larger than what fits into a 4 byte integer,
246             # fall back to the ZIP64 extension
247             fmt = '<hhqq'
248             extra = extra + struct.pack(fmt,
249                     1, struct.calcsize(fmt)-4, file_size, compress_size)
250             file_size = 0xffffffff # -1
251             compress_size = 0xffffffff # -1
252             self.extract_version = max(45, self.extract_version)
253             self.create_version = max(45, self.extract_version)
254
255         header = struct.pack(structFileHeader, stringFileHeader,
256                  self.extract_version, self.reserved, self.flag_bits,
257                  self.compress_type, dostime, dosdate, CRC,
258                  compress_size, file_size,
259                  len(self.filename), len(extra))
260         return header + self.filename + extra
261
262     def _decodeExtra(self):
263         # Try to decode the extra field.
264         extra = self.extra
265         unpack = struct.unpack
266         while extra:
267             tp, ln = unpack('<hh', extra[:4])
268             if tp == 1:
269                 if ln >= 24:
270                     counts = unpack('<qqq', extra[4:28])
271                 elif ln == 16:
272                     counts = unpack('<qq', extra[4:20])
273                 elif ln == 8:
274                     counts = unpack('<q', extra[4:12])
275                 elif ln == 0:
276                     counts = ()
277                 else:
278                     raise RuntimeError, "Corrupt extra field %s"%(ln,)
279
280                 idx = 0
281
282                 # ZIP64 extension (large files and/or large archives)
283                 if self.file_size == -1 or self.file_size == 0xFFFFFFFFL:
284                     self.file_size = counts[idx]
285                     idx += 1
286
287                 if self.compress_size == -1 or self.compress_size == 0xFFFFFFFFL:
288                     self.compress_size = counts[idx]
289                     idx += 1
290
291                 if self.header_offset == -1 or self.header_offset == 0xffffffffL:
292                     old = self.header_offset
293                     self.header_offset = counts[idx]
294                     idx+=1
295
296             extra = extra[ln+4:]
297
298
299 class ZipFile:
300     """ Class with methods to open, read, write, close, list zip files.
301
302     z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=True)
303
304     file: Either the path to the file, or a file-like object.
305           If it is a path, the file will be opened and closed by ZipFile.
306     mode: The mode can be either read "r", write "w" or append "a".
307     compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib).
308     allowZip64: if True ZipFile will create files with ZIP64 extensions when
309                 needed, otherwise it will raise an exception when this would
310                 be necessary.
311
312     """
313
314     fp = None                   # Set here since __del__ checks it
315
316     def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False):
317         """Open the ZIP file with mode read "r", write "w" or append "a"."""
318         self._allowZip64 = allowZip64
319         self._didModify = False
320         if compression == ZIP_STORED:
321             pass
322         elif compression == ZIP_DEFLATED:
323             if not zlib:
324                 raise RuntimeError,\
325                       "Compression requires the (missing) zlib module"
326         else:
327             raise RuntimeError, "That compression method is not supported"
328         self.debug = 0  # Level of printing: 0 through 3
329         self.NameToInfo = {}    # Find file info given name
330         self.filelist = []      # List of ZipInfo instances for archive
331         self.compression = compression  # Method of compression
332         self.mode = key = mode.replace('b', '')[0]
333
334         # Check if we were passed a file-like object
335         if isinstance(file, basestring):
336             self._filePassed = 0
337             self.filename = file
338             modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
339             self.fp = open(file, modeDict[mode])
340         else:
341             self._filePassed = 1
342             self.fp = file
343             self.filename = getattr(file, 'name', None)
344
345         if key == 'r':
346             self._GetContents()
347         elif key == 'w':
348             pass
349         elif key == 'a':
350             try:                        # See if file is a zip file
351                 self._RealGetContents()
352                 # seek to start of directory and overwrite
353                 self.fp.seek(self.start_dir, 0)
354             except BadZipfile:          # file is not a zip file, just append
355                 self.fp.seek(0, 2)
356         else:
357             if not self._filePassed:
358                 self.fp.close()
359                 self.fp = None
360             raise RuntimeError, 'Mode must be "r", "w" or "a"'
361
362     def _GetContents(self):
363         """Read the directory, making sure we close the file if the format
364         is bad."""
365         try:
366             self._RealGetContents()
367         except BadZipfile:
368             if not self._filePassed:
369                 self.fp.close()
370                 self.fp = None
371             raise
372
373     def _RealGetContents(self):
374         """Read in the table of contents for the ZIP file."""
375         fp = self.fp
376         endrec = _EndRecData(fp)
377         if not endrec:
378             raise BadZipfile, "File is not a zip file"
379         if self.debug > 1:
380             print endrec
381         size_cd = endrec[5]             # bytes in central directory
382         offset_cd = endrec[6]   # offset of central directory
383         self.comment = endrec[8]        # archive comment
384         # endrec[9] is the offset of the "End of Central Dir" record
385         if endrec[9] > ZIP64_LIMIT:
386             x = endrec[9] - size_cd - 56 - 20
387         else:
388             x = endrec[9] - size_cd
389         # "concat" is zero, unless zip was concatenated to another file
390         concat = x - offset_cd
391         if self.debug > 2:
392             print "given, inferred, offset", offset_cd, x, concat
393         # self.start_dir:  Position of start of central directory
394         self.start_dir = offset_cd + concat
395         fp.seek(self.start_dir, 0)
396         data = fp.read(size_cd)
397         fp = cStringIO.StringIO(data)
398         total = 0
399         while total < size_cd:
400             centdir = fp.read(46)
401             total = total + 46
402             if centdir[0:4] != stringCentralDir:
403                 raise BadZipfile, "Bad magic number for central directory"
404             centdir = struct.unpack(structCentralDir, centdir)
405             if self.debug > 2:
406                 print centdir
407             filename = fp.read(centdir[_CD_FILENAME_LENGTH])
408             # Create ZipInfo instance to store file information
409             x = ZipInfo(filename)
410             x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH])
411             x.comment = fp.read(centdir[_CD_COMMENT_LENGTH])
412             total = (total + centdir[_CD_FILENAME_LENGTH]
413                      + centdir[_CD_EXTRA_FIELD_LENGTH]
414                      + centdir[_CD_COMMENT_LENGTH])
415             x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET]
416             (x.create_version, x.create_system, x.extract_version, x.reserved,
417                 x.flag_bits, x.compress_type, t, d,
418                 x.CRC, x.compress_size, x.file_size) = centdir[1:12]
419             x.volume, x.internal_attr, x.external_attr = centdir[15:18]
420             # Convert date/time code to (year, month, day, hour, min, sec)
421             x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F,
422                                      t>>11, (t>>5)&0x3F, (t&0x1F) * 2 )
423
424             x._decodeExtra()
425             x.header_offset = x.header_offset + concat
426             self.filelist.append(x)
427             self.NameToInfo[x.filename] = x
428             if self.debug > 2:
429                 print "total", total
430
431
432     def namelist(self):
433         """Return a list of file names in the archive."""
434         l = []
435         for data in self.filelist:
436             l.append(data.filename)
437         return l
438
439     def infolist(self):
440         """Return a list of class ZipInfo instances for files in the
441         archive."""
442         return self.filelist
443
444     def printdir(self):
445         """Print a table of contents for the zip file."""
446         print "%-46s %19s %12s" % ("File Name", "Modified    ", "Size")
447         for zinfo in self.filelist:
448             date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
449             print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size)
450
451     def testzip(self):
452         """Read all the files and check the CRC."""
453         for zinfo in self.filelist:
454             try:
455                 self.read(zinfo.filename)       # Check CRC-32
456             except BadZipfile:
457                 return zinfo.filename
458
459
460     def getinfo(self, name):
461         """Return the instance of ZipInfo given 'name'."""
462         return self.NameToInfo[name]
463
464     def read(self, name):
465         """Return file bytes (as a string) for name."""
466         if self.mode not in ("r", "a"):
467             raise RuntimeError, 'read() requires mode "r" or "a"'
468         if not self.fp:
469             raise RuntimeError, \
470                   "Attempt to read ZIP archive that was already closed"
471         zinfo = self.getinfo(name)
472         filepos = self.fp.tell()
473
474         self.fp.seek(zinfo.header_offset, 0)
475
476         # Skip the file header:
477         fheader = self.fp.read(30)
478         if fheader[0:4] != stringFileHeader:
479             raise BadZipfile, "Bad magic number for file header"
480
481         fheader = struct.unpack(structFileHeader, fheader)
482         fname = self.fp.read(fheader[_FH_FILENAME_LENGTH])
483         if fheader[_FH_EXTRA_FIELD_LENGTH]:
484             self.fp.read(fheader[_FH_EXTRA_FIELD_LENGTH])
485
486         if fname != zinfo.orig_filename:
487             raise BadZipfile, \
488                       'File name in directory "%s" and header "%s" differ.' % (
489                           zinfo.orig_filename, fname)
490
491         bytes = self.fp.read(zinfo.compress_size)
492         self.fp.seek(filepos, 0)
493         if zinfo.compress_type == ZIP_STORED:
494             pass
495         elif zinfo.compress_type == ZIP_DEFLATED:
496             if not zlib:
497                 raise RuntimeError, \
498                       "De-compression requires the (missing) zlib module"
499             # zlib compress/decompress code by Jeremy Hylton of CNRI
500             dc = zlib.decompressobj(-15)
501             bytes = dc.decompress(bytes)
502             # need to feed in unused pad byte so that zlib won't choke
503             ex = dc.decompress('Z') + dc.flush()
504             if ex:
505                 bytes = bytes + ex
506         else:
507             raise BadZipfile, \
508                   "Unsupported compression method %d for file %s" % \
509             (zinfo.compress_type, name)
510         crc = binascii.crc32(bytes)
511         if crc != zinfo.CRC:
512             raise BadZipfile, "Bad CRC-32 for file %s" % name
513         return bytes
514
515     def _writecheck(self, zinfo):
516         """Check for errors before writing a file to the archive."""
517         if zinfo.filename in self.NameToInfo:
518             if self.debug:      # Warning for duplicate names
519                 print "Duplicate name:", zinfo.filename
520         if self.mode not in ("w", "a"):
521             raise RuntimeError, 'write() requires mode "w" or "a"'
522         if not self.fp:
523             raise RuntimeError, \
524                   "Attempt to write ZIP archive that was already closed"
525         if zinfo.compress_type == ZIP_DEFLATED and not zlib:
526             raise RuntimeError, \
527                   "Compression requires the (missing) zlib module"
528         if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED):
529             raise RuntimeError, \
530                   "That compression method is not supported"
531         if zinfo.file_size > ZIP64_LIMIT:
532             if not self._allowZip64:
533                 raise LargeZipFile("Filesize would require ZIP64 extensions")
534         if zinfo.header_offset > ZIP64_LIMIT:
535             if not self._allowZip64:
536                 raise LargeZipFile("Zipfile size would require ZIP64 extensions")
537
538     def write(self, filename, arcname=None, compress_type=None):
539         """Put the bytes from filename into the archive under the name
540         arcname."""
541         st = os.stat(filename)
542         mtime = time.localtime(st.st_mtime)
543         date_time = mtime[0:6]
544         # Create ZipInfo instance to store file information
545         if arcname is None:
546             arcname = filename
547         arcname = os.path.normpath(os.path.splitdrive(arcname)[1])
548         while arcname[0] in (os.sep, os.altsep):
549             arcname = arcname[1:]
550         zinfo = ZipInfo(arcname, date_time)
551         zinfo.external_attr = (st[0] & 0xFFFF) << 16L      # Unix attributes
552         if compress_type is None:
553             zinfo.compress_type = self.compression
554         else:
555             zinfo.compress_type = compress_type
556
557         zinfo.file_size = st.st_size
558         zinfo.flag_bits = 0x00
559         zinfo.header_offset = self.fp.tell()    # Start of header bytes
560
561         self._writecheck(zinfo)
562         self._didModify = True
563         fp = open(filename, "rb")
564         # Must overwrite CRC and sizes with correct data later
565         zinfo.CRC = CRC = 0
566         zinfo.compress_size = compress_size = 0
567         zinfo.file_size = file_size = 0
568         self.fp.write(zinfo.FileHeader())
569         if zinfo.compress_type == ZIP_DEFLATED:
570             cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
571                  zlib.DEFLATED, -15)
572         else:
573             cmpr = None
574         while 1:
575             buf = fp.read(1024 * 8)
576             if not buf:
577                 break
578             file_size = file_size + len(buf)
579             CRC = binascii.crc32(buf, CRC)
580             if cmpr:
581                 buf = cmpr.compress(buf)
582                 compress_size = compress_size + len(buf)
583             self.fp.write(buf)
584         fp.close()
585         if cmpr:
586             buf = cmpr.flush()
587             compress_size = compress_size + len(buf)
588             self.fp.write(buf)
589             zinfo.compress_size = compress_size
590         else:
591             zinfo.compress_size = file_size
592         zinfo.CRC = CRC
593         zinfo.file_size = file_size
594         # Seek backwards and write CRC and file sizes
595         position = self.fp.tell()       # Preserve current position in file
596         self.fp.seek(zinfo.header_offset + 14, 0)
597         self.fp.write(struct.pack("<lLL", zinfo.CRC, zinfo.compress_size,
598               zinfo.file_size))
599         self.fp.seek(position, 0)
600         self.filelist.append(zinfo)
601         self.NameToInfo[zinfo.filename] = zinfo
602
603     def writestr(self, zinfo_or_arcname, bytes):
604         """Write a file into the archive.  The contents is the string
605         'bytes'.  'zinfo_or_arcname' is either a ZipInfo instance or
606         the name of the file in the archive."""
607         if not isinstance(zinfo_or_arcname, ZipInfo):
608             zinfo = ZipInfo(filename=zinfo_or_arcname,
609                             date_time=time.localtime(time.time()))
610             zinfo.compress_type = self.compression
611         else:
612             zinfo = zinfo_or_arcname
613         zinfo.file_size = len(bytes)            # Uncompressed size
614         zinfo.header_offset = self.fp.tell()    # Start of header bytes
615         self._writecheck(zinfo)
616         self._didModify = True
617         zinfo.CRC = binascii.crc32(bytes)       # CRC-32 checksum
618         if zinfo.compress_type == ZIP_DEFLATED:
619             co = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
620                  zlib.DEFLATED, -15)
621             bytes = co.compress(bytes) + co.flush()
622             zinfo.compress_size = len(bytes)    # Compressed size
623         else:
624             zinfo.compress_size = zinfo.file_size
625         zinfo.header_offset = self.fp.tell()    # Start of header bytes
626         self.fp.write(zinfo.FileHeader())
627         self.fp.write(bytes)
628         self.fp.flush()
629         if zinfo.flag_bits & 0x08:
630             # Write CRC and file sizes after the file data
631             self.fp.write(struct.pack("<lLL", zinfo.CRC, zinfo.compress_size,
632                   zinfo.file_size))
633         self.filelist.append(zinfo)
634         self.NameToInfo[zinfo.filename] = zinfo
635
636     def __del__(self):
637         """Call the "close()" method in case the user forgot."""
638         self.close()
639
640     def close(self):
641         """Close the file, and for mode "w" and "a" write the ending
642         records."""
643         if self.fp is None:
644             return
645
646         if self.mode in ("w", "a") and self._didModify: # write ending records
647             count = 0
648             pos1 = self.fp.tell()
649             for zinfo in self.filelist:         # write central directory
650                 count = count + 1
651                 dt = zinfo.date_time
652                 dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
653                 dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
654                 extra = []
655                 if zinfo.file_size > ZIP64_LIMIT \
656                         or zinfo.compress_size > ZIP64_LIMIT:
657                     extra.append(zinfo.file_size)
658                     extra.append(zinfo.compress_size)
659                     file_size = 0xffffffff #-1
660                     compress_size = 0xffffffff #-1
661                 else:
662                     file_size = zinfo.file_size
663                     compress_size = zinfo.compress_size
664
665                 if zinfo.header_offset > ZIP64_LIMIT:
666                     extra.append(zinfo.header_offset)
667                     header_offset = -1  # struct "l" format:  32 one bits
668                 else:
669                     header_offset = zinfo.header_offset
670
671                 extra_data = zinfo.extra
672                 if extra:
673                     # Append a ZIP64 field to the extra's
674                     extra_data = struct.pack(
675                             '<hh' + 'q'*len(extra),
676                             1, 8*len(extra), *extra) + extra_data
677
678                     extract_version = max(45, zinfo.extract_version)
679                     create_version = max(45, zinfo.create_version)
680                 else:
681                     extract_version = zinfo.extract_version
682                     create_version = zinfo.create_version
683
684                 centdir = struct.pack(structCentralDir,
685                   stringCentralDir, create_version,
686                   zinfo.create_system, extract_version, zinfo.reserved,
687                   zinfo.flag_bits, zinfo.compress_type, dostime, dosdate,
688                   zinfo.CRC, compress_size, file_size,
689                   len(zinfo.filename), len(extra_data), len(zinfo.comment),
690                   0, zinfo.internal_attr, zinfo.external_attr,
691                   header_offset)
692                 self.fp.write(centdir)
693                 self.fp.write(zinfo.filename)
694                 self.fp.write(extra_data)
695                 self.fp.write(zinfo.comment)
696
697             pos2 = self.fp.tell()
698             # Write end-of-zip-archive record
699             if pos1 > ZIP64_LIMIT:
700                 # Need to write the ZIP64 end-of-archive records
701                 zip64endrec = struct.pack(
702                         structEndArchive64, stringEndArchive64,
703                         44, 45, 45, 0, 0, count, count, pos2 - pos1, pos1)
704                 self.fp.write(zip64endrec)
705
706                 zip64locrec = struct.pack(
707                         structEndArchive64Locator,
708                         stringEndArchive64Locator, 0, pos2, 1)
709                 self.fp.write(zip64locrec)
710
711                 # XXX Why is `pos3` computed next?  It's never referenced.
712                 pos3 = self.fp.tell()
713                 endrec = struct.pack(structEndArchive, stringEndArchive,
714                             0, 0, count, count, pos2 - pos1, -1, 0)
715                 self.fp.write(endrec)
716
717             else:
718                 endrec = struct.pack(structEndArchive, stringEndArchive,
719                          0, 0, count, count, pos2 - pos1, pos1, 0)
720                 self.fp.write(endrec)
721             self.fp.flush()
722         if not self._filePassed:
723             self.fp.close()
724         self.fp = None
725
726
727 class PyZipFile(ZipFile):
728     """Class to create ZIP archives with Python library files and packages."""
729
730     def writepy(self, pathname, basename = ""):
731         """Add all files from "pathname" to the ZIP archive.
732
733         If pathname is a package directory, search the directory and
734         all package subdirectories recursively for all *.py and enter
735         the modules into the archive.  If pathname is a plain
736         directory, listdir *.py and enter all modules.  Else, pathname
737         must be a Python *.py file and the module will be put into the
738         archive.  Added modules are always module.pyo or module.pyc.
739         This method will compile the module.py into module.pyc if
740         necessary.
741         """
742         dir, name = os.path.split(pathname)
743         if os.path.isdir(pathname):
744             initname = os.path.join(pathname, "__init__.py")
745             if os.path.isfile(initname):
746                 # This is a package directory, add it
747                 if basename:
748                     basename = "%s/%s" % (basename, name)
749                 else:
750                     basename = name
751                 if self.debug:
752                     print "Adding package in", pathname, "as", basename
753                 fname, arcname = self._get_codename(initname[0:-3], basename)
754                 if self.debug:
755                     print "Adding", arcname
756                 self.write(fname, arcname)
757                 dirlist = os.listdir(pathname)
758                 dirlist.remove("__init__.py")
759                 # Add all *.py files and package subdirectories
760                 for filename in dirlist:
761                     path = os.path.join(pathname, filename)
762                     root, ext = os.path.splitext(filename)
763                     if os.path.isdir(path):
764                         if os.path.isfile(os.path.join(path, "__init__.py")):
765                             # This is a package directory, add it
766                             self.writepy(path, basename)  # Recursive call
767                     elif ext == ".py":
768                         fname, arcname = self._get_codename(path[0:-3],
769                                          basename)
770                         if self.debug:
771                             print "Adding", arcname
772                         self.write(fname, arcname)
773             else:
774                 # This is NOT a package directory, add its files at top level
775                 if self.debug:
776                     print "Adding files from directory", pathname
777                 for filename in os.listdir(pathname):
778                     path = os.path.join(pathname, filename)
779                     root, ext = os.path.splitext(filename)
780                     if ext == ".py":
781                         fname, arcname = self._get_codename(path[0:-3],
782                                          basename)
783                         if self.debug:
784                             print "Adding", arcname
785                         self.write(fname, arcname)
786         else:
787             if pathname[-3:] != ".py":
788                 raise RuntimeError, \
789                       'Files added with writepy() must end with ".py"'
790             fname, arcname = self._get_codename(pathname[0:-3], basename)
791             if self.debug:
792                 print "Adding file", arcname
793             self.write(fname, arcname)
794
795     def _get_codename(self, pathname, basename):
796         """Return (filename, archivename) for the path.
797
798         Given a module name path, return the correct file path and
799         archive name, compiling if necessary.  For example, given
800         /python/lib/string, return (/python/lib/string.pyc, string).
801         """
802         file_py  = pathname + ".py"
803         file_pyc = pathname + ".pyc"
804         file_pyo = pathname + ".pyo"
805         if os.path.isfile(file_pyo) and \
806                             os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime:
807             fname = file_pyo    # Use .pyo file
808         elif not os.path.isfile(file_pyc) or \
809              os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime:
810             import py_compile
811             if self.debug:
812                 print "Compiling", file_py
813             try:
814                 py_compile.compile(file_py, file_pyc, None, True)
815             except py_compile.PyCompileError,err:
816                 print err.msg
817             fname = file_pyc
818         else:
819             fname = file_pyc
820         archivename = os.path.split(fname)[1]
821         if basename:
822             archivename = "%s/%s" % (basename, archivename)
823         return (fname, archivename)
824
825
826 def main(args = None):
827     import textwrap
828     USAGE=textwrap.dedent("""\
829         Usage:
830             zipfile.py -l zipfile.zip        # Show listing of a zipfile
831             zipfile.py -t zipfile.zip        # Test if a zipfile is valid
832             zipfile.py -e zipfile.zip target # Extract zipfile into target dir
833             zipfile.py -c zipfile.zip src ... # Create zipfile from sources
834         """)
835     if args is None:
836         args = sys.argv[1:]
837
838     if not args or args[0] not in ('-l', '-c', '-e', '-t'):
839         print USAGE
840         sys.exit(1)
841
842     if args[0] == '-l':
843         if len(args) != 2:
844             print USAGE
845             sys.exit(1)
846         zf = ZipFile(args[1], 'r')
847         zf.printdir()
848         zf.close()
849
850     elif args[0] == '-t':
851         if len(args) != 2:
852             print USAGE
853             sys.exit(1)
854         zf = ZipFile(args[1], 'r')
855         zf.testzip()
856         print "Done testing"
857
858     elif args[0] == '-e':
859         if len(args) != 3:
860             print USAGE
861             sys.exit(1)
862
863         zf = ZipFile(args[1], 'r')
864         out = args[2]
865         for path in zf.namelist():
866             if path.startswith('./'):
867                 tgt = os.path.join(out, path[2:])
868             else:
869                 tgt = os.path.join(out, path)
870
871             tgtdir = os.path.dirname(tgt)
872             if not os.path.exists(tgtdir):
873                 os.makedirs(tgtdir)
874             fp = open(tgt, 'wb')
875             fp.write(zf.read(path))
876             fp.close()
877         zf.close()
878
879     elif args[0] == '-c':
880         if len(args) < 3:
881             print USAGE
882             sys.exit(1)
883
884         def addToZip(zf, path, zippath):
885             if os.path.isfile(path):
886                 zf.write(path, zippath, ZIP_DEFLATED)
887             elif os.path.isdir(path):
888                 for nm in os.listdir(path):
889                     addToZip(zf,
890                             os.path.join(path, nm), os.path.join(zippath, nm))
891             # else: ignore
892
893         zf = ZipFile(args[1], 'w', allowZip64=True)
894         for src in args[2:]:
895             addToZip(zf, src, os.path.basename(src))
896
897         zf.close()
898
899 if __name__ == "__main__":
900     main()