MDL-37429 zipping improvements
[moodle.git] / lib / filestorage / zip_archive.php
CommitLineData
33488ad6 1<?php
33488ad6 2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
33488ad6 17/**
18 * Implementation of zip file archive.
19 *
d2b7803e
DC
20 * @package core_files
21 * @copyright 2008 Petr Skoda (http://skodak.org)
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33488ad6 23 */
0b0bfa93 24
64f93798 25defined('MOODLE_INTERNAL') || die();
0b0bfa93 26
64f93798
PS
27require_once("$CFG->libdir/filestorage/file_archive.php");
28
29/**
01b4040a 30 * Zip file archive class.
64f93798 31 *
d2b7803e
DC
32 * @package core_files
33 * @category files
34 * @copyright 2008 Petr Skoda (http://skodak.org)
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
64f93798 36 */
0b0bfa93 37class zip_archive extends file_archive {
38
d2b7803e 39 /** @var string Pathname of archive */
0b0bfa93 40 protected $archivepathname = null;
41
6fb8ae95
PS
42 /** @var int archive open mode */
43 protected $mode = null;
44
d2b7803e 45 /** @var int Used memory tracking */
0b0bfa93 46 protected $usedmem = 0;
47
d2b7803e 48 /** @var int Iteration position */
0b0bfa93 49 protected $pos = 0;
50
79c966cf 51 /** @var ZipArchive instance */
0b0bfa93 52 protected $za;
53
79c966cf
PS
54 /** @var bool was this archive modified? */
55 protected $modified = false;
56
01b4040a 57 /** @var array unicode decoding array, created by decoding zip file */
6fb8ae95
PS
58 protected $namelookup = null;
59
60 /**
61 * Create new zip_archive instance.
62 */
63 public function __construct() {
64 $this->encoding = null; // Autodetects encoding by default.
65 }
66
0b0bfa93 67 /**
01b4040a 68 * Open or create archive (depending on $mode).
d2b7803e
DC
69 *
70 * @todo MDL-31048 return error message
0b0bfa93 71 * @param string $archivepathname
72 * @param int $mode OPEN, CREATE or OVERWRITE constant
6fb8ae95 73 * @param string $encoding archive local paths encoding, empty means autodetect
0b0bfa93 74 * @return bool success
75 */
6fb8ae95 76 public function open($archivepathname, $mode=file_archive::CREATE, $encoding=null) {
0b0bfa93 77 $this->close();
78
6fb8ae95
PS
79 $this->usedmem = 0;
80 $this->pos = 0;
81 $this->encoding = $encoding;
82 $this->mode = $mode;
0b0bfa93 83
84 $this->za = new ZipArchive();
85
86 switch($mode) {
87 case file_archive::OPEN: $flags = 0; break;
05f2d18b 88 case file_archive::OVERWRITE: $flags = ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE; break; //changed in PHP 5.2.8
0b0bfa93 89 case file_archive::CREATE:
90 default : $flags = ZIPARCHIVE::CREATE; break;
91 }
92
93 $result = $this->za->open($archivepathname, $flags);
94
95 if ($result === true) {
0b0bfa93 96 if (file_exists($archivepathname)) {
97 $this->archivepathname = realpath($archivepathname);
98 } else {
99 $this->archivepathname = $archivepathname;
100 }
101 return true;
102
103 } else {
01b4040a
PS
104 $message = 'Unknown error.';
105 switch ($result) {
106 case ZIPARCHIVE::ER_EXISTS: $message = 'File already exists.'; break;
107 case ZIPARCHIVE::ER_INCONS: $message = 'Zip archive inconsistent.'; break;
108 case ZIPARCHIVE::ER_INVAL: $message = 'Invalid argument.'; break;
109 case ZIPARCHIVE::ER_MEMORY: $message = 'Malloc failure.'; break;
110 case ZIPARCHIVE::ER_NOENT: $message = 'No such file.'; break;
111 case ZIPARCHIVE::ER_NOZIP: $message = 'Not a zip archive.'; break;
112 case ZIPARCHIVE::ER_OPEN: $message = 'Can\'t open file.'; break;
113 case ZIPARCHIVE::ER_READ: $message = 'Read error.'; break;
114 case ZIPARCHIVE::ER_SEEK: $message = 'Seek error.'; break;
115 }
116 debugging($message.': '.$archivepathname, DEBUG_DEVELOPER);
0b0bfa93 117 $this->za = null;
118 $this->archivepathname = null;
0b0bfa93 119 return false;
120 }
121 }
122
6fb8ae95
PS
123 /**
124 * Normalize $localname, always keep in utf-8 encoding.
125 *
126 * @param string $localname name of file in utf-8 encoding
127 * @return string normalised compressed file or directory name
128 */
129 protected function mangle_pathname($localname) {
130 $result = str_replace('\\', '/', $localname); // no MS \ separators
131 $result = preg_replace('/\.\.+/', '', $result); // prevent /.../
132 $result = ltrim($result, '/'); // no leading slash
133
134 if ($result === '.') {
135 $result = '';
136 }
137
138 return $result;
139 }
140
141 /**
142 * Tries to convert $localname into utf-8
143 * please note that it may fail really badly.
144 * The resulting file name is cleaned.
145 *
146 * @param string $localname name (encoding is read from zip file or guessed)
147 * @return string in utf-8
148 */
149 protected function unmangle_pathname($localname) {
150 $this->init_namelookup();
151
152 if (!isset($this->namelookup[$localname])) {
153 $name = $localname;
01b4040a 154 // This should not happen.
6fb8ae95
PS
155 if (!empty($this->encoding) and $this->encoding !== 'utf-8') {
156 $name = @textlib::convert($name, $this->encoding, 'utf-8');
157 }
158 $name = str_replace('\\', '/', $name); // no MS \ separators
159 $name = clean_param($name, PARAM_PATH); // only safe chars
160 return ltrim($name, '/'); // no leading slash
161 }
162
163 return $this->namelookup[$localname];
164 }
165
0b0bfa93 166 /**
01b4040a 167 * Close archive, write changes to disk.
d2b7803e 168 *
0b0bfa93 169 * @return bool success
170 */
171 public function close() {
172 if (!isset($this->za)) {
173 return false;
174 }
175
01b4040a
PS
176 if ($this->za->numFiles == 0) {
177 // PHP can not create empty archives, so let's fake it.
178 $this->za->close();
179 $this->za = null;
180 $this->mode = null;
181 $this->namelookup = null;
182 $this->modified = false;
183 @unlink($this->archivepathname);
184 $data = base64_decode('UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==');
185 if (!file_put_contents($this->archivepathname, $data)) {
186 return false;
187 }
188 return true;
189 }
190
0b0bfa93 191 $res = $this->za->close();
192 $this->za = null;
6fb8ae95
PS
193 $this->mode = null;
194 $this->namelookup = null;
0b0bfa93 195
79c966cf
PS
196 if ($this->modified) {
197 $this->fix_utf8_flags();
198 $this->modified = false;
199 }
200
0b0bfa93 201 return $res;
202 }
203
204 /**
01b4040a 205 * Returns file stream for reading of content.
d2b7803e
DC
206 *
207 * @param int $index index of file
208 * @return resource|bool file handle or false if error
0b0bfa93 209 */
210 public function get_stream($index) {
211 if (!isset($this->za)) {
212 return false;
213 }
214
215 $name = $this->za->getNameIndex($index);
216 if ($name === false) {
217 return false;
218 }
219
220 return $this->za->getStream($name);
221 }
222
223 /**
01b4040a 224 * Returns file information.
d2b7803e
DC
225 *
226 * @param int $index index of file
6fb8ae95 227 * @return stdClass|bool info object or false if error
0b0bfa93 228 */
229 public function get_info($index) {
230 if (!isset($this->za)) {
231 return false;
232 }
233
234 if ($index < 0 or $index >=$this->count()) {
235 return false;
236 }
237
238 $result = $this->za->statIndex($index);
239
240 if ($result === false) {
241 return false;
242 }
243
ac6f1a82 244 $info = new stdClass();
0b0bfa93 245 $info->index = $index;
246 $info->original_pathname = $result['name'];
247 $info->pathname = $this->unmangle_pathname($result['name']);
248 $info->mtime = (int)$result['mtime'];
249
250 if ($info->pathname[strlen($info->pathname)-1] === '/') {
251 $info->is_directory = true;
252 $info->size = 0;
253 } else {
254 $info->is_directory = false;
255 $info->size = (int)$result['size'];
256 }
257
258 return $info;
259 }
260
261 /**
01b4040a 262 * Returns array of info about all files in archive.
d2b7803e 263 *
0b0bfa93 264 * @return array of file infos
265 */
266 public function list_files() {
267 if (!isset($this->za)) {
268 return false;
269 }
270
271 $infos = array();
272
273 for ($i=0; $i<$this->count(); $i++) {
274 $info = $this->get_info($i);
275 if ($info === false) {
276 continue;
277 }
278 $infos[$i] = $info;
279 }
280
281 return $infos;
282 }
283
284 /**
01b4040a 285 * Returns number of files in archive.
d2b7803e 286 *
0b0bfa93 287 * @return int number of files
288 */
289 public function count() {
290 if (!isset($this->za)) {
291 return false;
292 }
293
294 return $this->za->numFiles;
295 }
296
297 /**
01b4040a 298 * Add file into archive.
d2b7803e 299 *
0b0bfa93 300 * @param string $localname name of file in archive
83020ca0 301 * @param string $pathname location of file
0b0bfa93 302 * @return bool success
303 */
304 public function add_file_from_pathname($localname, $pathname) {
305 if (!isset($this->za)) {
306 return false;
307 }
308
309 if ($this->archivepathname === realpath($pathname)) {
01b4040a
PS
310 // Do not add self into archive.
311 return false;
312 }
313
314 if (!is_readable($pathname) or is_dir($pathname)) {
0b0bfa93 315 return false;
316 }
317
318 if (is_null($localname)) {
319 $localname = clean_param($pathname, PARAM_PATH);
320 }
01b4040a 321 $localname = trim($localname, '/'); // No leading slashes in archives!
0b0bfa93 322 $localname = $this->mangle_pathname($localname);
323
324 if ($localname === '') {
01b4040a 325 // Sorry - conversion failed badly.
0b0bfa93 326 return false;
327 }
328
79c966cf
PS
329 if (!$this->za->addFile($pathname, $localname)) {
330 return false;
331 }
332 $this->modified = true;
333 return true;
0b0bfa93 334 }
335
336 /**
01b4040a 337 * Add content of string into archive.
d2b7803e 338 *
0b0bfa93 339 * @param string $localname name of file in archive
d2b7803e 340 * @param string $contents contents
0b0bfa93 341 * @return bool success
342 */
343 public function add_file_from_string($localname, $contents) {
344 if (!isset($this->za)) {
345 return false;
346 }
347
01b4040a 348 $localname = trim($localname, '/'); // No leading slashes in archives!
0b0bfa93 349 $localname = $this->mangle_pathname($localname);
350
351 if ($localname === '') {
01b4040a 352 // Sorry - conversion failed badly.
0b0bfa93 353 return false;
354 }
355
356 if ($this->usedmem > 2097151) {
01b4040a 357 // This prevents running out of memory when adding many large files using strings.
0b0bfa93 358 $this->close();
359 $res = $this->open($this->archivepathname, file_archive::OPEN, $this->encoding);
360 if ($res !== true) {
d2b7803e 361 print_error('cannotopenzip');
0b0bfa93 362 }
363 }
364 $this->usedmem += strlen($contents);
365
79c966cf
PS
366 if (!$this->za->addFromString($localname, $contents)) {
367 return false;
368 }
369 $this->modified = true;
370 return true;
0b0bfa93 371 }
372
373 /**
01b4040a 374 * Add empty directory into archive.
d2b7803e
DC
375 *
376 * @param string $localname name of file in archive
0b0bfa93 377 * @return bool success
378 */
379 public function add_directory($localname) {
380 if (!isset($this->za)) {
381 return false;
382 }
697ade28 383 $localname = trim($localname, '/'). '/';
0b0bfa93 384 $localname = $this->mangle_pathname($localname);
385
386 if ($localname === '/') {
01b4040a 387 // Sorry - conversion failed badly.
0b0bfa93 388 return false;
389 }
390
697ade28
VD
391 if ($localname !== '') {
392 if (!$this->za->addEmptyDir($localname)) {
393 return false;
394 }
395 $this->modified = true;
79c966cf 396 }
79c966cf 397 return true;
0b0bfa93 398 }
399
400 /**
01b4040a 401 * Returns current file info.
d2b7803e
DC
402 *
403 * @return stdClass
0b0bfa93 404 */
405 public function current() {
406 if (!isset($this->za)) {
407 return false;
408 }
409
410 return $this->get_info($this->pos);
411 }
412
413 /**
01b4040a 414 * Returns the index of current file.
d2b7803e 415 *
0b0bfa93 416 * @return int current file index
417 */
418 public function key() {
419 return $this->pos;
420 }
421
422 /**
01b4040a 423 * Moves forward to next file.
0b0bfa93 424 */
425 public function next() {
426 $this->pos++;
427 }
428
429 /**
01b4040a 430 * Rewinds back to the first file.
0b0bfa93 431 */
432 public function rewind() {
433 $this->pos = 0;
434 }
435
436 /**
437 * Did we reach the end?
d2b7803e
DC
438 *
439 * @return bool
0b0bfa93 440 */
441 public function valid() {
442 if (!isset($this->za)) {
443 return false;
444 }
445
446 return ($this->pos < $this->count());
447 }
79c966cf 448
6fb8ae95
PS
449 /**
450 * Create a map of file names used in zip archive.
451 * @return void
452 */
453 protected function init_namelookup() {
454 if (!isset($this->za)) {
455 return;
456 }
457 if (isset($this->namelookup)) {
458 return;
459 }
460
461 $this->namelookup = array();
462
463 if ($this->mode != file_archive::OPEN) {
464 // No need to tweak existing names when creating zip file because there are none yet!
465 return;
466 }
467
468 if (!file_exists($this->archivepathname)) {
469 return;
470 }
471
472 if (!$fp = fopen($this->archivepathname, 'rb')) {
473 return;
474 }
475 if (!$filesize = filesize($this->archivepathname)) {
476 return;
477 }
478
479 $centralend = self::zip_get_central_end($fp, $filesize);
480
481 if ($centralend === false or $centralend['disk'] !== 0 or $centralend['disk_start'] !== 0 or $centralend['offset'] === 0xFFFFFFFF) {
482 // Single disk archives only and o support for ZIP64, sorry.
483 fclose($fp);
484 return;
485 }
486
487 fseek($fp, $centralend['offset']);
488 $data = fread($fp, $centralend['size']);
489 $pos = 0;
490 $files = array();
491 for($i=0; $i<$centralend['entries']; $i++) {
492 $file = self::zip_parse_file_header($data, $centralend, $pos);
493 if ($file === false) {
494 // Wrong header, sorry.
495 fclose($fp);
496 return;
497 }
498 $files[] = $file;
499 }
500 fclose($fp);
501
502 foreach ($files as $file) {
503 $name = $file['name'];
504 if (preg_match('/^[a-zA-Z0-9_\-\.]*$/', $file['name'])) {
505 // No need to fix ASCII.
506 $name = fix_utf8($name);
507
508 } else if (!($file['general'] & pow(2, 11))) {
509 // First look for unicode name alternatives.
510 $found = false;
511 foreach($file['extra'] as $extra) {
512 if ($extra['id'] === 0x7075) {
513 $data = unpack('cversion/Vcrc', substr($extra['data'], 0, 5));
514 if ($data['crc'] === crc32($name)) {
515 $found = true;
516 $name = substr($extra['data'], 5);
517 }
518 }
519 }
520 if (!$found and !empty($this->encoding) and $this->encoding !== 'utf-8') {
521 // Try the encoding from open().
522 $newname = @textlib::convert($name, $this->encoding, 'utf-8');
523 $original = textlib::convert($newname, 'utf-8', $this->encoding);
524 if ($original === $name) {
525 $found = true;
526 $name = $newname;
527 }
528 }
529 if (!$found and $file['version'] === 0x315) {
530 // This looks like OS X build in zipper.
531 $newname = fix_utf8($name);
532 if ($newname === $name) {
533 $found = true;
534 $name = $newname;
535 }
536 }
537 if (!$found and $file['version'] === 0) {
538 // This looks like our old borked Moodle 2.2 file.
539 $newname = fix_utf8($name);
540 if ($newname === $name) {
541 $found = true;
542 $name = $newname;
543 }
544 }
545 if (!$found and $encoding = get_string('oldcharset', 'langconfig')) {
546 // Last attempt - try the dos/unix encoding from current language.
547 $windows = true;
548 foreach($file['extra'] as $extra) {
549 // In Windows archivers do not usually set any extras with the exception of NTFS flag in WinZip/WinRar.
550 $windows = false;
551 if ($extra['id'] === 0x000a) {
552 $windows = true;
553 break;
554 }
555 }
556
557 if ($windows === true) {
558 switch(strtoupper($encoding)) {
559 case 'ISO-8859-1': $encoding = 'CP850'; break;
560 case 'ISO-8859-2': $encoding = 'CP852'; break;
561 case 'ISO-8859-4': $encoding = 'CP775'; break;
562 case 'ISO-8859-5': $encoding = 'CP866'; break;
563 case 'ISO-8859-6': $encoding = 'CP720'; break;
564 case 'ISO-8859-7': $encoding = 'CP737'; break;
177ffd37 565 case 'ISO-8859-8': $encoding = 'CP862'; break;
6fb8ae95
PS
566 }
567 }
568 $newname = @textlib::convert($name, $encoding, 'utf-8');
569 $original = textlib::convert($newname, 'utf-8', $encoding);
570
571 if ($original === $name) {
572 $name = $newname;
573 }
574 }
575 }
576 $name = str_replace('\\', '/', $name); // no MS \ separators
577 $name = clean_param($name, PARAM_PATH); // only safe chars
578 $name = ltrim($name, '/'); // no leading slash
579
580 if (function_exists('normalizer_normalize')) {
581 $name = normalizer_normalize($name, Normalizer::FORM_C);
582 }
583
584 $this->namelookup[$file['name']] = $name;
585 }
586 }
587
79c966cf
PS
588 /**
589 * Add unicode flag to all files in archive.
590 *
591 * NOTE: single disk archives only, no ZIP64 support.
592 *
593 * @return bool success, modifies the file contents
594 */
595 protected function fix_utf8_flags() {
79c966cf
PS
596 if (!file_exists($this->archivepathname)) {
597 return true;
598 }
599
600 // Note: the ZIP structure is described at http://www.pkware.com/documents/casestudies/APPNOTE.TXT
601 if (!$fp = fopen($this->archivepathname, 'rb+')) {
602 return false;
603 }
604 if (!$filesize = filesize($this->archivepathname)) {
605 return false;
606 }
607
6fb8ae95 608 $centralend = self::zip_get_central_end($fp, $filesize);
79c966cf 609
6fb8ae95
PS
610 if ($centralend === false or $centralend['disk'] !== 0 or $centralend['disk_start'] !== 0 or $centralend['offset'] === 0xFFFFFFFF) {
611 // Single disk archives only and o support for ZIP64, sorry.
79c966cf
PS
612 fclose($fp);
613 return false;
614 }
615
616 fseek($fp, $centralend['offset']);
617 $data = fread($fp, $centralend['size']);
618 $pos = 0;
619 $files = array();
620 for($i=0; $i<$centralend['entries']; $i++) {
6fb8ae95
PS
621 $file = self::zip_parse_file_header($data, $centralend, $pos);
622 if ($file === false) {
623 // Wrong header, sorry.
79c966cf
PS
624 fclose($fp);
625 return false;
626 }
79c966cf
PS
627
628 $newgeneral = $file['general'] | pow(2, 11);
629 if ($newgeneral === $file['general']) {
630 // Nothing to do with this file.
631 continue;
632 }
633
634 if (preg_match('/^[a-zA-Z0-9_\-\.]*$/', $file['name'])) {
635 // ASCII file names are always ok.
636 continue;
637 }
00f744d2 638 if ($file['extra']) {
79c966cf
PS
639 // Most probably not created by php zip ext, better to skip it.
640 continue;
641 }
642 if (fix_utf8($file['name']) !== $file['name']) {
643 // Does not look like a valid utf-8 encoded file name, skip it.
644 continue;
645 }
646
647 // Read local file header.
648 fseek($fp, $file['local_offset']);
649 $localfile = unpack('Vsig/vversion_req/vgeneral/vmethod/vmtime/vmdate/Vcrc/Vsize_compressed/Vsize/vname_length/vextra_length', fread($fp, 30));
650 if ($localfile['sig'] !== 0x04034b50) {
651 // Borked file!
652 fclose($fp);
653 return false;
654 }
655
656 $file['local'] = $localfile;
657 $files[] = $file;
658 }
659
660 foreach ($files as $file) {
661 $localfile = $file['local'];
662 // Add the unicode flag in central file header.
663 fseek($fp, $file['central_offset'] + 8);
664 if (ftell($fp) === $file['central_offset'] + 8) {
665 $newgeneral = $file['general'] | pow(2, 11);
666 fwrite($fp, pack('v', $newgeneral));
667 }
668 // Modify local file header too.
669 fseek($fp, $file['local_offset'] + 6);
670 if (ftell($fp) === $file['local_offset'] + 6) {
671 $newgeneral = $localfile['general'] | pow(2, 11);
672 fwrite($fp, pack('v', $newgeneral));
673 }
674 }
675
676 fclose($fp);
677 return true;
678 }
6fb8ae95
PS
679
680 /**
681 * Read end of central signature of ZIP file.
682 * @internal
683 * @static
684 * @param resource $fp
685 * @param int $filesize
686 * @return array|bool
687 */
688 public static function zip_get_central_end($fp, $filesize) {
689 // Find end of central directory record.
690 fseek($fp, $filesize - 22);
691 $info = unpack('Vsig', fread($fp, 4));
692 if ($info['sig'] === 0x06054b50) {
693 // There is no comment.
694 fseek($fp, $filesize - 22);
695 $data = fread($fp, 22);
696 } else {
697 // There is some comment with 0xFF max size - that is 65557.
698 fseek($fp, $filesize - 65557);
699 $data = fread($fp, 65557);
700 }
701
702 $pos = strpos($data, pack('V', 0x06054b50));
703 if ($pos === false) {
704 // Borked ZIP structure!
705 return false;
706 }
707 $centralend = unpack('Vsig/vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_length', substr($data, $pos, 22));
708 if ($centralend['comment_length']) {
709 $centralend['comment'] = substr($data, 22, $centralend['comment_length']);
710 } else {
711 $centralend['comment'] = '';
712 }
713
714 return $centralend;
715 }
716
717 /**
01b4040a 718 * Parse file header.
6fb8ae95
PS
719 * @internal
720 * @param string $data
721 * @param array $centralend
722 * @param int $pos (modified)
723 * @return array|bool file info
724 */
725 public static function zip_parse_file_header($data, $centralend, &$pos) {
726 $file = unpack('Vsig/vversion/vversion_req/vgeneral/vmethod/Vmodified/Vcrc/Vsize_compressed/Vsize/vname_length/vextra_length/vcomment_length/vdisk/vattr/Vattrext/Vlocal_offset', substr($data, $pos, 46));
727 $file['central_offset'] = $centralend['offset'] + $pos;
728 $pos = $pos + 46;
729 if ($file['sig'] !== 0x02014b50) {
730 // Borked ZIP structure!
731 return false;
732 }
733 $file['name'] = substr($data, $pos, $file['name_length']);
734 $pos = $pos + $file['name_length'];
735 $file['extra'] = array();
736 $file['extra_data'] = '';
737 if ($file['extra_length']) {
738 $extradata = substr($data, $pos, $file['extra_length']);
739 $file['extra_data'] = $extradata;
740 while (strlen($extradata) > 4) {
741 $extra = unpack('vid/vsize', substr($extradata, 0, 4));
742 $extra['data'] = substr($extradata, 4, $extra['size']);
743 $extradata = substr($extradata, 4+$extra['size']);
744 $file['extra'][] = $extra;
745 }
746 $pos = $pos + $file['extra_length'];
747 }
748 if ($file['comment_length']) {
749 $pos = $pos + $file['comment_length'];
750 $file['comment'] = substr($data, $pos, $file['comment_length']);
751 } else {
752 $file['comment'] = '';
753 }
754 return $file;
755 }
60e40dda 756}