Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
 
4
/**
5
 * ZIP archive reader
6
 *
7
 * PHP versions 4 and 5
8
 *
9
 * This library is free software; you can redistribute it and/or
10
 * modify it under the terms of the GNU Lesser General Public
11
 * License as published by the Free Software Foundation; either
12
 * version 2.1 of the License, or (at your option) any later version.
13
 *
14
 * This library is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17
 * Lesser General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Lesser General Public
20
 * License along with this library; if not, write to the Free Software
21
 * Foundation, Inc., 59 Temple Place, Suite 330,Boston,MA 02111-1307 USA
22
 *
23
 * @category   File Formats
24
 * @package    File_Archive
25
 * @author     Vincent Lascaux <vincentlascaux@php.net>
26
 * @copyright  1997-2005 The PHP Group
27
 * @license    http://www.gnu.org/copyleft/lesser.html  LGPL
28
 * @version    CVS: $Id: Zip.php,v 1.28 2008/05/28 15:53:50 cbrunet Exp $
29
 * @link       http://pear.php.net/package/File_Archive
30
 */
31
 
32
require_once "File/Archive/Reader/Archive.php";
33
 
34
/**
35
 * ZIP archive reader
36
 * Currently only allows to browse the archive (getData is not available)
37
 */
38
class File_Archive_Reader_Zip extends File_Archive_Reader_Archive
39
{
40
    var $currentFilename = null;
41
    var $currentStat = null;
42
    var $header = null;
43
    var $offset = 0;
44
    var $data = null;
45
    var $files = array();
46
    var $seekToEnd = 0;
47
 
48
    var $centralDirectory = null;
49
 
50
    /**
51
     * @see File_Archive_Reader::close()
52
     */
53
    function close()
54
    {
55
        $this->currentFilename = null;
56
        $this->currentStat = null;
57
        $this->compLength = 0;
58
        $this->data = null;
59
        $this->seekToEnd = 0;
60
        $this->files = array();
61
        $this->centralDirectory = null;
62
 
63
        return parent::close();
64
    }
65
 
66
    /**
67
     * @see File_Archive_Reader::getFilename()
68
     */
69
    function getFilename() { return $this->currentFilename; }
70
    /**
71
     * @see File_Archive_Reader::getStat()
72
     */
73
    function getStat() { return $this->currentStat; }
74
 
75
    /**
76
     * Go to next entry in ZIP archive
77
     *
78
     * @see File_Archive_Reader::next()
79
     */
80
    function next()
81
    {
82
        if ($this->seekToEnd > 0) {
83
            return false;
84
        }
85
 
86
        //Skip the data and the footer if they haven't been uncompressed
87
        if ($this->header !== null && $this->data === null) {
88
            $toSkip = $this->header['CLen'];
89
            $error = $this->source->skip($toSkip);
90
            if (PEAR::isError($error)) {
91
                return $error;
92
            }
93
        }
94
 
95
        $this->offset = 0;
96
        $this->data = null;
97
 
98
        //Read the header
99
        $header = $this->source->getData(4);
100
        // Handle PK00PK archives
101
        if ($header == "\x50\x4b\x30\x30") { //PK00
102
            $header = $this->source->getData(4);
103
        }
104
        // Sometimes this header is used to tag the data descriptor section
105
        if($header == "\x50\x4b\x07\x08") {
106
            // Read out the data descriptor (always 12 bytes)
107
            $this->source->getData(12);
108
 
109
            // Get a new header from the file
110
            $header = $this->source->getData(4);
111
        }
112
        if (PEAR::isError($header)) {
113
            return $header;
114
        }
115
        if ($header == "\x50\x4b\x03\x04") {
116
            //New entry
117
            $header = $this->source->getData(26);
118
            if (PEAR::isError($header)) {
119
                return $header;
120
            }
121
            $this->header = unpack(
122
                "vVersion/vFlag/vMethod/vTime/vDate/VCRC/VCLen/VNLen/vFile/vExtra",
123
                $header);
124
 
125
            //Check the compression method
126
            if ($this->header['Method'] != 0 &&
127
                $this->header['Method'] != 8 &&
128
                $this->header['Method'] != 12) {
129
                return PEAR::raiseError("File_Archive_Reader_Zip doesn't ".
130
                        "handle compression method {$this->header['Method']}");
131
            }
132
            if ($this->header['Flag'] & 1) {
133
                return PEAR::raiseError("File_Archive_Reader_Zip doesn't ".
134
                        "handle encrypted files");
135
            }
136
            if ($this->header['Flag'] & 8) {
137
                if ($this->centralDirectory === null) {
138
                    $this->readCentralDirectory();
139
                }
140
                $centralDirEntry = $this->centralDirectory[count($this->files)];
141
 
142
                $this->header['CRC'] = $centralDirEntry['CRC'];
143
                $this->header['CLen'] = $centralDirEntry['CLen'];
144
                $this->header['NLen'] = $centralDirEntry['NLen'];
145
            }
146
            if ($this->header['Flag'] & 32) {
147
                return PEAR::raiseError("File_Archive_Reader_Zip doesn't ".
148
                        "handle compressed patched data");
149
            }
150
            if ($this->header['Flag'] & 64) {
151
                return PEAR::raiseError("File_Archive_Reader_Zip doesn't ".
152
                        "handle strong encrypted files");
153
            }
154
 
155
            $this->currentStat = array(
156
                7=>$this->header['NLen'],
157
                9=>mktime(
158
                    ($this->header['Time'] & 0xF800) >> 11,         //hour
159
                    ($this->header['Time'] & 0x07E0) >> 5,          //minute
160
                    ($this->header['Time'] & 0x001F) >> 1,          //second
161
                    ($this->header['Date'] & 0x01E0) >> 5,          //month
162
                    ($this->header['Date'] & 0x001F)     ,          //day
163
                   (($this->header['Date'] & 0xFE00) >> 9) + 1980   //year
164
                )
165
            );
166
            $this->currentStat['size']  = $this->currentStat[7];
167
            $this->currentStat['mtime'] = $this->currentStat[9];
168
 
169
            $this->currentFilename = $this->source->getData($this->header['File']);
170
 
171
            $error = $this->source->skip($this->header['Extra']);
172
            if (PEAR::isError($error)) {
173
                return $error;
174
            }
175
 
176
            $this->files[] = array('name' => $this->currentFilename,
177
                            'stat' => $this->currentStat,
178
                            'CRC' => $this->header['CRC'],
179
                            'CLen' => $this->header['CLen']
180
                           );
181
            return true;
182
        } else {
183
            //Begining of central area
184
            $this->seekToEnd = 4;
185
            $this->currentFilename = null;
186
            return false;
187
        }
188
    }
189
 
190
    /**
191
     * @see File_Archive_Reader::getData()
192
     */
193
    function getData($length = -1)
194
    {
195
        if ($this->offset >= $this->currentStat[7]) {
196
            return null;
197
        }
198
 
199
        if ($length>=0) {
200
            $actualLength = min($length, $this->currentStat[7]-$this->offset);
201
        } else {
202
            $actualLength = $this->currentStat[7]-$this->offset;
203
        }
204
 
205
        $error = $this->uncompressData();
206
        if (PEAR::isError($error)) {
207
            return $error;
208
        }
209
        $result = substr($this->data, $this->offset, $actualLength);
210
        $this->offset += $actualLength;
211
        return $result;
212
    }
213
    /**
214
     * @see File_Archive_Reader::skip()
215
     */
216
    function skip($length = -1)
217
    {
218
        $before = $this->offset;
219
        if ($length == -1) {
220
            $this->offset = $this->currentStat[7];
221
        } else {
222
            $this->offset = min($this->offset + $length, $this->currentStat[7]);
223
        }
224
        return $this->offset - $before;
225
    }
226
    /**
227
     * @see File_Archive_Reader::rewind()
228
     */
229
    function rewind($length = -1)
230
    {
231
        $before = $this->offset;
232
        if ($length == -1) {
233
            $this->offset = 0;
234
        } else {
235
            $this->offset = min(0, $this->offset - $length);
236
        }
237
        return $before - $this->offset;
238
    }
239
    /**
240
     * @see File_Archive_Reader::tell()
241
     */
242
    function tell()
243
    {
244
        return $this->offset;
245
    }
246
 
247
    function uncompressData()
248
    {
249
        if ($this->data !== null)
250
            return;
251
 
252
        $this->data = $this->source->getData($this->header['CLen']);
253
        if (PEAR::isError($this->data)) {
254
            return $this->data;
255
        }
256
        if ($this->header['Method'] == 8) {
257
            $this->data = gzinflate($this->data);
258
        }
259
        if ($this->header['Method'] == 12) {
260
            $this->data = bzdecompress($this->data);
261
        }
262
 
263
        if (crc32($this->data) != ($this->header['CRC'] & 0xFFFFFFFF)) {
264
            return PEAR::raiseError("Zip archive: CRC fails on entry ".
265
                                    $this->currentFilename);
266
        }
267
    }
268
 
269
    /**
270
     * @see File_Archive_Reader::makeWriterRemoveFiles()
271
     */
272
    function makeWriterRemoveFiles($pred)
273
    {
274
        require_once "File/Archive/Writer/Zip.php";
275
 
276
        $blocks = array();
277
        $seek = null;
278
        $gap = 0;
279
        if ($this->currentFilename !== null && $pred->isTrue($this)) {
280
            $seek = 30 + $this->header['File'] + $this->header['Extra'] + $this->header['CLen'];
281
            $blocks[] = $seek; //Remove this file
282
            array_pop($this->files);
283
        }
284
 
285
        while (($error = $this->next()) === true) {
286
            $size = 30 + $this->header['File'] + $this->header['Extra'] + $this->header['CLen'];
287
            if (substr($this->getFilename(), -1) == '/' || $pred->isTrue($this)) {
288
                array_pop($this->files);
289
                if ($seek === null) {
290
                    $seek = $size;
291
                    $blocks[] = $size;
292
                } else if ($gap > 0) {
293
                    $blocks[] = $gap; //Don't remove the files between the gap
294
                    $blocks[] = $size;
295
                    $seek += $size;
296
                } else {
297
                    $blocks[count($blocks)-1] += $size;   //Also remove this file
298
                    $seek += $size;
299
                }
300
                $gap = 0;
301
            } else {
302
                if ($seek !== null) {
303
                    $seek += $size;
304
                    $gap += $size;
305
                }
306
            }
307
        }
308
        if (PEAR::isError($error)) {
309
            return $error;
310
        }
311
 
312
        if ($seek === null) {
313
            $seek = 4;
314
        } else {
315
            $seek += 4;
316
            if ($gap == 0) {
317
                array_pop($blocks);
318
            } else {
319
                $blocks[] = $gap;
320
            }
321
        }
322
 
323
        $writer = new File_Archive_Writer_Zip(null,
324
            $this->source->makeWriterRemoveBlocks($blocks, -$seek)
325
        );
326
        if (PEAR::isError($writer)) {
327
            return $writer;
328
        }
329
 
330
        foreach ($this->files as $file) {
331
            $writer->alreadyWrittenFile($file['name'], $file['stat'], $file['CRC'], $file['CLen']);
332
        }
333
 
334
        $this->close();
335
        return $writer;
336
    }
337
 
338
    /**
339
     * @see File_Archive_Reader::makeWriterRemoveBlocks()
340
     */
341
    function makeWriterRemoveBlocks($blocks, $seek = 0)
342
    {
343
        if ($this->currentFilename === null) {
344
            return PEAR::raiseError('No file selected');
345
        }
346
 
347
        $keep = false;
348
 
349
        $this->uncompressData();
350
        $newData = substr($this->data, 0, $this->offset + $seek);
351
        $this->data = substr($this->data, $this->offset + $seek);
352
        foreach ($blocks as $length) {
353
            if ($keep) {
354
                $newData .= substr($this->data, 0, $length);
355
            }
356
            $this->data = substr($this->data, $length);
357
            $keep = !$keep;
358
        }
359
        if ($keep) {
360
            $newData .= $this->data;
361
        }
362
 
363
        $filename = $this->currentFilename;
364
        $stat = $this->currentStat;
365
 
366
        $writer = $this->makeWriterRemove();
367
        if (PEAR::isError($writer)) {
368
            return $writer;
369
        }
370
 
371
        unset($stat[7]);
372
        $stat[9] = $stat['mtime'] = time();
373
        $writer->newFile($filename, $stat);
374
        $writer->writeData($newData);
375
        return $writer;
376
    }
377
 
378
    /**
379
     * @see File_Archive_Reader::makeAppendWriter
380
     */
381
    function makeAppendWriter()
382
    {
383
        require_once "File/Archive/Writer/Zip.php";
384
 
385
        while (($error = $this->next()) === true) { }
386
        if (PEAR::isError($error)) {
387
            $this->close();
388
            return $error;
389
        }
390
 
391
        $writer = new File_Archive_Writer_Zip(null,
392
            $this->source->makeWriterRemoveBlocks(array(), -4)
393
        );
394
 
395
        foreach ($this->files as $file) {
396
            $writer->alreadyWrittenFile($file['name'], $file['stat'], $file['CRC'], $file['CLen']);
397
        }
398
 
399
        $this->close();
400
        return $writer;
401
    }
402
 
403
    /**
404
     * This function seeks to the start of the [end of central directory] field,
405
     * just after the \x50\x4b\x05\x06 signature and returns the number of bytes
406
     * skipped
407
     *
408
     * The stream must initially be positioned before the end of central directory
409
     */
410
    function seekToEndOfCentralDirectory()
411
    {
412
        $nbSkipped = $this->source->skip();
413
 
414
        $nbSkipped -= $this->source->rewind(22) - 4;
415
        if ($this->source->getData(4) == "\x50\x4b\x05\x06") {
416
            return $nbSkipped;
417
        }
418
 
419
        while ($nbSkipped > 0) {
420
 
421
            $nbRewind = $this->source->rewind(min(100, $nbSkipped));
422
            while ($nbRewind >= -4) {
423
                if ($nbRewind-- && $this->source->getData(1) == "\x50" &&
424
                    $nbRewind-- && $this->source->getData(1) == "\x4b" &&
425
                    $nbRewind-- && $this->source->getData(1) == "\x05" &&
426
                    $nbRewind-- && $this->source->getData(1) == "\x06") {
427
                    //We finally found it!
428
                    return $nbSkipped - $nbRewind;
429
                }
430
            }
431
            $nbSkipped -= $nbRewind;
432
        }
433
 
434
        return PEAR::raiseError('End of central directory not found. The file is probably not a zip archive');
435
    }
436
 
437
    /**
438
     * This function will fill the central directory variable
439
     * and seek back to where it was called
440
     */
441
    function readCentralDirectory()
442
    {
443
        $nbSkipped = $this->seekToEndOfCentralDirectory();
444
        if (PEAR::isError($nbSkipped)) {
445
            return $nbSkipped;
446
        }
447
 
448
        $this->source->skip(12);
449
        $offset = $this->source->getData(4);
450
        $nbSkipped += 16;
451
        if (PEAR::isError($offset)) {
452
            return $offset;
453
        }
454
 
455
        $offset = unpack("Vvalue", $offset);
456
        $offset = $offset['value'];
457
 
458
        $current = $this->source->tell();
459
        $nbSkipped -= $this->source->rewind($current - $offset);
460
 
461
        //Now we are the right pos to read the central directory
462
        $this->centralDirectory = array();
463
        while ($this->source->getData(4) == "\x50\x4b\x01\x02") {
464
            $this->source->skip(12);
465
            $header = $this->source->getData(16);
466
            $nbSkipped += 32;
467
 
468
            if (PEAR::isError($header)) {
469
                return $header;
470
            }
471
 
472
            $header = unpack('VCRC/VCLen/VNLen/vFileLength/vExtraLength', $header);
473
            $this->centralDirectory[] = array('CRC'  => $header['CRC'],
474
                                              'CLen' => $header['CLen'],
475
                                              'NLen' => $header['NLen']);
476
            $nbSkipped += $this->source->skip(14 + $header['FileLength'] + $header['ExtraLength']);
477
        }
478
 
479
        $this->source->rewind($nbSkipped+4);
480
    }
481
}
482
?>