xref: /dokuwiki/vendor/splitbrain/php-archive/src/Zip.php (revision 605f8e8d0e501057749c50581087ce05089c1af3)
1*605f8e8dSAndreas Gohr<?php
2*605f8e8dSAndreas Gohr
3*605f8e8dSAndreas Gohrnamespace splitbrain\PHPArchive;
4*605f8e8dSAndreas Gohr
5*605f8e8dSAndreas Gohr/**
6*605f8e8dSAndreas Gohr * Class Zip
7*605f8e8dSAndreas Gohr *
8*605f8e8dSAndreas Gohr * Creates or extracts Zip archives
9*605f8e8dSAndreas Gohr *
10*605f8e8dSAndreas Gohr * @author  Andreas Gohr <andi@splitbrain.org>
11*605f8e8dSAndreas Gohr * @package splitbrain\PHPArchive
12*605f8e8dSAndreas Gohr * @license MIT
13*605f8e8dSAndreas Gohr */
14*605f8e8dSAndreas Gohrclass Zip extends Archive
15*605f8e8dSAndreas Gohr{
16*605f8e8dSAndreas Gohr
17*605f8e8dSAndreas Gohr    protected $file = '';
18*605f8e8dSAndreas Gohr    protected $fh;
19*605f8e8dSAndreas Gohr    protected $memory = '';
20*605f8e8dSAndreas Gohr    protected $closed = true;
21*605f8e8dSAndreas Gohr    protected $writeaccess = false;
22*605f8e8dSAndreas Gohr    protected $ctrl_dir;
23*605f8e8dSAndreas Gohr    protected $complevel = 9;
24*605f8e8dSAndreas Gohr
25*605f8e8dSAndreas Gohr    /**
26*605f8e8dSAndreas Gohr     * Set the compression level.
27*605f8e8dSAndreas Gohr     *
28*605f8e8dSAndreas Gohr     * Compression Type is ignored for ZIP
29*605f8e8dSAndreas Gohr     *
30*605f8e8dSAndreas Gohr     * You can call this function before adding each file to set differen compression levels
31*605f8e8dSAndreas Gohr     * for each file.
32*605f8e8dSAndreas Gohr     *
33*605f8e8dSAndreas Gohr     * @param int $level Compression level (0 to 9)
34*605f8e8dSAndreas Gohr     * @param int $type  Type of compression to use ignored for ZIP
35*605f8e8dSAndreas Gohr     * @return mixed
36*605f8e8dSAndreas Gohr     */
37*605f8e8dSAndreas Gohr    public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
38*605f8e8dSAndreas Gohr    {
39*605f8e8dSAndreas Gohr        $this->complevel = $level;
40*605f8e8dSAndreas Gohr    }
41*605f8e8dSAndreas Gohr
42*605f8e8dSAndreas Gohr    /**
43*605f8e8dSAndreas Gohr     * Open an existing ZIP file for reading
44*605f8e8dSAndreas Gohr     *
45*605f8e8dSAndreas Gohr     * @param string $file
46*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
47*605f8e8dSAndreas Gohr     */
48*605f8e8dSAndreas Gohr    public function open($file)
49*605f8e8dSAndreas Gohr    {
50*605f8e8dSAndreas Gohr        $this->file = $file;
51*605f8e8dSAndreas Gohr        $this->fh   = @fopen($this->file, 'rb');
52*605f8e8dSAndreas Gohr        if (!$this->fh) {
53*605f8e8dSAndreas Gohr            throw new ArchiveIOException('Could not open file for reading: '.$this->file);
54*605f8e8dSAndreas Gohr        }
55*605f8e8dSAndreas Gohr        $this->closed = false;
56*605f8e8dSAndreas Gohr    }
57*605f8e8dSAndreas Gohr
58*605f8e8dSAndreas Gohr    /**
59*605f8e8dSAndreas Gohr     * Read the contents of a ZIP archive
60*605f8e8dSAndreas Gohr     *
61*605f8e8dSAndreas Gohr     * This function lists the files stored in the archive, and returns an indexed array of FileInfo objects
62*605f8e8dSAndreas Gohr     *
63*605f8e8dSAndreas Gohr     * The archive is closed afer reading the contents, for API compatibility with TAR files
64*605f8e8dSAndreas Gohr     * Reopen the file with open() again if you want to do additional operations
65*605f8e8dSAndreas Gohr     *
66*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
67*605f8e8dSAndreas Gohr     * @return FileInfo[]
68*605f8e8dSAndreas Gohr     */
69*605f8e8dSAndreas Gohr    public function contents()
70*605f8e8dSAndreas Gohr    {
71*605f8e8dSAndreas Gohr        if ($this->closed || !$this->file) {
72*605f8e8dSAndreas Gohr            throw new ArchiveIOException('Can not read from a closed archive');
73*605f8e8dSAndreas Gohr        }
74*605f8e8dSAndreas Gohr
75*605f8e8dSAndreas Gohr        $result = array();
76*605f8e8dSAndreas Gohr
77*605f8e8dSAndreas Gohr        $centd = $this->readCentralDir();
78*605f8e8dSAndreas Gohr
79*605f8e8dSAndreas Gohr        @rewind($this->fh);
80*605f8e8dSAndreas Gohr        @fseek($this->fh, $centd['offset']);
81*605f8e8dSAndreas Gohr
82*605f8e8dSAndreas Gohr        for ($i = 0; $i < $centd['entries']; $i++) {
83*605f8e8dSAndreas Gohr            $result[] = $this->header2fileinfo($this->readCentralFileHeader());
84*605f8e8dSAndreas Gohr        }
85*605f8e8dSAndreas Gohr
86*605f8e8dSAndreas Gohr        $this->close();
87*605f8e8dSAndreas Gohr        return $result;
88*605f8e8dSAndreas Gohr    }
89*605f8e8dSAndreas Gohr
90*605f8e8dSAndreas Gohr    /**
91*605f8e8dSAndreas Gohr     * Extract an existing ZIP archive
92*605f8e8dSAndreas Gohr     *
93*605f8e8dSAndreas Gohr     * The $strip parameter allows you to strip a certain number of path components from the filenames
94*605f8e8dSAndreas Gohr     * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
95*605f8e8dSAndreas Gohr     * an integer is passed as $strip.
96*605f8e8dSAndreas Gohr     * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
97*605f8e8dSAndreas Gohr     * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
98*605f8e8dSAndreas Gohr     *
99*605f8e8dSAndreas Gohr     * By default this will extract all files found in the archive. You can restrict the output using the $include
100*605f8e8dSAndreas Gohr     * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
101*605f8e8dSAndreas Gohr     * $include is set only files that match this expression will be extracted. Files that match the $exclude
102*605f8e8dSAndreas Gohr     * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
103*605f8e8dSAndreas Gohr     * stripped filenames as described above.
104*605f8e8dSAndreas Gohr     *
105*605f8e8dSAndreas Gohr     * @param string     $outdir  the target directory for extracting
106*605f8e8dSAndreas Gohr     * @param int|string $strip   either the number of path components or a fixed prefix to strip
107*605f8e8dSAndreas Gohr     * @param string     $exclude a regular expression of files to exclude
108*605f8e8dSAndreas Gohr     * @param string     $include a regular expression of files to include
109*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
110*605f8e8dSAndreas Gohr     * @return FileInfo[]
111*605f8e8dSAndreas Gohr     */
112*605f8e8dSAndreas Gohr    function extract($outdir, $strip = '', $exclude = '', $include = '')
113*605f8e8dSAndreas Gohr    {
114*605f8e8dSAndreas Gohr        if ($this->closed || !$this->file) {
115*605f8e8dSAndreas Gohr            throw new ArchiveIOException('Can not read from a closed archive');
116*605f8e8dSAndreas Gohr        }
117*605f8e8dSAndreas Gohr
118*605f8e8dSAndreas Gohr        $outdir = rtrim($outdir, '/');
119*605f8e8dSAndreas Gohr        @mkdir($outdir, 0777, true);
120*605f8e8dSAndreas Gohr
121*605f8e8dSAndreas Gohr        $extracted = array();
122*605f8e8dSAndreas Gohr
123*605f8e8dSAndreas Gohr        $cdir      = $this->readCentralDir();
124*605f8e8dSAndreas Gohr        $pos_entry = $cdir['offset']; // begin of the central file directory
125*605f8e8dSAndreas Gohr
126*605f8e8dSAndreas Gohr        for ($i = 0; $i < $cdir['entries']; $i++) {
127*605f8e8dSAndreas Gohr            // read file header
128*605f8e8dSAndreas Gohr            @fseek($this->fh, $pos_entry);
129*605f8e8dSAndreas Gohr            $header          = $this->readCentralFileHeader();
130*605f8e8dSAndreas Gohr            $header['index'] = $i;
131*605f8e8dSAndreas Gohr            $pos_entry       = ftell($this->fh); // position of the next file in central file directory
132*605f8e8dSAndreas Gohr            fseek($this->fh, $header['offset']); // seek to beginning of file header
133*605f8e8dSAndreas Gohr            $header   = $this->readFileHeader($header);
134*605f8e8dSAndreas Gohr            $fileinfo = $this->header2fileinfo($header);
135*605f8e8dSAndreas Gohr
136*605f8e8dSAndreas Gohr            // apply strip rules
137*605f8e8dSAndreas Gohr            $fileinfo->strip($strip);
138*605f8e8dSAndreas Gohr
139*605f8e8dSAndreas Gohr            // skip unwanted files
140*605f8e8dSAndreas Gohr            if (!strlen($fileinfo->getPath()) || !$fileinfo->match($include, $exclude)) {
141*605f8e8dSAndreas Gohr                continue;
142*605f8e8dSAndreas Gohr            }
143*605f8e8dSAndreas Gohr
144*605f8e8dSAndreas Gohr            $extracted[] = $fileinfo;
145*605f8e8dSAndreas Gohr
146*605f8e8dSAndreas Gohr            // create output directory
147*605f8e8dSAndreas Gohr            $output    = $outdir.'/'.$fileinfo->getPath();
148*605f8e8dSAndreas Gohr            $directory = ($header['folder']) ? $output : dirname($output);
149*605f8e8dSAndreas Gohr            @mkdir($directory, 0777, true);
150*605f8e8dSAndreas Gohr
151*605f8e8dSAndreas Gohr            // nothing more to do for directories
152*605f8e8dSAndreas Gohr            if ($fileinfo->getIsdir()) {
153*605f8e8dSAndreas Gohr                continue;
154*605f8e8dSAndreas Gohr            }
155*605f8e8dSAndreas Gohr
156*605f8e8dSAndreas Gohr            // compressed files are written to temporary .gz file first
157*605f8e8dSAndreas Gohr            if ($header['compression'] == 0) {
158*605f8e8dSAndreas Gohr                $extractto = $output;
159*605f8e8dSAndreas Gohr            } else {
160*605f8e8dSAndreas Gohr                $extractto = $output.'.gz';
161*605f8e8dSAndreas Gohr            }
162*605f8e8dSAndreas Gohr
163*605f8e8dSAndreas Gohr            // open file for writing
164*605f8e8dSAndreas Gohr            $fp = fopen($extractto, "wb");
165*605f8e8dSAndreas Gohr            if (!$fp) {
166*605f8e8dSAndreas Gohr                throw new ArchiveIOException('Could not open file for writing: '.$extractto);
167*605f8e8dSAndreas Gohr            }
168*605f8e8dSAndreas Gohr
169*605f8e8dSAndreas Gohr            // prepend compression header
170*605f8e8dSAndreas Gohr            if ($header['compression'] != 0) {
171*605f8e8dSAndreas Gohr                $binary_data = pack(
172*605f8e8dSAndreas Gohr                    'va1a1Va1a1',
173*605f8e8dSAndreas Gohr                    0x8b1f,
174*605f8e8dSAndreas Gohr                    chr($header['compression']),
175*605f8e8dSAndreas Gohr                    chr(0x00),
176*605f8e8dSAndreas Gohr                    time(),
177*605f8e8dSAndreas Gohr                    chr(0x00),
178*605f8e8dSAndreas Gohr                    chr(3)
179*605f8e8dSAndreas Gohr                );
180*605f8e8dSAndreas Gohr                fwrite($fp, $binary_data, 10);
181*605f8e8dSAndreas Gohr            }
182*605f8e8dSAndreas Gohr
183*605f8e8dSAndreas Gohr            // read the file and store it on disk
184*605f8e8dSAndreas Gohr            $size = $header['compressed_size'];
185*605f8e8dSAndreas Gohr            while ($size != 0) {
186*605f8e8dSAndreas Gohr                $read_size   = ($size < 2048 ? $size : 2048);
187*605f8e8dSAndreas Gohr                $buffer      = fread($this->fh, $read_size);
188*605f8e8dSAndreas Gohr                $binary_data = pack('a'.$read_size, $buffer);
189*605f8e8dSAndreas Gohr                fwrite($fp, $binary_data, $read_size);
190*605f8e8dSAndreas Gohr                $size -= $read_size;
191*605f8e8dSAndreas Gohr            }
192*605f8e8dSAndreas Gohr
193*605f8e8dSAndreas Gohr            // finalize compressed file
194*605f8e8dSAndreas Gohr            if ($header['compression'] != 0) {
195*605f8e8dSAndreas Gohr                $binary_data = pack('VV', $header['crc'], $header['size']);
196*605f8e8dSAndreas Gohr                fwrite($fp, $binary_data, 8);
197*605f8e8dSAndreas Gohr            }
198*605f8e8dSAndreas Gohr
199*605f8e8dSAndreas Gohr            // close file
200*605f8e8dSAndreas Gohr            fclose($fp);
201*605f8e8dSAndreas Gohr
202*605f8e8dSAndreas Gohr            // unpack compressed file
203*605f8e8dSAndreas Gohr            if ($header['compression'] != 0) {
204*605f8e8dSAndreas Gohr                $gzp = @gzopen($extractto, 'rb');
205*605f8e8dSAndreas Gohr                if (!$gzp) {
206*605f8e8dSAndreas Gohr                    @unlink($extractto);
207*605f8e8dSAndreas Gohr                    throw new ArchiveIOException('Failed file extracting. gzip support missing?');
208*605f8e8dSAndreas Gohr                }
209*605f8e8dSAndreas Gohr                $fp = @fopen($output, 'wb');
210*605f8e8dSAndreas Gohr                if (!$fp) {
211*605f8e8dSAndreas Gohr                    throw new ArchiveIOException('Could not open file for writing: '.$extractto);
212*605f8e8dSAndreas Gohr                }
213*605f8e8dSAndreas Gohr
214*605f8e8dSAndreas Gohr                $size = $header['size'];
215*605f8e8dSAndreas Gohr                while ($size != 0) {
216*605f8e8dSAndreas Gohr                    $read_size   = ($size < 2048 ? $size : 2048);
217*605f8e8dSAndreas Gohr                    $buffer      = gzread($gzp, $read_size);
218*605f8e8dSAndreas Gohr                    $binary_data = pack('a'.$read_size, $buffer);
219*605f8e8dSAndreas Gohr                    @fwrite($fp, $binary_data, $read_size);
220*605f8e8dSAndreas Gohr                    $size -= $read_size;
221*605f8e8dSAndreas Gohr                }
222*605f8e8dSAndreas Gohr                fclose($fp);
223*605f8e8dSAndreas Gohr                gzclose($gzp);
224*605f8e8dSAndreas Gohr            }
225*605f8e8dSAndreas Gohr
226*605f8e8dSAndreas Gohr            touch($output, $fileinfo->getMtime());
227*605f8e8dSAndreas Gohr            //FIXME what about permissions?
228*605f8e8dSAndreas Gohr        }
229*605f8e8dSAndreas Gohr
230*605f8e8dSAndreas Gohr        $this->close();
231*605f8e8dSAndreas Gohr        return $extracted;
232*605f8e8dSAndreas Gohr    }
233*605f8e8dSAndreas Gohr
234*605f8e8dSAndreas Gohr    /**
235*605f8e8dSAndreas Gohr     * Create a new ZIP file
236*605f8e8dSAndreas Gohr     *
237*605f8e8dSAndreas Gohr     * If $file is empty, the zip file will be created in memory
238*605f8e8dSAndreas Gohr     *
239*605f8e8dSAndreas Gohr     * @param string $file
240*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
241*605f8e8dSAndreas Gohr     */
242*605f8e8dSAndreas Gohr    public function create($file = '')
243*605f8e8dSAndreas Gohr    {
244*605f8e8dSAndreas Gohr        $this->file   = $file;
245*605f8e8dSAndreas Gohr        $this->memory = '';
246*605f8e8dSAndreas Gohr        $this->fh     = 0;
247*605f8e8dSAndreas Gohr
248*605f8e8dSAndreas Gohr        if ($this->file) {
249*605f8e8dSAndreas Gohr            $this->fh = @fopen($this->file, 'wb');
250*605f8e8dSAndreas Gohr
251*605f8e8dSAndreas Gohr            if (!$this->fh) {
252*605f8e8dSAndreas Gohr                throw new ArchiveIOException('Could not open file for writing: '.$this->file);
253*605f8e8dSAndreas Gohr            }
254*605f8e8dSAndreas Gohr        }
255*605f8e8dSAndreas Gohr        $this->writeaccess = true;
256*605f8e8dSAndreas Gohr        $this->closed      = false;
257*605f8e8dSAndreas Gohr        $this->ctrl_dir    = array();
258*605f8e8dSAndreas Gohr    }
259*605f8e8dSAndreas Gohr
260*605f8e8dSAndreas Gohr    /**
261*605f8e8dSAndreas Gohr     * Add a file to the current ZIP archive using an existing file in the filesystem
262*605f8e8dSAndreas Gohr     *
263*605f8e8dSAndreas Gohr     * @param string          $file     path to the original file
264*605f8e8dSAndreas Gohr     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
265*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
266*605f8e8dSAndreas Gohr     */
267*605f8e8dSAndreas Gohr
268*605f8e8dSAndreas Gohr    /**
269*605f8e8dSAndreas Gohr     * Add a file to the current archive using an existing file in the filesystem
270*605f8e8dSAndreas Gohr     *
271*605f8e8dSAndreas Gohr     * @param string          $file     path to the original file
272*605f8e8dSAndreas Gohr     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
273*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
274*605f8e8dSAndreas Gohr     */
275*605f8e8dSAndreas Gohr    public function addFile($file, $fileinfo = '')
276*605f8e8dSAndreas Gohr    {
277*605f8e8dSAndreas Gohr        if (is_string($fileinfo)) {
278*605f8e8dSAndreas Gohr            $fileinfo = FileInfo::fromPath($file, $fileinfo);
279*605f8e8dSAndreas Gohr        }
280*605f8e8dSAndreas Gohr
281*605f8e8dSAndreas Gohr        if ($this->closed) {
282*605f8e8dSAndreas Gohr            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
283*605f8e8dSAndreas Gohr        }
284*605f8e8dSAndreas Gohr
285*605f8e8dSAndreas Gohr        $data = @file_get_contents($file);
286*605f8e8dSAndreas Gohr        if ($data === false) {
287*605f8e8dSAndreas Gohr            throw new ArchiveIOException('Could not open file for reading: '.$file);
288*605f8e8dSAndreas Gohr        }
289*605f8e8dSAndreas Gohr
290*605f8e8dSAndreas Gohr        // FIXME could we stream writing compressed data? gzwrite on a fopen handle?
291*605f8e8dSAndreas Gohr        $this->addData($fileinfo, $data);
292*605f8e8dSAndreas Gohr    }
293*605f8e8dSAndreas Gohr
294*605f8e8dSAndreas Gohr    /**
295*605f8e8dSAndreas Gohr     * Add a file to the current TAR archive using the given $data as content
296*605f8e8dSAndreas Gohr     *
297*605f8e8dSAndreas Gohr     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
298*605f8e8dSAndreas Gohr     * @param string          $data     binary content of the file to add
299*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
300*605f8e8dSAndreas Gohr     */
301*605f8e8dSAndreas Gohr    public function addData($fileinfo, $data)
302*605f8e8dSAndreas Gohr    {
303*605f8e8dSAndreas Gohr        if (is_string($fileinfo)) {
304*605f8e8dSAndreas Gohr            $fileinfo = new FileInfo($fileinfo);
305*605f8e8dSAndreas Gohr        }
306*605f8e8dSAndreas Gohr
307*605f8e8dSAndreas Gohr        if ($this->closed) {
308*605f8e8dSAndreas Gohr            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
309*605f8e8dSAndreas Gohr        }
310*605f8e8dSAndreas Gohr
311*605f8e8dSAndreas Gohr        // prepare the various header infos
312*605f8e8dSAndreas Gohr        $dtime    = dechex($this->makeDosTime($fileinfo->getMtime()));
313*605f8e8dSAndreas Gohr        $hexdtime = pack(
314*605f8e8dSAndreas Gohr            'H*',
315*605f8e8dSAndreas Gohr            $dtime[6].$dtime[7].
316*605f8e8dSAndreas Gohr            $dtime[4].$dtime[5].
317*605f8e8dSAndreas Gohr            $dtime[2].$dtime[3].
318*605f8e8dSAndreas Gohr            $dtime[0].$dtime[1]
319*605f8e8dSAndreas Gohr        );
320*605f8e8dSAndreas Gohr        $size     = strlen($data);
321*605f8e8dSAndreas Gohr        $crc      = crc32($data);
322*605f8e8dSAndreas Gohr        if ($this->complevel) {
323*605f8e8dSAndreas Gohr            $fmagic = "\x50\x4b\x03\x04\x14\x00\x00\x00\x08\x00";
324*605f8e8dSAndreas Gohr            $cmagic = "\x50\x4b\x01\x02\x00\x00\x14\x00\x00\x00\x08\x00";
325*605f8e8dSAndreas Gohr            $data   = gzcompress($data, $this->complevel);
326*605f8e8dSAndreas Gohr            $data   = substr($data, 2, -4); // strip compression headers
327*605f8e8dSAndreas Gohr        } else {
328*605f8e8dSAndreas Gohr            $fmagic = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00";
329*605f8e8dSAndreas Gohr            $cmagic = "\x50\x4b\x01\x02\x14\x00\x0a\x00\x00\x00\x00\x00";
330*605f8e8dSAndreas Gohr        }
331*605f8e8dSAndreas Gohr        $csize  = strlen($data);
332*605f8e8dSAndreas Gohr        $offset = $this->dataOffset();
333*605f8e8dSAndreas Gohr        $name   = $fileinfo->getPath();
334*605f8e8dSAndreas Gohr
335*605f8e8dSAndreas Gohr        // write data
336*605f8e8dSAndreas Gohr        $this->writebytes($fmagic);
337*605f8e8dSAndreas Gohr        $this->writebytes($hexdtime);
338*605f8e8dSAndreas Gohr        $this->writebytes(pack('V', $crc).pack('V', $csize).pack('V', $size)); //pre header
339*605f8e8dSAndreas Gohr        $this->writebytes(pack('v', strlen($name)).pack('v', 0).$name.$data); //file data
340*605f8e8dSAndreas Gohr        $this->writebytes(pack('V', $crc).pack('V', $csize).pack('V', $size)); //post header
341*605f8e8dSAndreas Gohr
342*605f8e8dSAndreas Gohr        // add info to central file directory
343*605f8e8dSAndreas Gohr        $cdrec = $cmagic;
344*605f8e8dSAndreas Gohr        $cdrec .= $hexdtime.pack('V', $crc).pack('V', $csize).pack('V', $size);
345*605f8e8dSAndreas Gohr        $cdrec .= pack('v', strlen($name)).pack('v', 0).pack('v', 0);
346*605f8e8dSAndreas Gohr        $cdrec .= pack('v', 0).pack('v', 0).pack('V', 32);
347*605f8e8dSAndreas Gohr        $cdrec .= pack('V', $offset);
348*605f8e8dSAndreas Gohr        $cdrec .= $name;
349*605f8e8dSAndreas Gohr        $this->ctrl_dir[] = $cdrec;
350*605f8e8dSAndreas Gohr    }
351*605f8e8dSAndreas Gohr
352*605f8e8dSAndreas Gohr    /**
353*605f8e8dSAndreas Gohr     * Add the closing footer to the archive if in write mode, close all file handles
354*605f8e8dSAndreas Gohr     *
355*605f8e8dSAndreas Gohr     * After a call to this function no more data can be added to the archive, for
356*605f8e8dSAndreas Gohr     * read access no reading is allowed anymore
357*605f8e8dSAndreas Gohr     */
358*605f8e8dSAndreas Gohr    public function close()
359*605f8e8dSAndreas Gohr    {
360*605f8e8dSAndreas Gohr        if ($this->closed) {
361*605f8e8dSAndreas Gohr            return;
362*605f8e8dSAndreas Gohr        } // we did this already
363*605f8e8dSAndreas Gohr
364*605f8e8dSAndreas Gohr        // write footer
365*605f8e8dSAndreas Gohr        if ($this->writeaccess) {
366*605f8e8dSAndreas Gohr            $offset  = $this->dataOffset();
367*605f8e8dSAndreas Gohr            $ctrldir = join('', $this->ctrl_dir);
368*605f8e8dSAndreas Gohr            $this->writebytes($ctrldir);
369*605f8e8dSAndreas Gohr            $this->writebytes("\x50\x4b\x05\x06\x00\x00\x00\x00"); // EOF CTRL DIR
370*605f8e8dSAndreas Gohr            $this->writebytes(pack('v', count($this->ctrl_dir)).pack('v', count($this->ctrl_dir)));
371*605f8e8dSAndreas Gohr            $this->writebytes(pack('V', strlen($ctrldir)).pack('V', strlen($offset))."\x00\x00");
372*605f8e8dSAndreas Gohr            $this->ctrl_dir = array();
373*605f8e8dSAndreas Gohr        }
374*605f8e8dSAndreas Gohr
375*605f8e8dSAndreas Gohr        // close file handles
376*605f8e8dSAndreas Gohr        if ($this->file) {
377*605f8e8dSAndreas Gohr            fclose($this->fh);
378*605f8e8dSAndreas Gohr            $this->file = '';
379*605f8e8dSAndreas Gohr            $this->fh   = 0;
380*605f8e8dSAndreas Gohr        }
381*605f8e8dSAndreas Gohr
382*605f8e8dSAndreas Gohr        $this->writeaccess = false;
383*605f8e8dSAndreas Gohr        $this->closed      = true;
384*605f8e8dSAndreas Gohr    }
385*605f8e8dSAndreas Gohr
386*605f8e8dSAndreas Gohr    /**
387*605f8e8dSAndreas Gohr     * Returns the created in-memory archive data
388*605f8e8dSAndreas Gohr     *
389*605f8e8dSAndreas Gohr     * This implicitly calls close() on the Archive
390*605f8e8dSAndreas Gohr     */
391*605f8e8dSAndreas Gohr    public function getArchive()
392*605f8e8dSAndreas Gohr    {
393*605f8e8dSAndreas Gohr        $this->close();
394*605f8e8dSAndreas Gohr
395*605f8e8dSAndreas Gohr        return $this->memory;
396*605f8e8dSAndreas Gohr    }
397*605f8e8dSAndreas Gohr
398*605f8e8dSAndreas Gohr    /**
399*605f8e8dSAndreas Gohr     * Save the created in-memory archive data
400*605f8e8dSAndreas Gohr     *
401*605f8e8dSAndreas Gohr     * Note: It's more memory effective to specify the filename in the create() function and
402*605f8e8dSAndreas Gohr     * let the library work on the new file directly.
403*605f8e8dSAndreas Gohr     *
404*605f8e8dSAndreas Gohr     * @param     $file
405*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
406*605f8e8dSAndreas Gohr     */
407*605f8e8dSAndreas Gohr    public function save($file)
408*605f8e8dSAndreas Gohr    {
409*605f8e8dSAndreas Gohr        if (!file_put_contents($file, $this->getArchive())) {
410*605f8e8dSAndreas Gohr            throw new ArchiveIOException('Could not write to file: '.$file);
411*605f8e8dSAndreas Gohr        }
412*605f8e8dSAndreas Gohr    }
413*605f8e8dSAndreas Gohr
414*605f8e8dSAndreas Gohr    /**
415*605f8e8dSAndreas Gohr     * Read the central directory
416*605f8e8dSAndreas Gohr     *
417*605f8e8dSAndreas Gohr     * This key-value list contains general information about the ZIP file
418*605f8e8dSAndreas Gohr     *
419*605f8e8dSAndreas Gohr     * @return array
420*605f8e8dSAndreas Gohr     */
421*605f8e8dSAndreas Gohr    protected function readCentralDir()
422*605f8e8dSAndreas Gohr    {
423*605f8e8dSAndreas Gohr        $size = filesize($this->file);
424*605f8e8dSAndreas Gohr        if ($size < 277) {
425*605f8e8dSAndreas Gohr            $maximum_size = $size;
426*605f8e8dSAndreas Gohr        } else {
427*605f8e8dSAndreas Gohr            $maximum_size = 277;
428*605f8e8dSAndreas Gohr        }
429*605f8e8dSAndreas Gohr
430*605f8e8dSAndreas Gohr        @fseek($this->fh, $size - $maximum_size);
431*605f8e8dSAndreas Gohr        $pos   = ftell($this->fh);
432*605f8e8dSAndreas Gohr        $bytes = 0x00000000;
433*605f8e8dSAndreas Gohr
434*605f8e8dSAndreas Gohr        while ($pos < $size) {
435*605f8e8dSAndreas Gohr            $byte  = @fread($this->fh, 1);
436*605f8e8dSAndreas Gohr            $bytes = (($bytes << 8) & 0xFFFFFFFF) | ord($byte);
437*605f8e8dSAndreas Gohr            if ($bytes == 0x504b0506) {
438*605f8e8dSAndreas Gohr                break;
439*605f8e8dSAndreas Gohr            }
440*605f8e8dSAndreas Gohr            $pos++;
441*605f8e8dSAndreas Gohr        }
442*605f8e8dSAndreas Gohr
443*605f8e8dSAndreas Gohr        $data = unpack(
444*605f8e8dSAndreas Gohr            'vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size',
445*605f8e8dSAndreas Gohr            fread($this->fh, 18)
446*605f8e8dSAndreas Gohr        );
447*605f8e8dSAndreas Gohr
448*605f8e8dSAndreas Gohr        if ($data['comment_size'] != 0) {
449*605f8e8dSAndreas Gohr            $centd['comment'] = fread($this->fh, $data['comment_size']);
450*605f8e8dSAndreas Gohr        } else {
451*605f8e8dSAndreas Gohr            $centd['comment'] = '';
452*605f8e8dSAndreas Gohr        }
453*605f8e8dSAndreas Gohr        $centd['entries']      = $data['entries'];
454*605f8e8dSAndreas Gohr        $centd['disk_entries'] = $data['disk_entries'];
455*605f8e8dSAndreas Gohr        $centd['offset']       = $data['offset'];
456*605f8e8dSAndreas Gohr        $centd['disk_start']   = $data['disk_start'];
457*605f8e8dSAndreas Gohr        $centd['size']         = $data['size'];
458*605f8e8dSAndreas Gohr        $centd['disk']         = $data['disk'];
459*605f8e8dSAndreas Gohr        return $centd;
460*605f8e8dSAndreas Gohr    }
461*605f8e8dSAndreas Gohr
462*605f8e8dSAndreas Gohr    /**
463*605f8e8dSAndreas Gohr     * Read the next central file header
464*605f8e8dSAndreas Gohr     *
465*605f8e8dSAndreas Gohr     * Assumes the current file pointer is pointing at the right position
466*605f8e8dSAndreas Gohr     *
467*605f8e8dSAndreas Gohr     * @return array
468*605f8e8dSAndreas Gohr     */
469*605f8e8dSAndreas Gohr    protected function readCentralFileHeader()
470*605f8e8dSAndreas Gohr    {
471*605f8e8dSAndreas Gohr        $binary_data = fread($this->fh, 46);
472*605f8e8dSAndreas Gohr        $header      = unpack(
473*605f8e8dSAndreas Gohr            'vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset',
474*605f8e8dSAndreas Gohr            $binary_data
475*605f8e8dSAndreas Gohr        );
476*605f8e8dSAndreas Gohr
477*605f8e8dSAndreas Gohr        if ($header['filename_len'] != 0) {
478*605f8e8dSAndreas Gohr            $header['filename'] = fread($this->fh, $header['filename_len']);
479*605f8e8dSAndreas Gohr        } else {
480*605f8e8dSAndreas Gohr            $header['filename'] = '';
481*605f8e8dSAndreas Gohr        }
482*605f8e8dSAndreas Gohr
483*605f8e8dSAndreas Gohr        if ($header['extra_len'] != 0) {
484*605f8e8dSAndreas Gohr            $header['extra'] = fread($this->fh, $header['extra_len']);
485*605f8e8dSAndreas Gohr        } else {
486*605f8e8dSAndreas Gohr            $header['extra'] = '';
487*605f8e8dSAndreas Gohr        }
488*605f8e8dSAndreas Gohr
489*605f8e8dSAndreas Gohr        if ($header['comment_len'] != 0) {
490*605f8e8dSAndreas Gohr            $header['comment'] = fread($this->fh, $header['comment_len']);
491*605f8e8dSAndreas Gohr        } else {
492*605f8e8dSAndreas Gohr            $header['comment'] = '';
493*605f8e8dSAndreas Gohr        }
494*605f8e8dSAndreas Gohr
495*605f8e8dSAndreas Gohr        if ($header['mdate'] && $header['mtime']) {
496*605f8e8dSAndreas Gohr            $hour            = ($header['mtime'] & 0xF800) >> 11;
497*605f8e8dSAndreas Gohr            $minute          = ($header['mtime'] & 0x07E0) >> 5;
498*605f8e8dSAndreas Gohr            $seconde         = ($header['mtime'] & 0x001F) * 2;
499*605f8e8dSAndreas Gohr            $year            = (($header['mdate'] & 0xFE00) >> 9) + 1980;
500*605f8e8dSAndreas Gohr            $month           = ($header['mdate'] & 0x01E0) >> 5;
501*605f8e8dSAndreas Gohr            $day             = $header['mdate'] & 0x001F;
502*605f8e8dSAndreas Gohr            $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year);
503*605f8e8dSAndreas Gohr        } else {
504*605f8e8dSAndreas Gohr            $header['mtime'] = time();
505*605f8e8dSAndreas Gohr        }
506*605f8e8dSAndreas Gohr
507*605f8e8dSAndreas Gohr        $header['stored_filename'] = $header['filename'];
508*605f8e8dSAndreas Gohr        $header['status']          = 'ok';
509*605f8e8dSAndreas Gohr        if (substr($header['filename'], -1) == '/') {
510*605f8e8dSAndreas Gohr            $header['external'] = 0x41FF0010;
511*605f8e8dSAndreas Gohr        }
512*605f8e8dSAndreas Gohr        $header['folder'] = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0;
513*605f8e8dSAndreas Gohr
514*605f8e8dSAndreas Gohr        return $header;
515*605f8e8dSAndreas Gohr    }
516*605f8e8dSAndreas Gohr
517*605f8e8dSAndreas Gohr    /**
518*605f8e8dSAndreas Gohr     * Reads the local file header
519*605f8e8dSAndreas Gohr     *
520*605f8e8dSAndreas Gohr     * This header precedes each individual file inside the zip file. Assumes the current file pointer is pointing at
521*605f8e8dSAndreas Gohr     * the right position already. Enhances this given central header with the data found at the local header.
522*605f8e8dSAndreas Gohr     *
523*605f8e8dSAndreas Gohr     * @param array $header the central file header read previously (see above)
524*605f8e8dSAndreas Gohr     * @return array
525*605f8e8dSAndreas Gohr     */
526*605f8e8dSAndreas Gohr    function readFileHeader($header)
527*605f8e8dSAndreas Gohr    {
528*605f8e8dSAndreas Gohr        $binary_data = fread($this->fh, 30);
529*605f8e8dSAndreas Gohr        $data        = unpack(
530*605f8e8dSAndreas Gohr            'vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len',
531*605f8e8dSAndreas Gohr            $binary_data
532*605f8e8dSAndreas Gohr        );
533*605f8e8dSAndreas Gohr
534*605f8e8dSAndreas Gohr        $header['filename'] = fread($this->fh, $data['filename_len']);
535*605f8e8dSAndreas Gohr        if ($data['extra_len'] != 0) {
536*605f8e8dSAndreas Gohr            $header['extra'] = fread($this->fh, $data['extra_len']);
537*605f8e8dSAndreas Gohr        } else {
538*605f8e8dSAndreas Gohr            $header['extra'] = '';
539*605f8e8dSAndreas Gohr        }
540*605f8e8dSAndreas Gohr
541*605f8e8dSAndreas Gohr        $header['compression'] = $data['compression'];
542*605f8e8dSAndreas Gohr        foreach (array(
543*605f8e8dSAndreas Gohr                     'size',
544*605f8e8dSAndreas Gohr                     'compressed_size',
545*605f8e8dSAndreas Gohr                     'crc'
546*605f8e8dSAndreas Gohr                 ) as $hd) { // On ODT files, these headers are 0. Keep the previous value.
547*605f8e8dSAndreas Gohr            if ($data[$hd] != 0) {
548*605f8e8dSAndreas Gohr                $header[$hd] = $data[$hd];
549*605f8e8dSAndreas Gohr            }
550*605f8e8dSAndreas Gohr        }
551*605f8e8dSAndreas Gohr        $header['flag']  = $data['flag'];
552*605f8e8dSAndreas Gohr        $header['mdate'] = $data['mdate'];
553*605f8e8dSAndreas Gohr        $header['mtime'] = $data['mtime'];
554*605f8e8dSAndreas Gohr
555*605f8e8dSAndreas Gohr        if ($header['mdate'] && $header['mtime']) {
556*605f8e8dSAndreas Gohr            $hour            = ($header['mtime'] & 0xF800) >> 11;
557*605f8e8dSAndreas Gohr            $minute          = ($header['mtime'] & 0x07E0) >> 5;
558*605f8e8dSAndreas Gohr            $seconde         = ($header['mtime'] & 0x001F) * 2;
559*605f8e8dSAndreas Gohr            $year            = (($header['mdate'] & 0xFE00) >> 9) + 1980;
560*605f8e8dSAndreas Gohr            $month           = ($header['mdate'] & 0x01E0) >> 5;
561*605f8e8dSAndreas Gohr            $day             = $header['mdate'] & 0x001F;
562*605f8e8dSAndreas Gohr            $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year);
563*605f8e8dSAndreas Gohr        } else {
564*605f8e8dSAndreas Gohr            $header['mtime'] = time();
565*605f8e8dSAndreas Gohr        }
566*605f8e8dSAndreas Gohr
567*605f8e8dSAndreas Gohr        $header['stored_filename'] = $header['filename'];
568*605f8e8dSAndreas Gohr        $header['status']          = "ok";
569*605f8e8dSAndreas Gohr        $header['folder']          = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0;
570*605f8e8dSAndreas Gohr        return $header;
571*605f8e8dSAndreas Gohr    }
572*605f8e8dSAndreas Gohr
573*605f8e8dSAndreas Gohr    /**
574*605f8e8dSAndreas Gohr     * Create fileinfo object from header data
575*605f8e8dSAndreas Gohr     *
576*605f8e8dSAndreas Gohr     * @param $header
577*605f8e8dSAndreas Gohr     * @return FileInfo
578*605f8e8dSAndreas Gohr     */
579*605f8e8dSAndreas Gohr    protected function header2fileinfo($header)
580*605f8e8dSAndreas Gohr    {
581*605f8e8dSAndreas Gohr        $fileinfo = new FileInfo();
582*605f8e8dSAndreas Gohr        $fileinfo->setPath($header['filename']);
583*605f8e8dSAndreas Gohr        $fileinfo->setSize($header['size']);
584*605f8e8dSAndreas Gohr        $fileinfo->setCompressedSize($header['compressed_size']);
585*605f8e8dSAndreas Gohr        $fileinfo->setMtime($header['mtime']);
586*605f8e8dSAndreas Gohr        $fileinfo->setComment($header['comment']);
587*605f8e8dSAndreas Gohr        $fileinfo->setIsdir($header['external'] == 0x41FF0010 || $header['external'] == 16);
588*605f8e8dSAndreas Gohr        return $fileinfo;
589*605f8e8dSAndreas Gohr    }
590*605f8e8dSAndreas Gohr
591*605f8e8dSAndreas Gohr    /**
592*605f8e8dSAndreas Gohr     * Write to the open filepointer or memory
593*605f8e8dSAndreas Gohr     *
594*605f8e8dSAndreas Gohr     * @param string $data
595*605f8e8dSAndreas Gohr     * @throws ArchiveIOException
596*605f8e8dSAndreas Gohr     * @return int number of bytes written
597*605f8e8dSAndreas Gohr     */
598*605f8e8dSAndreas Gohr    protected function writebytes($data)
599*605f8e8dSAndreas Gohr    {
600*605f8e8dSAndreas Gohr        if (!$this->file) {
601*605f8e8dSAndreas Gohr            $this->memory .= $data;
602*605f8e8dSAndreas Gohr            $written = strlen($data);
603*605f8e8dSAndreas Gohr        } else {
604*605f8e8dSAndreas Gohr            $written = @fwrite($this->fh, $data);
605*605f8e8dSAndreas Gohr        }
606*605f8e8dSAndreas Gohr        if ($written === false) {
607*605f8e8dSAndreas Gohr            throw new ArchiveIOException('Failed to write to archive stream');
608*605f8e8dSAndreas Gohr        }
609*605f8e8dSAndreas Gohr        return $written;
610*605f8e8dSAndreas Gohr    }
611*605f8e8dSAndreas Gohr
612*605f8e8dSAndreas Gohr    /**
613*605f8e8dSAndreas Gohr     * Current data pointer position
614*605f8e8dSAndreas Gohr     *
615*605f8e8dSAndreas Gohr     * @fixme might need a -1
616*605f8e8dSAndreas Gohr     * @return int
617*605f8e8dSAndreas Gohr     */
618*605f8e8dSAndreas Gohr    protected function dataOffset()
619*605f8e8dSAndreas Gohr    {
620*605f8e8dSAndreas Gohr        if ($this->file) {
621*605f8e8dSAndreas Gohr            return ftell($this->fh);
622*605f8e8dSAndreas Gohr        } else {
623*605f8e8dSAndreas Gohr            return strlen($this->memory);
624*605f8e8dSAndreas Gohr        }
625*605f8e8dSAndreas Gohr    }
626*605f8e8dSAndreas Gohr
627*605f8e8dSAndreas Gohr    /**
628*605f8e8dSAndreas Gohr     * Create a DOS timestamp from a UNIX timestamp
629*605f8e8dSAndreas Gohr     *
630*605f8e8dSAndreas Gohr     * DOS timestamps start at 1980-01-01, earlier UNIX stamps will be set to this date
631*605f8e8dSAndreas Gohr     *
632*605f8e8dSAndreas Gohr     * @param $time
633*605f8e8dSAndreas Gohr     * @return int
634*605f8e8dSAndreas Gohr     */
635*605f8e8dSAndreas Gohr    protected function makeDosTime($time)
636*605f8e8dSAndreas Gohr    {
637*605f8e8dSAndreas Gohr        $timearray = getdate($time);
638*605f8e8dSAndreas Gohr        if ($timearray['year'] < 1980) {
639*605f8e8dSAndreas Gohr            $timearray['year']    = 1980;
640*605f8e8dSAndreas Gohr            $timearray['mon']     = 1;
641*605f8e8dSAndreas Gohr            $timearray['mday']    = 1;
642*605f8e8dSAndreas Gohr            $timearray['hours']   = 0;
643*605f8e8dSAndreas Gohr            $timearray['minutes'] = 0;
644*605f8e8dSAndreas Gohr            $timearray['seconds'] = 0;
645*605f8e8dSAndreas Gohr        }
646*605f8e8dSAndreas Gohr        return (($timearray['year'] - 1980) << 25) |
647*605f8e8dSAndreas Gohr        ($timearray['mon'] << 21) |
648*605f8e8dSAndreas Gohr        ($timearray['mday'] << 16) |
649*605f8e8dSAndreas Gohr        ($timearray['hours'] << 11) |
650*605f8e8dSAndreas Gohr        ($timearray['minutes'] << 5) |
651*605f8e8dSAndreas Gohr        ($timearray['seconds'] >> 1);
652*605f8e8dSAndreas Gohr    }
653*605f8e8dSAndreas Gohr
654*605f8e8dSAndreas Gohr}
655