Skip to content

Commit aae45ac

Browse files
authored
Merge pull request #6 from xp-framework/feature/list-without-password
Support listing entries without password
2 parents b129f6d + 9ad8927 commit aae45ac

File tree

7 files changed

+176
-93
lines changed

7 files changed

+176
-93
lines changed

src/main/php/io/archive/zip/AbstractZipReaderImpl.class.php

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -221,55 +221,61 @@ public function currentEntry() {
221221
$this->skip= $header['compressed'] + 16;
222222
}
223223

224+
$stream= new ZipFileInputStream($this, $this->position, $header['compressed']);
225+
224226
// AES vs. traditional PKZIP cipher
225227
if (99 === $header['compression']) {
226-
if (null === $this->password) {
227-
throw new IllegalAccessException('No password set');
228-
}
229-
230228
$aes= unpack('vheader/vsize/vversion/a2vendor/cstrength/vcompression', $extra);
231-
switch ($aes['strength']) {
232-
case 1: $sl= 8; $dl= 16; break;
233-
case 2: $sl= 12; $dl= 24; break;
234-
case 3: $sl= 16; $dl= 32; break;
235-
default: throw new IllegalArgumentException('Invalid AES strength '.$aes['strength']);
236-
}
229+
$header['compression']= $aes['compression'];
230+
$is= function() use($header, $stream, $aes) {
231+
if (null === $this->password) {
232+
throw new IllegalAccessException('No password set');
233+
}
237234

238-
// Verify password
239-
$salt= $this->streamRead($sl);
240-
$pvv= $this->streamRead(2);
241-
$dk= hash_pbkdf2('sha1', $this->password->reveal(), $salt, 1000, 2 * $dl + 2, true);
242-
if (0 !== substr_compare($dk, $pvv, 2 * $dl, 2)) {
243-
throw new IllegalAccessException('The password did not match');
244-
}
235+
switch ($aes['strength']) {
236+
case 1: $sl= 8; $dl= 16; break;
237+
case 2: $sl= 12; $dl= 24; break;
238+
case 3: $sl= 16; $dl= 32; break;
239+
default: throw new IllegalArgumentException('Invalid AES strength '.$aes['strength']);
240+
}
245241

246-
$this->skip-= $sl + 2;
247-
$header['compression']= $aes['compression'];
248-
$is= new AESInputStream(
249-
new ZipFileInputStream($this, $this->position, $header['compressed'] - $sl - 2),
250-
substr($dk, 0, $dl),
251-
substr($dk, $dl, $dl)
252-
);
242+
// Verify password
243+
$stream->seek(0);
244+
$salt= $stream->read($sl);
245+
$pvv= $stream->read(2);
246+
$dk= hash_pbkdf2('sha1', $this->password->reveal(), $salt, 1000, 2 * $dl + 2, true);
247+
if (0 !== substr_compare($dk, $pvv, 2 * $dl, 2)) {
248+
throw new IllegalAccessException('The password did not match');
249+
}
250+
251+
return new AESInputStream(
252+
$stream,
253+
substr($dk, 0, $dl),
254+
substr($dk, $dl, $dl)
255+
);
256+
};
253257
} else if ($header['flags'] & 1) {
254-
if (null === $this->password) {
255-
throw new IllegalAccessException('No password set');
256-
}
258+
$is= function() use($header, $stream) {
259+
if (null === $this->password) {
260+
throw new IllegalAccessException('No password set');
261+
}
257262

258-
// Verify password
259-
$cipher= new ZipCipher();
260-
$cipher->initialize(iconv(\xp::ENCODING, 'cp437', $this->password->reveal()));
261-
$preamble= $cipher->decipher($this->streamRead(12));
262-
if (ord($preamble[11]) !== (($header['crc'] >> 24) & 0xff)) {
263-
throw new IllegalAccessException('The password did not match');
264-
}
265-
266-
$this->skip-= 12;
267-
$is= new DecipheringInputStream(
268-
new ZipFileInputStream($this, $this->position, $header['compressed'] - 12),
269-
$cipher
270-
);
263+
// Verify password
264+
$stream->seek(0);
265+
$cipher= new ZipCipher();
266+
$cipher->initialize(iconv(\xp::ENCODING, 'cp437', $this->password->reveal()));
267+
$preamble= $cipher->decipher($stream->read(12));
268+
if (ord($preamble[11]) !== (($header['crc'] >> 24) & 0xff)) {
269+
throw new IllegalAccessException('The password did not match');
270+
}
271+
272+
return new DecipheringInputStream($stream, $cipher);
273+
};
271274
} else {
272-
$is= new ZipFileInputStream($this, $this->position, $header['compressed']);
275+
$is= function() use($stream) {
276+
$stream->seek(0);
277+
return $stream;
278+
};
273279
}
274280

275281
// Create ZipEntry object and return it

src/main/php/io/archive/zip/SequentialZipReaderImpl.class.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php namespace io\archive\zip;
22

33
use io\streams\Seekable;
4-
use lang\{IllegalArgumentException, IllegalStateException};
4+
use lang\IllegalStateException;
55

66
/** Zip archive reader that works on any input stream */
77
class SequentialZipReaderImpl extends AbstractZipReaderImpl {
@@ -37,6 +37,6 @@ public function nextEntry() {
3737
* @param int whence
3838
*/
3939
protected function streamSeek($offset, $whence) {
40-
throw new IllegalArgumentException('Stream not seekable');
40+
throw new IllegalStateException('Stream not seekable');
4141
}
4242
}

src/main/php/io/archive/zip/ZipFileEntry.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public function isDirectory() {
117117
* @return io.streams.InputStream
118118
*/
119119
public function in() {
120-
return $this->compression[0]->getDecompressionStream($this->is);
120+
return $this->compression[0]->getDecompressionStream(($this->is)());
121121
}
122122

123123
/**

src/main/php/io/archive/zip/ZipFileInputStream.class.php

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,55 @@
11
<?php namespace io\archive\zip;
22

3-
use io\IOException;
4-
use io\streams\InputStream;
3+
use io\streams\{Seekable, InputStream};
54

65
/**
76
* Zip File input stream. Reads from the current position up until a
87
* certain length.
98
*/
10-
class ZipFileInputStream implements InputStream {
11-
protected
12-
$reader = null,
13-
$start = 0,
14-
$pos = 0,
15-
$length = 0;
9+
class ZipFileInputStream implements InputStream, Seekable {
10+
protected $reader, $start, $length;
11+
protected $pos= 0;
1612

1713
/**
1814
* Constructor
1915
*
20-
* @param io.archive.zip.AbstractZipReaderImpl reader
21-
* @param int start
22-
* @param int length
16+
* @param io.archive.zip.AbstractZipReaderImpl $reader
17+
* @param int $start
18+
* @param int $length
2319
*/
2420
public function __construct(AbstractZipReaderImpl $reader, $start, $length) {
2521
$this->reader= $reader;
2622
$this->start= $start;
2723
$this->length= $length;
2824
}
2925

26+
/** @return int */
27+
public function tell() { return $this->pos; }
28+
29+
/**
30+
* Seek
31+
*
32+
* @param int $offset
33+
* @param int $whence
34+
* @return void
35+
*/
36+
public function seek($offset, $whence= SEEK_SET) {
37+
switch ($whence) {
38+
case SEEK_SET: $this->pos= $offset; break;
39+
case SEEK_END: $this->pos= $length + $offset; break;
40+
case SEEK_CUR: $this->pos+= $offset; break;
41+
}
42+
$this->reader->streamPosition($this->start + $this->pos);
43+
}
44+
3045
/**
3146
* Read a string
3247
*
33-
* @param int limit default 8192
34-
* @return string
48+
* @param int $limit default 8192
49+
* @return string
3550
*/
3651
public function read($limit= 8192) {
37-
if (0 === $this->pos) {
38-
$this->reader->streamPosition($this->start);
39-
} else if ($this->pos >= $this->length) {
40-
throw new IOException('EOF');
41-
}
42-
$chunk= $this->reader->streamRead(min($limit, $this->length- $this->pos));
52+
$chunk= $this->reader->streamRead(min($limit, $this->length - $this->pos));
4353
$l= strlen($chunk);
4454
$this->pos+= $l;
4555
$this->reader->skip-= $l;
@@ -50,6 +60,7 @@ public function read($limit= 8192) {
5060
* Returns the number of bytes that can be read from this stream
5161
* without blocking.
5262
*
63+
* @return int
5364
*/
5465
public function available() {
5566
return $this->pos < $this->length ? $this->reader->streamAvailable() : 0;
@@ -58,6 +69,7 @@ public function available() {
5869
/**
5970
* Close this buffer
6071
*
72+
* @return void
6173
*/
6274
public function close() {
6375
// NOOP, leave underlying stream open

src/test/php/io/archive/zip/unittest/AbstractZipFileTest.class.php

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?php namespace io\archive\zip\unittest;
22

3-
use io\archive\zip\{ZipArchiveReader, ZipEntry, ZipFile};
4-
use io\streams\Streams;
3+
use io\File;
4+
use io\archive\zip\{ZipArchiveReader, ZipEntry};
5+
use io\streams\{Streams, InputStream};
56
use test\verify\Runtime;
67
use util\Secret;
78

@@ -15,11 +16,45 @@
1516
abstract class AbstractZipFileTest {
1617

1718
/** @return iterable */
18-
private function passwords() {
19+
protected function passwords() {
1920
yield ['secret'];
2021
yield [new Secret('secret')];
2122
}
2223

24+
/**
25+
* Returns a random access input stream for a given zip file
26+
*
27+
* @param string $package
28+
* @param string $name
29+
* @return io.streams.InputStream
30+
*/
31+
protected function randomAccess($package, $name) {
32+
return typeof($this)
33+
->getPackage()
34+
->getPackage($package)
35+
->getResourceAsStream($name.'.zip')
36+
->in()
37+
;
38+
}
39+
40+
/**
41+
* Returns a sequential access input stream for a given zip file
42+
*
43+
* @param string $package
44+
* @param string $name
45+
* @return io.streams.InputStream
46+
*/
47+
protected function sequentialAccess($package, $name) {
48+
$resource= typeof($this)->getPackage()->getPackage($package)->getResourceAsStream($name.'.zip');
49+
return newinstance(InputStream::class, [$resource], [
50+
'file' => null,
51+
'__construct' => function($file) { $this->file= $file->open(File::READ); },
52+
'read' => function($limit= 8192) { return $this->file->read($limit); },
53+
'available' => function() { return $this->file->eof() ? 0 : 1; },
54+
'close' => function() { $this->file->close(); },
55+
]);
56+
}
57+
2358
/**
2459
* Returns entry content; or NULL for directories
2560
*
@@ -42,12 +77,7 @@ protected function entryContent(ZipEntry $entry) {
4277
* @return io.archive.zip.ZipArchiveReader
4378
*/
4479
protected function archiveReaderFor($package, $name) {
45-
return ZipFile::open(typeof($this)
46-
->getPackage()
47-
->getPackage($package)
48-
->getResourceAsStream($name.'.zip')
49-
->in()
50-
);
80+
return new ZipArchiveReader($this->randomAccess($package, $name));
5181
}
5282

5383
/**

src/test/php/io/archive/zip/unittest/ZipFileContentsTest.class.php

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
<?php namespace io\archive\zip\unittest;
22

33
use io\archive\zip\ZipArchiveReader;
4-
use io\streams\Streams;
4+
use io\streams\InputStream;
5+
use lang\IllegalStateException;
56
use test\{Assert, Test};
67

7-
/**
8-
* Base class for testing zip file contents
9-
*
10-
* @see xp://net.xp_framework.unittest.io.archive.MalformedZipFileTest
11-
* @see xp://net.xp_framework.unittest.io.archive.vendors.ZipFileVendorTest
12-
*/
138
abstract class ZipFileContentsTest extends AbstractZipFileTest {
149

1510
/**
@@ -53,9 +48,31 @@ public function twofiles() {
5348
}
5449

5550
#[Test]
56-
public function loadContentAfterIteration() {
57-
$entries= $this->entriesIn($this->archiveReaderFor('fixtures', 'twofiles'));
51+
public function load_content_after_iteration() {
52+
$reader= new ZipArchiveReader($this->randomAccess('fixtures', 'twofiles'));
53+
$entries= iterator_to_array($reader->entries());
54+
5855
Assert::equals('Eins', $this->entryContent($entries[0]));
5956
Assert::equals('Zwei', $this->entryContent($entries[1]));
6057
}
58+
59+
#[Test]
60+
public function load_content_twice_from_seekable() {
61+
$reader= new ZipArchiveReader($this->randomAccess('fixtures', 'onefile'));
62+
$entry= $reader->iterator()->next();
63+
64+
Assert::equals('World', $this->entryContent($entry));
65+
Assert::equals('World', $this->entryContent($entry));
66+
}
67+
68+
#[Test]
69+
public function load_content_twice_from_unseekable() {
70+
$reader= new ZipArchiveReader($this->sequentialAccess('fixtures', 'onefile'));
71+
$entry= $reader->iterator()->next();
72+
73+
Assert::equals('World', $this->entryContent($entry));
74+
Assert::throws(IllegalStateException::class, function() use($entry) {
75+
$this->entryContent($entry);
76+
});
77+
}
6178
}

0 commit comments

Comments
 (0)