PageRenderTime 25ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/filestorage/zip_archive.php

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