ZipStreamTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <?php
  2. declare(strict_types=1);
  3. namespace ZipStreamTest;
  4. use org\bovigo\vfs\vfsStream;
  5. use GuzzleHttp\Psr7\Response;
  6. use PHPUnit\Framework\TestCase;
  7. use ZipStream\File;
  8. use ZipStream\Option\Archive as ArchiveOptions;
  9. use ZipStream\Option\File as FileOptions;
  10. use ZipStream\Option\Method;
  11. use ZipStream\Stream;
  12. use ZipStream\ZipStream;
  13. /**
  14. * Test Class for the Main ZipStream CLass
  15. */
  16. class ZipStreamTest extends TestCase
  17. {
  18. const OSX_ARCHIVE_UTILITY =
  19. '/System/Library/CoreServices/Applications/Archive Utility.app/Contents/MacOS/Archive Utility';
  20. public function testFileNotFoundException(): void
  21. {
  22. $this->expectException(\ZipStream\Exception\FileNotFoundException::class);
  23. // Get ZipStream Object
  24. $zip = new ZipStream();
  25. // Trigger error by adding a file which doesn't exist
  26. $zip->addFileFromPath('foobar.php', '/foo/bar/foobar.php');
  27. }
  28. public function testFileNotReadableException(): void
  29. {
  30. // create new virtual filesystem
  31. $root = vfsStream::setup('vfs');
  32. // create a virtual file with no permissions
  33. $file = vfsStream::newFile('foo.txt', 0000)->at($root)->setContent('bar');
  34. $zip = new ZipStream();
  35. $this->expectException(\ZipStream\Exception\FileNotReadableException::class);
  36. $zip->addFileFromPath('foo.txt', $file->url());
  37. }
  38. public function testDostime(): void
  39. {
  40. // Allows testing of protected method
  41. $class = new \ReflectionClass(File::class);
  42. $method = $class->getMethod('dostime');
  43. $method->setAccessible(true);
  44. $this->assertSame($method->invoke(null, 1416246368), 1165069764);
  45. // January 1 1980 - DOS Epoch.
  46. $this->assertSame($method->invoke(null, 315532800), 2162688);
  47. // January 1 1970 -> January 1 1980 due to minimum DOS Epoch. @todo Throw Exception?
  48. $this->assertSame($method->invoke(null, 0), 2162688);
  49. }
  50. public function testAddFile(): void
  51. {
  52. [$tmp, $stream] = $this->getTmpFileStream();
  53. $options = new ArchiveOptions();
  54. $options->setOutputStream($stream);
  55. $zip = new ZipStream(null, $options);
  56. $zip->addFile('sample.txt', 'Sample String Data');
  57. $zip->addFile('test/sample.txt', 'More Simple Sample Data');
  58. $zip->finish();
  59. fclose($stream);
  60. $tmpDir = $this->validateAndExtractZip($tmp);
  61. $files = $this->getRecursiveFileList($tmpDir);
  62. $this->assertEquals(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files);
  63. $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data');
  64. $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data');
  65. }
  66. /**
  67. * @return array
  68. */
  69. protected function getTmpFileStream(): array
  70. {
  71. $tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest');
  72. $stream = fopen($tmp, 'wb+');
  73. return array($tmp, $stream);
  74. }
  75. /**
  76. * @param string $tmp
  77. * @return string
  78. */
  79. protected function validateAndExtractZip($tmp): string
  80. {
  81. $tmpDir = $this->getTmpDir();
  82. $zipArch = new \ZipArchive;
  83. $res = $zipArch->open($tmp);
  84. if ($res !== true) {
  85. $this->fail("Failed to open {$tmp}. Code: $res");
  86. return $tmpDir;
  87. }
  88. $this->assertEquals(0, $zipArch->status);
  89. $this->assertEquals(0, $zipArch->statusSys);
  90. $zipArch->extractTo($tmpDir);
  91. $zipArch->close();
  92. return $tmpDir;
  93. }
  94. protected function getTmpDir(): string
  95. {
  96. $tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest');
  97. unlink($tmp);
  98. mkdir($tmp) or $this->fail('Failed to make directory');
  99. return $tmp;
  100. }
  101. /**
  102. * @param string $path
  103. * @return string[]
  104. */
  105. protected function getRecursiveFileList(string $path): array
  106. {
  107. $data = array();
  108. $path = (string)realpath($path);
  109. $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
  110. $pathLen = strlen($path);
  111. foreach ($files as $file) {
  112. $filePath = $file->getRealPath();
  113. if (!is_dir($filePath)) {
  114. $data[] = substr($filePath, $pathLen + 1);
  115. }
  116. }
  117. sort($data);
  118. return $data;
  119. }
  120. public function testAddFileUtf8NameComment(): void
  121. {
  122. [$tmp, $stream] = $this->getTmpFileStream();
  123. $options = new ArchiveOptions();
  124. $options->setOutputStream($stream);
  125. $zip = new ZipStream(null, $options);
  126. $name = 'árvíztűrő tükörfúrógép.txt';
  127. $content = 'Sample String Data';
  128. $comment =
  129. 'Filename has every special characters ' .
  130. 'from Hungarian language in lowercase. ' .
  131. 'In uppercase: ÁÍŰŐÜÖÚÓÉ';
  132. $fileOptions = new FileOptions();
  133. $fileOptions->setComment($comment);
  134. $zip->addFile($name, $content, $fileOptions);
  135. $zip->finish();
  136. fclose($stream);
  137. $tmpDir = $this->validateAndExtractZip($tmp);
  138. $files = $this->getRecursiveFileList($tmpDir);
  139. $this->assertEquals(array($name), $files);
  140. $this->assertStringEqualsFile($tmpDir . '/' . $name, $content);
  141. $zipArch = new \ZipArchive();
  142. $zipArch->open($tmp);
  143. $this->assertEquals($comment, $zipArch->getCommentName($name));
  144. }
  145. public function testAddFileUtf8NameNonUtfComment(): void
  146. {
  147. $this->expectException(\ZipStream\Exception\EncodingException::class);
  148. $stream = $this->getTmpFileStream()[1];
  149. $options = new ArchiveOptions();
  150. $options->setOutputStream($stream);
  151. $zip = new ZipStream(null, $options);
  152. $name = 'á.txt';
  153. $content = 'any';
  154. $comment = 'á';
  155. $fileOptions = new FileOptions();
  156. $fileOptions->setComment(mb_convert_encoding($comment, 'ISO-8859-2', 'UTF-8'));
  157. $zip->addFile($name, $content, $fileOptions);
  158. }
  159. public function testAddFileNonUtf8NameUtfComment(): void
  160. {
  161. $this->expectException(\ZipStream\Exception\EncodingException::class);
  162. $stream = $this->getTmpFileStream()[1];
  163. $options = new ArchiveOptions();
  164. $options->setOutputStream($stream);
  165. $zip = new ZipStream(null, $options);
  166. $name = 'á.txt';
  167. $content = 'any';
  168. $comment = 'á';
  169. $fileOptions = new FileOptions();
  170. $fileOptions->setComment($comment);
  171. $zip->addFile(mb_convert_encoding($name, 'ISO-8859-2', 'UTF-8'), $content, $fileOptions);
  172. }
  173. public function testAddFileWithStorageMethod(): void
  174. {
  175. [$tmp, $stream] = $this->getTmpFileStream();
  176. $options = new ArchiveOptions();
  177. $options->setOutputStream($stream);
  178. $zip = new ZipStream(null, $options);
  179. $fileOptions = new FileOptions();
  180. $fileOptions->setMethod(Method::STORE());
  181. $zip->addFile('sample.txt', 'Sample String Data', $fileOptions);
  182. $zip->addFile('test/sample.txt', 'More Simple Sample Data');
  183. $zip->finish();
  184. fclose($stream);
  185. $zipArch = new \ZipArchive();
  186. $zipArch->open($tmp);
  187. $sample1 = $zipArch->statName('sample.txt');
  188. $sample12 = $zipArch->statName('test/sample.txt');
  189. $this->assertEquals($sample1['comp_method'], Method::STORE);
  190. $this->assertEquals($sample12['comp_method'], Method::DEFLATE);
  191. $zipArch->close();
  192. }
  193. public function testDecompressFileWithMacUnarchiver(): void
  194. {
  195. if (!file_exists(self::OSX_ARCHIVE_UTILITY)) {
  196. $this->markTestSkipped('The Mac OSX Archive Utility is not available.');
  197. }
  198. [$tmp, $stream] = $this->getTmpFileStream();
  199. $options = new ArchiveOptions();
  200. $options->setOutputStream($stream);
  201. $zip = new ZipStream(null, $options);
  202. $folder = uniqid('', true);
  203. $zip->addFile($folder . '/sample.txt', 'Sample Data');
  204. $zip->finish();
  205. fclose($stream);
  206. exec(escapeshellarg(self::OSX_ARCHIVE_UTILITY) . ' ' . escapeshellarg($tmp), $output, $returnStatus);
  207. $this->assertEquals(0, $returnStatus);
  208. $this->assertCount(0, $output);
  209. $this->assertFileExists(dirname($tmp) . '/' . $folder . '/sample.txt');
  210. $this->assertStringEqualsFile(dirname($tmp) . '/' . $folder . '/sample.txt', 'Sample Data');
  211. }
  212. public function testAddFileFromPath(): void
  213. {
  214. [$tmp, $stream] = $this->getTmpFileStream();
  215. $options = new ArchiveOptions();
  216. $options->setOutputStream($stream);
  217. $zip = new ZipStream(null, $options);
  218. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  219. fwrite($streamExample, 'Sample String Data');
  220. fclose($streamExample);
  221. $zip->addFileFromPath('sample.txt', $tmpExample);
  222. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  223. fwrite($streamExample, 'More Simple Sample Data');
  224. fclose($streamExample);
  225. $zip->addFileFromPath('test/sample.txt', $tmpExample);
  226. $zip->finish();
  227. fclose($stream);
  228. $tmpDir = $this->validateAndExtractZip($tmp);
  229. $files = $this->getRecursiveFileList($tmpDir);
  230. $this->assertEquals(array('sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'), $files);
  231. $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data');
  232. $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data');
  233. }
  234. public function testAddFileFromPathWithStorageMethod(): void
  235. {
  236. [$tmp, $stream] = $this->getTmpFileStream();
  237. $options = new ArchiveOptions();
  238. $options->setOutputStream($stream);
  239. $zip = new ZipStream(null, $options);
  240. $fileOptions = new FileOptions();
  241. $fileOptions->setMethod(Method::STORE());
  242. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  243. fwrite($streamExample, 'Sample String Data');
  244. fclose($streamExample);
  245. $zip->addFileFromPath('sample.txt', $tmpExample, $fileOptions);
  246. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  247. fwrite($streamExample, 'More Simple Sample Data');
  248. fclose($streamExample);
  249. $zip->addFileFromPath('test/sample.txt', $tmpExample);
  250. $zip->finish();
  251. fclose($stream);
  252. $zipArch = new \ZipArchive();
  253. $zipArch->open($tmp);
  254. $sample1 = $zipArch->statName('sample.txt');
  255. $this->assertEquals(Method::STORE, $sample1['comp_method']);
  256. $sample2 = $zipArch->statName('test/sample.txt');
  257. $this->assertEquals(Method::DEFLATE, $sample2['comp_method']);
  258. $zipArch->close();
  259. }
  260. public function testAddLargeFileFromPath(): void
  261. {
  262. $methods = [Method::DEFLATE(), Method::STORE()];
  263. $falseTrue = [false, true];
  264. foreach ($methods as $method) {
  265. foreach ($falseTrue as $zeroHeader) {
  266. foreach ($falseTrue as $zip64) {
  267. if ($zeroHeader && $method->equals(Method::DEFLATE())) {
  268. continue;
  269. }
  270. $this->addLargeFileFileFromPath($method, $zeroHeader, $zip64);
  271. }
  272. }
  273. }
  274. }
  275. protected function addLargeFileFileFromPath($method, $zeroHeader, $zip64): void
  276. {
  277. [$tmp, $stream] = $this->getTmpFileStream();
  278. $options = new ArchiveOptions();
  279. $options->setOutputStream($stream);
  280. $options->setLargeFileMethod($method);
  281. $options->setLargeFileSize(5);
  282. $options->setZeroHeader($zeroHeader);
  283. $options->setEnableZip64($zip64);
  284. $zip = new ZipStream(null, $options);
  285. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  286. for ($i = 0; $i <= 10000; $i++) {
  287. fwrite($streamExample, sha1((string)$i));
  288. if ($i % 100 === 0) {
  289. fwrite($streamExample, "\n");
  290. }
  291. }
  292. fclose($streamExample);
  293. $shaExample = sha1_file($tmpExample);
  294. $zip->addFileFromPath('sample.txt', $tmpExample);
  295. unlink($tmpExample);
  296. $zip->finish();
  297. fclose($stream);
  298. $tmpDir = $this->validateAndExtractZip($tmp);
  299. $files = $this->getRecursiveFileList($tmpDir);
  300. $this->assertEquals(array('sample.txt'), $files);
  301. $this->assertEquals(sha1_file($tmpDir . '/sample.txt'), $shaExample, "SHA-1 Mismatch Method: {$method}");
  302. }
  303. public function testAddFileFromStream(): void
  304. {
  305. [$tmp, $stream] = $this->getTmpFileStream();
  306. $options = new ArchiveOptions();
  307. $options->setOutputStream($stream);
  308. $zip = new ZipStream(null, $options);
  309. // In this test we can't use temporary stream to feed data
  310. // because zlib.deflate filter gives empty string before PHP 7
  311. // it works fine with file stream
  312. $streamExample = fopen(__FILE__, 'rb');
  313. $zip->addFileFromStream('sample.txt', $streamExample);
  314. // fclose($streamExample);
  315. $fileOptions = new FileOptions();
  316. $fileOptions->setMethod(Method::STORE());
  317. $streamExample2 = fopen('php://temp', 'wb+');
  318. fwrite($streamExample2, 'More Simple Sample Data');
  319. rewind($streamExample2); // move the pointer back to the beginning of file.
  320. $zip->addFileFromStream('test/sample.txt', $streamExample2, $fileOptions);
  321. // fclose($streamExample2);
  322. $zip->finish();
  323. fclose($stream);
  324. $tmpDir = $this->validateAndExtractZip($tmp);
  325. $files = $this->getRecursiveFileList($tmpDir);
  326. $this->assertEquals(array('sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'), $files);
  327. $this->assertStringEqualsFile(__FILE__, file_get_contents($tmpDir . '/sample.txt'));
  328. $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data');
  329. }
  330. public function testAddFileFromStreamWithStorageMethod(): void
  331. {
  332. [$tmp, $stream] = $this->getTmpFileStream();
  333. $options = new ArchiveOptions();
  334. $options->setOutputStream($stream);
  335. $zip = new ZipStream(null, $options);
  336. $fileOptions = new FileOptions();
  337. $fileOptions->setMethod(Method::STORE());
  338. $streamExample = fopen('php://temp', 'wb+');
  339. fwrite($streamExample, 'Sample String Data');
  340. rewind($streamExample); // move the pointer back to the beginning of file.
  341. $zip->addFileFromStream('sample.txt', $streamExample, $fileOptions);
  342. // fclose($streamExample);
  343. $streamExample2 = fopen('php://temp', 'bw+');
  344. fwrite($streamExample2, 'More Simple Sample Data');
  345. rewind($streamExample2); // move the pointer back to the beginning of file.
  346. $zip->addFileFromStream('test/sample.txt', $streamExample2);
  347. // fclose($streamExample2);
  348. $zip->finish();
  349. fclose($stream);
  350. $zipArch = new \ZipArchive();
  351. $zipArch->open($tmp);
  352. $sample1 = $zipArch->statName('sample.txt');
  353. $this->assertEquals(Method::STORE, $sample1['comp_method']);
  354. $sample2 = $zipArch->statName('test/sample.txt');
  355. $this->assertEquals(Method::DEFLATE, $sample2['comp_method']);
  356. $zipArch->close();
  357. }
  358. public function testAddFileFromPsr7Stream(): void
  359. {
  360. [$tmp, $stream] = $this->getTmpFileStream();
  361. $options = new ArchiveOptions();
  362. $options->setOutputStream($stream);
  363. $zip = new ZipStream(null, $options);
  364. $body = 'Sample String Data';
  365. $response = new Response(200, [], $body);
  366. $fileOptions = new FileOptions();
  367. $fileOptions->setMethod(Method::STORE());
  368. $zip->addFileFromPsr7Stream('sample.json', $response->getBody(), $fileOptions);
  369. $zip->finish();
  370. fclose($stream);
  371. $tmpDir = $this->validateAndExtractZip($tmp);
  372. $files = $this->getRecursiveFileList($tmpDir);
  373. $this->assertEquals(array('sample.json'), $files);
  374. $this->assertStringEqualsFile($tmpDir . '/sample.json', $body);
  375. }
  376. public function testAddFileFromPsr7StreamWithOutputToPsr7Stream(): void
  377. {
  378. [$tmp, $resource] = $this->getTmpFileStream();
  379. $psr7OutputStream = new Stream($resource);
  380. $options = new ArchiveOptions();
  381. $options->setOutputStream($psr7OutputStream);
  382. $zip = new ZipStream(null, $options);
  383. $body = 'Sample String Data';
  384. $response = new Response(200, [], $body);
  385. $fileOptions = new FileOptions();
  386. $fileOptions->setMethod(Method::STORE());
  387. $zip->addFileFromPsr7Stream('sample.json', $response->getBody(), $fileOptions);
  388. $zip->finish();
  389. $psr7OutputStream->close();
  390. $tmpDir = $this->validateAndExtractZip($tmp);
  391. $files = $this->getRecursiveFileList($tmpDir);
  392. $this->assertEquals(array('sample.json'), $files);
  393. $this->assertStringEqualsFile($tmpDir . '/sample.json', $body);
  394. }
  395. public function testAddFileFromPsr7StreamWithFileSizeSet(): void
  396. {
  397. [$tmp, $stream] = $this->getTmpFileStream();
  398. $options = new ArchiveOptions();
  399. $options->setOutputStream($stream);
  400. $zip = new ZipStream(null, $options);
  401. $body = 'Sample String Data';
  402. $fileSize = strlen($body);
  403. // Add fake padding
  404. $fakePadding = "\0\0\0\0\0\0";
  405. $response = new Response(200, [], $body . $fakePadding);
  406. $fileOptions = new FileOptions();
  407. $fileOptions->setMethod(Method::STORE());
  408. $fileOptions->setSize($fileSize);
  409. $zip->addFileFromPsr7Stream('sample.json', $response->getBody(), $fileOptions);
  410. $zip->finish();
  411. fclose($stream);
  412. $tmpDir = $this->validateAndExtractZip($tmp);
  413. $files = $this->getRecursiveFileList($tmpDir);
  414. $this->assertEquals(array('sample.json'), $files);
  415. $this->assertStringEqualsFile($tmpDir . '/sample.json', $body);
  416. }
  417. public function testCreateArchiveWithFlushOptionSet(): void
  418. {
  419. [$tmp, $stream] = $this->getTmpFileStream();
  420. $options = new ArchiveOptions();
  421. $options->setOutputStream($stream);
  422. $options->setFlushOutput(true);
  423. $zip = new ZipStream(null, $options);
  424. $zip->addFile('sample.txt', 'Sample String Data');
  425. $zip->addFile('test/sample.txt', 'More Simple Sample Data');
  426. $zip->finish();
  427. fclose($stream);
  428. $tmpDir = $this->validateAndExtractZip($tmp);
  429. $files = $this->getRecursiveFileList($tmpDir);
  430. $this->assertEquals(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files);
  431. $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data');
  432. $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data');
  433. }
  434. public function testCreateArchiveWithOutputBufferingOffAndFlushOptionSet(): void
  435. {
  436. // WORKAROUND (1/2): remove phpunit's output buffer in order to run test without any buffering
  437. ob_end_flush();
  438. $this->assertEquals(0, ob_get_level());
  439. [$tmp, $stream] = $this->getTmpFileStream();
  440. $options = new ArchiveOptions();
  441. $options->setOutputStream($stream);
  442. $options->setFlushOutput(true);
  443. $zip = new ZipStream(null, $options);
  444. $zip->addFile('sample.txt', 'Sample String Data');
  445. $zip->finish();
  446. fclose($stream);
  447. $tmpDir = $this->validateAndExtractZip($tmp);
  448. $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data');
  449. // WORKAROUND (2/2): add back output buffering so that PHPUnit doesn't complain that it is missing
  450. ob_start();
  451. }
  452. }