Skip to content

Commit 8fc0df6

Browse files
committed
Avoid warnings on corrupt zip files
When a corrupt zip file is read, an exception should be thrown without any previous notices and warnings leaking. This also introduces constants for a few magic numbers in the zip format.
1 parent e45d0d9 commit 8fc0df6

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

src/Zip.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ class Zip extends Archive
1717
{
1818
const LOCAL_FILE_HEADER_CRC_OFFSET = 14;
1919

20+
const SIG_LOCAL_FILE_HEADER = "\x50\x4b\x03\x04";
21+
const SIG_CENTRAL_FILE_HEADER = "\x50\x4b\x01\x02";
22+
const SIG_END_OF_CENTRAL_DIR = "\x50\x4b\x05\x06";
23+
2024
protected $file = '';
2125
protected $fh;
2226
protected $memory = '';
@@ -89,6 +93,7 @@ public function contents()
8993
*
9094
* @see contents()
9195
* @throws ArchiveIOException
96+
* @throws ArchiveCorruptedException
9297
* @return FileInfo[]
9398
*/
9499
public function yieldContents()
@@ -129,6 +134,7 @@ public function yieldContents()
129134
* @param string $exclude a regular expression of files to exclude
130135
* @param string $include a regular expression of files to include
131136
* @throws ArchiveIOException
137+
* @throws ArchiveCorruptedException
132138
* @return FileInfo[]
133139
*/
134140
public function extract($outdir, $strip = '', $exclude = '', $include = '')
@@ -478,7 +484,7 @@ public function close()
478484
$this->writebytes($ctrldir);
479485

480486
// write end of central directory record
481-
$this->writebytes("\x50\x4b\x05\x06"); // end of central dir signature
487+
$this->writebytes(self::SIG_END_OF_CENTRAL_DIR);
482488
$this->writebytes(pack('v', 0)); // number of this disk
483489
$this->writebytes(pack('v', 0)); // number of the disk with the start of the central directory
484490
$this->writebytes(pack('v',
@@ -538,6 +544,7 @@ public function save($file)
538544
* This key-value list contains general information about the ZIP file
539545
*
540546
* @return array
547+
* @throws ArchiveCorruptedException when the file is not a valid ZIP archive
541548
*/
542549
protected function readCentralDir()
543550
{
@@ -550,21 +557,31 @@ protected function readCentralDir()
550557

551558
@fseek($this->fh, $size - $maximum_size);
552559
$pos = ftell($this->fh);
553-
$bytes = 0x00000000;
560+
$bytes = '';
554561

555562
while ($pos < $size) {
556-
$byte = @fread($this->fh, 1);
557-
$bytes = (($bytes << 8) & 0xFFFFFFFF) | ord($byte);
558-
if ($bytes == 0x504b0506) {
563+
$bytes = substr($bytes . (string)@fread($this->fh, 1), -4);
564+
if ($bytes === self::SIG_END_OF_CENTRAL_DIR) {
559565
break;
560566
}
561567
$pos++;
562568
}
563569

570+
if ($bytes !== self::SIG_END_OF_CENTRAL_DIR) {
571+
throw new ArchiveCorruptedException(
572+
'End of central directory signature not found - not a valid ZIP file'
573+
);
574+
}
575+
564576
$data = unpack(
565577
'vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size',
566578
fread($this->fh, 18)
567579
);
580+
if ($data === false) {
581+
throw new ArchiveCorruptedException(
582+
'Could not read end of central directory record'
583+
);
584+
}
568585

569586
if ($data['comment_size'] != 0) {
570587
$centd['comment'] = fread($this->fh, $data['comment_size']);
@@ -912,7 +929,7 @@ protected function makeCentralFileRecord($offset, $ts, $crc, $len, $clen, $name,
912929

913930
list($name, $extra) = $this->encodeFilename($name);
914931

915-
$header = "\x50\x4b\x01\x02"; // central file header signature
932+
$header = self::SIG_CENTRAL_FILE_HEADER;
916933
$header .= pack('v', 14); // version made by - VFAT
917934
$header .= pack('v', 20); // version needed to extract - 2.0
918935
$header .= pack('v', 0); // general purpose flag - no flags set
@@ -959,7 +976,7 @@ protected function makeLocalFileHeader($ts, $crc, $len, $clen, $name, $comp = nu
959976

960977
list($name, $extra) = $this->encodeFilename($name);
961978

962-
$header = "\x50\x4b\x03\x04"; // local file header signature
979+
$header = self::SIG_LOCAL_FILE_HEADER;
963980
$header .= pack('v', 20); // version needed to extract - 2.0
964981
$header .= pack('v', 0); // general purpose flag - no flags set
965982
$header .= pack('v', $comp); // compression method - deflate|none

tests/ZipTestCase.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,51 @@ public function testMissing()
5252
$tar->open('nope.zip');
5353
}
5454

55+
/**
56+
* Feeding a file without an End-of-Central-Directory signature must raise
57+
* ArchiveCorruptedException, not bubble up PHP warnings from unpack().
58+
*/
59+
public function testNotAZip()
60+
{
61+
$tmp = sys_get_temp_dir() . '/phpa-bad-' . md5(uniqid('', true)) . '.bin';
62+
file_put_contents($tmp, str_repeat('this is not a zip file ', 50));
63+
$zip = new Zip();
64+
$zip->open($tmp);
65+
try {
66+
$this->expectException(ArchiveCorruptedException::class);
67+
iterator_to_array($zip->yieldContents());
68+
} finally {
69+
try {
70+
$zip->close();
71+
} catch(\Exception $e) {
72+
/* already errored */
73+
}
74+
@unlink($tmp);
75+
}
76+
}
77+
78+
/**
79+
* A truncated file (smaller than the EOCD record) must also fail cleanly.
80+
*/
81+
public function testTruncatedFile()
82+
{
83+
$tmp = sys_get_temp_dir() . '/phpa-trunc-' . md5(uniqid('', true)) . '.bin';
84+
file_put_contents($tmp, "PK\x03\x04");
85+
$zip = new Zip();
86+
$zip->open($tmp);
87+
try {
88+
$this->expectException(ArchiveCorruptedException::class);
89+
iterator_to_array($zip->yieldContents());
90+
} finally {
91+
try {
92+
$zip->close();
93+
} catch(\Exception $e) {
94+
/* already errored */
95+
}
96+
@unlink($tmp);
97+
}
98+
}
99+
55100
/**
56101
* simple test that checks that the given filenames and contents can be grepped from
57102
* the uncompressed zip stream

0 commit comments

Comments
 (0)