ZipStreamTest.php 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195
  1. <?php
  2. declare(strict_types=1);
  3. namespace ZipStream\Test;
  4. use DateTimeImmutable;
  5. use GuzzleHttp\Psr7\Response;
  6. use GuzzleHttp\Psr7\StreamWrapper;
  7. use org\bovigo\vfs\vfsStream;
  8. use PHPUnit\Framework\TestCase;
  9. use Psr\Http\Message\StreamInterface;
  10. use RuntimeException;
  11. use ZipArchive;
  12. use ZipStream\CompressionMethod;
  13. use ZipStream\Exception\FileNotFoundException;
  14. use ZipStream\Exception\FileNotReadableException;
  15. use ZipStream\Exception\FileSizeIncorrectException;
  16. use ZipStream\Exception\OverflowException;
  17. use ZipStream\Exception\ResourceActionException;
  18. use ZipStream\Exception\SimulationFileUnknownException;
  19. use ZipStream\Exception\StreamNotReadableException;
  20. use ZipStream\Exception\StreamNotSeekableException;
  21. use ZipStream\OperationMode;
  22. use ZipStream\PackField;
  23. use ZipStream\ZipStream;
  24. class ZipStreamTest extends TestCase
  25. {
  26. use Util;
  27. use Assertions;
  28. use Tempfile;
  29. public function testAddFile(): void
  30. {
  31. $zip = new ZipStream(
  32. outputStream: $this->tempfileStream,
  33. sendHttpHeaders: false,
  34. );
  35. $zip->addFile('sample.txt', 'Sample String Data');
  36. $zip->addFile('test/sample.txt', 'More Simple Sample Data');
  37. $zip->finish();
  38. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  39. $files = $this->getRecursiveFileList($tmpDir);
  40. $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files);
  41. $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data');
  42. $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data');
  43. }
  44. public function testAddFileUtf8NameComment(): void
  45. {
  46. $zip = new ZipStream(
  47. outputStream: $this->tempfileStream,
  48. sendHttpHeaders: false,
  49. );
  50. $name = 'árvíztűrő tükörfúrógép.txt';
  51. $content = 'Sample String Data';
  52. $comment =
  53. 'Filename has every special characters ' .
  54. 'from Hungarian language in lowercase. ' .
  55. 'In uppercase: ÁÍŰŐÜÖÚÓÉ';
  56. $zip->addFile(fileName: $name, data: $content, comment: $comment);
  57. $zip->finish();
  58. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  59. $files = $this->getRecursiveFileList($tmpDir);
  60. $this->assertSame([$name], $files);
  61. $this->assertStringEqualsFile($tmpDir . '/' . $name, $content);
  62. $zipArchive = new ZipArchive();
  63. $zipArchive->open($this->tempfile);
  64. $this->assertSame($comment, $zipArchive->getCommentName($name));
  65. }
  66. public function testAddFileUtf8NameNonUtfComment(): void
  67. {
  68. $zip = new ZipStream(
  69. outputStream: $this->tempfileStream,
  70. sendHttpHeaders: false,
  71. );
  72. $name = 'á.txt';
  73. $content = 'any';
  74. $comment = mb_convert_encoding('á', 'ISO-8859-2', 'UTF-8');
  75. // @see https://libzip.org/documentation/zip_file_get_comment.html
  76. //
  77. // mb_convert_encoding hasn't CP437.
  78. // nearly CP850 (DOS-Latin-1)
  79. $guessComment = mb_convert_encoding($comment, 'UTF-8', 'CP850');
  80. $zip->addFile(fileName: $name, data: $content, comment: $comment);
  81. $zip->finish();
  82. $zipArch = new ZipArchive();
  83. $zipArch->open($this->tempfile);
  84. $this->assertSame($guessComment, $zipArch->getCommentName($name));
  85. $this->assertSame($comment, $zipArch->getCommentName($name, ZipArchive::FL_ENC_RAW));
  86. }
  87. public function testAddFileWithStorageMethod(): void
  88. {
  89. $zip = new ZipStream(
  90. outputStream: $this->tempfileStream,
  91. sendHttpHeaders: false,
  92. );
  93. $zip->addFile(fileName: 'sample.txt', data: 'Sample String Data', compressionMethod: CompressionMethod::STORE);
  94. $zip->addFile(fileName: 'test/sample.txt', data: 'More Simple Sample Data');
  95. $zip->finish();
  96. $zipArchive = new ZipArchive();
  97. $zipArchive->open($this->tempfile);
  98. $sample1 = $zipArchive->statName('sample.txt');
  99. $sample12 = $zipArchive->statName('test/sample.txt');
  100. $this->assertSame($sample1['comp_method'], CompressionMethod::STORE->value);
  101. $this->assertSame($sample12['comp_method'], CompressionMethod::DEFLATE->value);
  102. $zipArchive->close();
  103. }
  104. public function testAddFileFromPath(): void
  105. {
  106. $zip = new ZipStream(
  107. outputStream: $this->tempfileStream,
  108. sendHttpHeaders: false,
  109. );
  110. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  111. fwrite($streamExample, 'Sample String Data');
  112. fclose($streamExample);
  113. $zip->addFileFromPath(fileName: 'sample.txt', path: $tmpExample);
  114. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  115. fwrite($streamExample, 'More Simple Sample Data');
  116. fclose($streamExample);
  117. $zip->addFileFromPath(fileName: 'test/sample.txt', path: $tmpExample);
  118. $zip->finish();
  119. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  120. $files = $this->getRecursiveFileList($tmpDir);
  121. $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files);
  122. $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data');
  123. $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data');
  124. unlink($tmpExample);
  125. }
  126. public function testAddFileFromPathFileNotFoundException(): void
  127. {
  128. $this->expectException(FileNotFoundException::class);
  129. // Get ZipStream Object
  130. $zip = new ZipStream(
  131. outputStream: $this->tempfileStream,
  132. sendHttpHeaders: false,
  133. );
  134. // Trigger error by adding a file which doesn't exist
  135. $zip->addFileFromPath(fileName: 'foobar.php', path: '/foo/bar/foobar.php');
  136. }
  137. public function testAddFileFromPathFileNotReadableException(): void
  138. {
  139. $this->expectException(FileNotReadableException::class);
  140. // create new virtual filesystem
  141. $root = vfsStream::setup('vfs');
  142. // create a virtual file with no permissions
  143. $file = vfsStream::newFile('foo.txt', 0)->at($root)->setContent('bar');
  144. // Get ZipStream Object
  145. $zip = new ZipStream(
  146. outputStream: $this->tempfileStream,
  147. sendHttpHeaders: false,
  148. );
  149. $zip->addFileFromPath('foo.txt', $file->url());
  150. }
  151. public function testAddFileFromPathWithStorageMethod(): void
  152. {
  153. $zip = new ZipStream(
  154. outputStream: $this->tempfileStream,
  155. sendHttpHeaders: false,
  156. );
  157. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  158. fwrite($streamExample, 'Sample String Data');
  159. fclose($streamExample);
  160. $zip->addFileFromPath(fileName: 'sample.txt', path: $tmpExample, compressionMethod: CompressionMethod::STORE);
  161. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  162. fwrite($streamExample, 'More Simple Sample Data');
  163. fclose($streamExample);
  164. $zip->addFileFromPath('test/sample.txt', $tmpExample);
  165. $zip->finish();
  166. $zipArchive = new ZipArchive();
  167. $zipArchive->open($this->tempfile);
  168. $sample1 = $zipArchive->statName('sample.txt');
  169. $this->assertSame(CompressionMethod::STORE->value, $sample1['comp_method']);
  170. $sample2 = $zipArchive->statName('test/sample.txt');
  171. $this->assertSame(CompressionMethod::DEFLATE->value, $sample2['comp_method']);
  172. $zipArchive->close();
  173. }
  174. public function testAddLargeFileFromPath(): void
  175. {
  176. foreach ([CompressionMethod::DEFLATE, CompressionMethod::STORE] as $compressionMethod) {
  177. foreach ([false, true] as $zeroHeader) {
  178. foreach ([false, true] as $zip64) {
  179. if ($zeroHeader && $compressionMethod === CompressionMethod::DEFLATE) {
  180. continue;
  181. }
  182. $this->addLargeFileFileFromPath(
  183. compressionMethod: $compressionMethod,
  184. zeroHeader: $zeroHeader,
  185. zip64: $zip64
  186. );
  187. }
  188. }
  189. }
  190. }
  191. public function testAddFileFromStream(): void
  192. {
  193. $zip = new ZipStream(
  194. outputStream: $this->tempfileStream,
  195. sendHttpHeaders: false,
  196. );
  197. // In this test we can't use temporary stream to feed data
  198. // because zlib.deflate filter gives empty string before PHP 7
  199. // it works fine with file stream
  200. $streamExample = fopen(__FILE__, 'rb');
  201. $zip->addFileFromStream('sample.txt', $streamExample);
  202. fclose($streamExample);
  203. $streamExample2 = fopen('php://temp', 'wb+');
  204. fwrite($streamExample2, 'More Simple Sample Data');
  205. rewind($streamExample2); // move the pointer back to the beginning of file.
  206. $zip->addFileFromStream('test/sample.txt', $streamExample2); //, $fileOptions);
  207. fclose($streamExample2);
  208. $zip->finish();
  209. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  210. $files = $this->getRecursiveFileList($tmpDir);
  211. $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files);
  212. $this->assertStringEqualsFile(__FILE__, file_get_contents($tmpDir . '/sample.txt'));
  213. $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data');
  214. }
  215. public function testAddFileFromStreamUnreadableInput(): void
  216. {
  217. $this->expectException(StreamNotReadableException::class);
  218. [$tmpInput] = $this->getTmpFileStream();
  219. $zip = new ZipStream(
  220. outputStream: $this->tempfileStream,
  221. sendHttpHeaders: false,
  222. );
  223. $streamUnreadable = fopen($tmpInput, 'w');
  224. $zip->addFileFromStream('sample.json', $streamUnreadable);
  225. }
  226. public function testAddFileFromStreamBrokenOutputWrite(): void
  227. {
  228. $this->expectException(ResourceActionException::class);
  229. $outputStream = FaultInjectionResource::getResource(['stream_write']);
  230. $zip = new ZipStream(
  231. outputStream: $outputStream,
  232. sendHttpHeaders: false,
  233. );
  234. $zip->addFile('sample.txt', 'foobar');
  235. }
  236. public function testAddFileFromStreamBrokenInputRewind(): void
  237. {
  238. $this->expectException(ResourceActionException::class);
  239. $zip = new ZipStream(
  240. outputStream: $this->tempfileStream,
  241. sendHttpHeaders: false,
  242. defaultEnableZeroHeader: false,
  243. );
  244. $fileStream = FaultInjectionResource::getResource(['stream_seek']);
  245. $zip->addFileFromStream('sample.txt', $fileStream, maxSize: 0);
  246. }
  247. public function testAddFileFromStreamUnseekableInputWithoutZeroHeader(): void
  248. {
  249. $this->expectException(StreamNotSeekableException::class);
  250. $zip = new ZipStream(
  251. outputStream: $this->tempfileStream,
  252. sendHttpHeaders: false,
  253. defaultEnableZeroHeader: false,
  254. );
  255. if (file_exists('/dev/null')) {
  256. $streamUnseekable = fopen('/dev/null', 'w+');
  257. } elseif (file_exists('NUL')) {
  258. $streamUnseekable = fopen('NUL', 'w+');
  259. } else {
  260. $this->markTestSkipped('Needs file /dev/null');
  261. }
  262. $zip->addFileFromStream('sample.txt', $streamUnseekable, maxSize: 2);
  263. }
  264. public function testAddFileFromStreamUnseekableInputWithZeroHeader(): void
  265. {
  266. $zip = new ZipStream(
  267. outputStream: $this->tempfileStream,
  268. sendHttpHeaders: false,
  269. defaultEnableZeroHeader: true,
  270. defaultCompressionMethod: CompressionMethod::STORE,
  271. );
  272. $streamUnseekable = StreamWrapper::getResource(new class ('test') extends EndlessCycleStream {
  273. public function isSeekable(): bool
  274. {
  275. return false;
  276. }
  277. public function seek(int $offset, int $whence = SEEK_SET): void
  278. {
  279. throw new RuntimeException('Not seekable');
  280. }
  281. });
  282. $zip->addFileFromStream('sample.txt', $streamUnseekable, maxSize: 7);
  283. $zip->finish();
  284. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  285. $files = $this->getRecursiveFileList($tmpDir);
  286. $this->assertSame(['sample.txt'], $files);
  287. $this->assertSame(filesize($tmpDir . '/sample.txt'), 7);
  288. }
  289. public function testAddFileFromStreamWithStorageMethod(): void
  290. {
  291. $zip = new ZipStream(
  292. outputStream: $this->tempfileStream,
  293. sendHttpHeaders: false,
  294. );
  295. $streamExample = fopen('php://temp', 'wb+');
  296. fwrite($streamExample, 'Sample String Data');
  297. rewind($streamExample); // move the pointer back to the beginning of file.
  298. $zip->addFileFromStream('sample.txt', $streamExample, compressionMethod: CompressionMethod::STORE);
  299. fclose($streamExample);
  300. $streamExample2 = fopen('php://temp', 'bw+');
  301. fwrite($streamExample2, 'More Simple Sample Data');
  302. rewind($streamExample2); // move the pointer back to the beginning of file.
  303. $zip->addFileFromStream('test/sample.txt', $streamExample2, compressionMethod: CompressionMethod::DEFLATE);
  304. fclose($streamExample2);
  305. $zip->finish();
  306. $zipArchive = new ZipArchive();
  307. $zipArchive->open($this->tempfile);
  308. $sample1 = $zipArchive->statName('sample.txt');
  309. $this->assertSame(CompressionMethod::STORE->value, $sample1['comp_method']);
  310. $sample2 = $zipArchive->statName('test/sample.txt');
  311. $this->assertSame(CompressionMethod::DEFLATE->value, $sample2['comp_method']);
  312. $zipArchive->close();
  313. }
  314. public function testAddFileFromPsr7Stream(): void
  315. {
  316. $zip = new ZipStream(
  317. outputStream: $this->tempfileStream,
  318. sendHttpHeaders: false,
  319. );
  320. $body = 'Sample String Data';
  321. $response = new Response(200, [], $body);
  322. $zip->addFileFromPsr7Stream('sample.json', $response->getBody());
  323. $zip->finish();
  324. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  325. $files = $this->getRecursiveFileList($tmpDir);
  326. $this->assertSame(['sample.json'], $files);
  327. $this->assertStringEqualsFile($tmpDir . '/sample.json', $body);
  328. }
  329. /**
  330. * @group slow
  331. */
  332. public function testAddLargeFileFromPsr7Stream(): void
  333. {
  334. $zip = new ZipStream(
  335. outputStream: $this->tempfileStream,
  336. sendHttpHeaders: false,
  337. enableZip64: true,
  338. );
  339. $zip->addFileFromPsr7Stream(
  340. fileName: 'sample.json',
  341. stream: new EndlessCycleStream('0'),
  342. maxSize: 0x100000000,
  343. compressionMethod: CompressionMethod::STORE,
  344. lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'),
  345. );
  346. $zip->finish();
  347. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  348. $files = $this->getRecursiveFileList($tmpDir);
  349. $this->assertSame(['sample.json'], $files);
  350. $this->assertFileIsReadable($tmpDir . '/sample.json');
  351. $this->assertStringStartsWith('000000', file_get_contents(filename: $tmpDir . '/sample.json', length: 20));
  352. }
  353. public function testContinueFinishedZip(): void
  354. {
  355. $this->expectException(RuntimeException::class);
  356. $zip = new ZipStream(
  357. outputStream: $this->tempfileStream,
  358. sendHttpHeaders: false,
  359. );
  360. $zip->finish();
  361. $zip->addFile('sample.txt', '1234');
  362. }
  363. /**
  364. * @group slow
  365. */
  366. public function testManyFilesWithoutZip64(): void
  367. {
  368. $this->expectException(OverflowException::class);
  369. $zip = new ZipStream(
  370. outputStream: $this->tempfileStream,
  371. sendHttpHeaders: false,
  372. enableZip64: false,
  373. );
  374. for ($i = 0; $i <= 0xFFFF; $i++) {
  375. $zip->addFile('sample' . $i, '');
  376. }
  377. $zip->finish();
  378. }
  379. /**
  380. * @group slow
  381. */
  382. public function testManyFilesWithZip64(): void
  383. {
  384. $zip = new ZipStream(
  385. outputStream: $this->tempfileStream,
  386. sendHttpHeaders: false,
  387. enableZip64: true,
  388. );
  389. for ($i = 0; $i <= 0xFFFF; $i++) {
  390. $zip->addFile('sample' . $i, '');
  391. }
  392. $zip->finish();
  393. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  394. $files = $this->getRecursiveFileList($tmpDir);
  395. $this->assertSame(count($files), 0x10000);
  396. }
  397. /**
  398. * @group slow
  399. */
  400. public function testLongZipWithout64(): void
  401. {
  402. $this->expectException(OverflowException::class);
  403. $zip = new ZipStream(
  404. outputStream: $this->tempfileStream,
  405. sendHttpHeaders: false,
  406. enableZip64: false,
  407. defaultCompressionMethod: CompressionMethod::STORE,
  408. );
  409. for ($i = 0; $i < 4; $i++) {
  410. $zip->addFileFromPsr7Stream(
  411. fileName: 'sample' . $i,
  412. stream: new EndlessCycleStream('0'),
  413. maxSize: 0xFFFFFFFF,
  414. compressionMethod: CompressionMethod::STORE,
  415. lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'),
  416. );
  417. }
  418. }
  419. /**
  420. * @group slow
  421. */
  422. public function testLongZipWith64(): void
  423. {
  424. $zip = new ZipStream(
  425. outputStream: $this->tempfileStream,
  426. sendHttpHeaders: false,
  427. enableZip64: true,
  428. defaultCompressionMethod: CompressionMethod::STORE,
  429. );
  430. for ($i = 0; $i < 4; $i++) {
  431. $zip->addFileFromPsr7Stream(
  432. fileName: 'sample' . $i,
  433. stream: new EndlessCycleStream('0'),
  434. maxSize: 0x5FFFFFFF,
  435. compressionMethod: CompressionMethod::STORE,
  436. lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'),
  437. );
  438. }
  439. $zip->finish();
  440. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  441. $files = $this->getRecursiveFileList($tmpDir);
  442. $this->assertSame(['sample0', 'sample1', 'sample2', 'sample3'], $files);
  443. }
  444. /**
  445. * @group slow
  446. */
  447. public function testAddLargeFileWithoutZip64WithZeroHeader(): void
  448. {
  449. $this->expectException(OverflowException::class);
  450. $zip = new ZipStream(
  451. outputStream: $this->tempfileStream,
  452. sendHttpHeaders: false,
  453. enableZip64: false,
  454. defaultEnableZeroHeader: true,
  455. );
  456. $zip->addFileFromPsr7Stream(
  457. fileName: 'sample.json',
  458. stream: new EndlessCycleStream('0'),
  459. maxSize: 0x100000000,
  460. compressionMethod: CompressionMethod::STORE,
  461. lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'),
  462. );
  463. }
  464. /**
  465. * @group slow
  466. */
  467. public function testAddsZip64HeaderWhenNeeded(): void
  468. {
  469. $zip = new ZipStream(
  470. outputStream: $this->tempfileStream,
  471. sendHttpHeaders: false,
  472. enableZip64: true,
  473. defaultEnableZeroHeader: false,
  474. );
  475. $zip->addFileFromPsr7Stream(
  476. fileName: 'sample.json',
  477. stream: new EndlessCycleStream('0'),
  478. maxSize: 0x100000000,
  479. compressionMethod: CompressionMethod::STORE,
  480. lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'),
  481. );
  482. $zip->finish();
  483. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  484. $files = $this->getRecursiveFileList($tmpDir);
  485. $this->assertSame(['sample.json'], $files);
  486. $this->assertFileContains($this->tempfile, PackField::pack(
  487. new PackField(format: 'V', value: 0x06064b50)
  488. ));
  489. }
  490. /**
  491. * @group slow
  492. */
  493. public function testDoesNotAddZip64HeaderWhenNotNeeded(): void
  494. {
  495. $zip = new ZipStream(
  496. outputStream: $this->tempfileStream,
  497. sendHttpHeaders: false,
  498. enableZip64: true,
  499. defaultEnableZeroHeader: false,
  500. );
  501. $zip->addFileFromPsr7Stream(
  502. fileName: 'sample.json',
  503. stream: new EndlessCycleStream('0'),
  504. maxSize: 0x10,
  505. compressionMethod: CompressionMethod::STORE,
  506. lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'),
  507. );
  508. $zip->finish();
  509. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  510. $files = $this->getRecursiveFileList($tmpDir);
  511. $this->assertSame(['sample.json'], $files);
  512. $this->assertFileDoesNotContain($this->tempfile, PackField::pack(
  513. new PackField(format: 'V', value: 0x06064b50)
  514. ));
  515. }
  516. /**
  517. * @group slow
  518. */
  519. public function testAddLargeFileWithoutZip64WithoutZeroHeader(): void
  520. {
  521. $this->expectException(OverflowException::class);
  522. $zip = new ZipStream(
  523. outputStream: $this->tempfileStream,
  524. sendHttpHeaders: false,
  525. enableZip64: false,
  526. defaultEnableZeroHeader: false,
  527. );
  528. $zip->addFileFromPsr7Stream(
  529. fileName: 'sample.json',
  530. stream: new EndlessCycleStream('0'),
  531. maxSize: 0x100000000,
  532. compressionMethod: CompressionMethod::STORE,
  533. lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'),
  534. );
  535. }
  536. public function testAddFileFromPsr7StreamWithOutputToPsr7Stream(): void
  537. {
  538. $psr7OutputStream = new ResourceStream($this->tempfileStream);
  539. $zip = new ZipStream(
  540. outputStream: $psr7OutputStream,
  541. sendHttpHeaders: false,
  542. );
  543. $body = 'Sample String Data';
  544. $response = new Response(200, [], $body);
  545. $zip->addFileFromPsr7Stream(
  546. fileName: 'sample.json',
  547. stream: $response->getBody(),
  548. compressionMethod: CompressionMethod::STORE,
  549. );
  550. $zip->finish();
  551. $psr7OutputStream->close();
  552. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  553. $files = $this->getRecursiveFileList($tmpDir);
  554. $this->assertSame(['sample.json'], $files);
  555. $this->assertStringEqualsFile($tmpDir . '/sample.json', $body);
  556. }
  557. public function testAddFileFromPsr7StreamWithFileSizeSet(): void
  558. {
  559. $zip = new ZipStream(
  560. outputStream: $this->tempfileStream,
  561. sendHttpHeaders: false,
  562. );
  563. $body = 'Sample String Data';
  564. $fileSize = strlen($body);
  565. // Add fake padding
  566. $fakePadding = "\0\0\0\0\0\0";
  567. $response = new Response(200, [], $body . $fakePadding);
  568. $zip->addFileFromPsr7Stream(
  569. fileName: 'sample.json',
  570. stream: $response->getBody(),
  571. compressionMethod: CompressionMethod::STORE,
  572. maxSize: $fileSize
  573. );
  574. $zip->finish();
  575. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  576. $files = $this->getRecursiveFileList($tmpDir);
  577. $this->assertSame(['sample.json'], $files);
  578. $this->assertStringEqualsFile($tmpDir . '/sample.json', $body);
  579. }
  580. public function testCreateArchiveHeaders(): void
  581. {
  582. $headers = [];
  583. $httpHeaderCallback = function (string $header) use (&$headers) {
  584. $headers[] = $header;
  585. };
  586. $zip = new ZipStream(
  587. outputStream: $this->tempfileStream,
  588. sendHttpHeaders: true,
  589. outputName: 'example.zip',
  590. httpHeaderCallback: $httpHeaderCallback,
  591. );
  592. $zip->addFile(
  593. fileName: 'sample.json',
  594. data: 'foo',
  595. );
  596. $zip->finish();
  597. $this->assertContains('Content-Type: application/x-zip', $headers);
  598. $this->assertContains("Content-Disposition: attachment; filename*=UTF-8''example.zip", $headers);
  599. $this->assertContains('Pragma: public', $headers);
  600. $this->assertContains('Cache-Control: public, must-revalidate', $headers);
  601. $this->assertContains('Content-Transfer-Encoding: binary', $headers);
  602. }
  603. public function testCreateArchiveWithFlushOptionSet(): void
  604. {
  605. $zip = new ZipStream(
  606. outputStream: $this->tempfileStream,
  607. flushOutput: true,
  608. sendHttpHeaders: false,
  609. );
  610. $zip->addFile('sample.txt', 'Sample String Data');
  611. $zip->addFile('test/sample.txt', 'More Simple Sample Data');
  612. $zip->finish();
  613. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  614. $files = $this->getRecursiveFileList($tmpDir);
  615. $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files);
  616. $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data');
  617. $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data');
  618. }
  619. public function testCreateArchiveWithOutputBufferingOffAndFlushOptionSet(): void
  620. {
  621. // WORKAROUND (1/2): remove phpunit's output buffer in order to run test without any buffering
  622. ob_end_flush();
  623. $this->assertSame(0, ob_get_level());
  624. $zip = new ZipStream(
  625. outputStream: $this->tempfileStream,
  626. flushOutput: true,
  627. sendHttpHeaders: false,
  628. );
  629. $zip->addFile('sample.txt', 'Sample String Data');
  630. $zip->finish();
  631. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  632. $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data');
  633. // WORKAROUND (2/2): add back output buffering so that PHPUnit doesn't complain that it is missing
  634. ob_start();
  635. }
  636. public function testAddEmptyDirectory(): void
  637. {
  638. $zip = new ZipStream(
  639. outputStream: $this->tempfileStream,
  640. sendHttpHeaders: false,
  641. );
  642. $zip->addDirectory('foo');
  643. $zip->finish();
  644. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  645. $files = $this->getRecursiveFileList($tmpDir, includeDirectories: true);
  646. $this->assertContains('foo', $files);
  647. $this->assertFileExists($tmpDir . DIRECTORY_SEPARATOR . 'foo');
  648. $this->assertDirectoryExists($tmpDir . DIRECTORY_SEPARATOR . 'foo');
  649. }
  650. public function testAddFileSimulate(): void
  651. {
  652. $create = function (OperationMode $operationMode): int {
  653. $zip = new ZipStream(
  654. sendHttpHeaders: false,
  655. operationMode: $operationMode,
  656. defaultEnableZeroHeader: true,
  657. outputStream: $this->tempfileStream,
  658. );
  659. $zip->addFile('sample.txt', 'Sample String Data');
  660. $zip->addFile('test/sample.txt', 'More Simple Sample Data');
  661. return $zip->finish();
  662. };
  663. $sizeExpected = $create(OperationMode::NORMAL);
  664. $sizeActual = $create(OperationMode::SIMULATE_LAX);
  665. $this->assertEquals($sizeExpected, $sizeActual);
  666. }
  667. public function testAddFileSimulateWithMaxSize(): void
  668. {
  669. $create = function (OperationMode $operationMode): int {
  670. $zip = new ZipStream(
  671. sendHttpHeaders: false,
  672. operationMode: $operationMode,
  673. defaultCompressionMethod: CompressionMethod::STORE,
  674. defaultEnableZeroHeader: true,
  675. outputStream: $this->tempfileStream,
  676. );
  677. $zip->addFile('sample.txt', 'Sample String Data', maxSize: 0);
  678. return $zip->finish();
  679. };
  680. $sizeExpected = $create(OperationMode::NORMAL);
  681. $sizeActual = $create(OperationMode::SIMULATE_LAX);
  682. $this->assertEquals($sizeExpected, $sizeActual);
  683. }
  684. public function testAddFileSimulateWithFstat(): void
  685. {
  686. $create = function (OperationMode $operationMode): int {
  687. $zip = new ZipStream(
  688. sendHttpHeaders: false,
  689. operationMode: $operationMode,
  690. defaultCompressionMethod: CompressionMethod::STORE,
  691. defaultEnableZeroHeader: true,
  692. outputStream: $this->tempfileStream,
  693. );
  694. $zip->addFile('sample.txt', 'Sample String Data');
  695. $zip->addFile('test/sample.txt', 'More Simple Sample Data');
  696. return $zip->finish();
  697. };
  698. $sizeExpected = $create(OperationMode::NORMAL);
  699. $sizeActual = $create(OperationMode::SIMULATE_LAX);
  700. $this->assertEquals($sizeExpected, $sizeActual);
  701. }
  702. public function testAddFileSimulateWithExactSizeZero(): void
  703. {
  704. $create = function (OperationMode $operationMode): int {
  705. $zip = new ZipStream(
  706. sendHttpHeaders: false,
  707. operationMode: $operationMode,
  708. defaultCompressionMethod: CompressionMethod::STORE,
  709. defaultEnableZeroHeader: true,
  710. outputStream: $this->tempfileStream,
  711. );
  712. $zip->addFile('sample.txt', 'Sample String Data', exactSize: 18);
  713. return $zip->finish();
  714. };
  715. $sizeExpected = $create(OperationMode::NORMAL);
  716. $sizeActual = $create(OperationMode::SIMULATE_LAX);
  717. $this->assertEquals($sizeExpected, $sizeActual);
  718. }
  719. public function testAddFileSimulateWithExactSizeInitial(): void
  720. {
  721. $create = function (OperationMode $operationMode): int {
  722. $zip = new ZipStream(
  723. sendHttpHeaders: false,
  724. operationMode: $operationMode,
  725. defaultCompressionMethod: CompressionMethod::STORE,
  726. defaultEnableZeroHeader: false,
  727. outputStream: $this->tempfileStream,
  728. );
  729. $zip->addFile('sample.txt', 'Sample String Data', exactSize: 18);
  730. return $zip->finish();
  731. };
  732. $sizeExpected = $create(OperationMode::NORMAL);
  733. $sizeActual = $create(OperationMode::SIMULATE_LAX);
  734. $this->assertEquals($sizeExpected, $sizeActual);
  735. }
  736. public function testAddFileSimulateWithZeroSizeInFstat(): void
  737. {
  738. $create = function (OperationMode $operationMode): int {
  739. $zip = new ZipStream(
  740. sendHttpHeaders: false,
  741. operationMode: $operationMode,
  742. defaultCompressionMethod: CompressionMethod::STORE,
  743. defaultEnableZeroHeader: false,
  744. outputStream: $this->tempfileStream,
  745. );
  746. $zip->addFileFromPsr7Stream('sample.txt', new class implements StreamInterface {
  747. public $pos = 0;
  748. public function __toString(): string
  749. {
  750. return 'test';
  751. }
  752. public function close(): void {}
  753. public function detach() {}
  754. public function getSize(): ?int
  755. {
  756. return null;
  757. }
  758. public function tell(): int
  759. {
  760. return $this->pos;
  761. }
  762. public function eof(): bool
  763. {
  764. return $this->pos >= 4;
  765. }
  766. public function isSeekable(): bool
  767. {
  768. return true;
  769. }
  770. public function seek(int $offset, int $whence = SEEK_SET): void
  771. {
  772. $this->pos = $offset;
  773. }
  774. public function rewind(): void
  775. {
  776. $this->pos = 0;
  777. }
  778. public function isWritable(): bool
  779. {
  780. return false;
  781. }
  782. public function write(string $string): int
  783. {
  784. return 0;
  785. }
  786. public function isReadable(): bool
  787. {
  788. return true;
  789. }
  790. public function read(int $length): string
  791. {
  792. $data = substr('test', $this->pos, $length);
  793. $this->pos += strlen($data);
  794. return $data;
  795. }
  796. public function getContents(): string
  797. {
  798. return $this->read(4);
  799. }
  800. public function getMetadata(?string $key = null)
  801. {
  802. return $key !== null ? null : [];
  803. }
  804. });
  805. return $zip->finish();
  806. };
  807. $sizeExpected = $create(OperationMode::NORMAL);
  808. $sizeActual = $create(OperationMode::SIMULATE_LAX);
  809. $this->assertEquals($sizeExpected, $sizeActual);
  810. }
  811. public function testAddFileSimulateWithWrongExactSize(): void
  812. {
  813. $this->expectException(FileSizeIncorrectException::class);
  814. $zip = new ZipStream(
  815. sendHttpHeaders: false,
  816. operationMode: OperationMode::SIMULATE_LAX,
  817. );
  818. $zip->addFile('sample.txt', 'Sample String Data', exactSize: 1000);
  819. }
  820. public function testAddFileSimulateStrictZero(): void
  821. {
  822. $this->expectException(SimulationFileUnknownException::class);
  823. $zip = new ZipStream(
  824. sendHttpHeaders: false,
  825. operationMode: OperationMode::SIMULATE_STRICT,
  826. defaultEnableZeroHeader: true
  827. );
  828. $zip->addFile('sample.txt', 'Sample String Data');
  829. }
  830. public function testAddFileSimulateStrictInitial(): void
  831. {
  832. $this->expectException(SimulationFileUnknownException::class);
  833. $zip = new ZipStream(
  834. sendHttpHeaders: false,
  835. operationMode: OperationMode::SIMULATE_STRICT,
  836. defaultEnableZeroHeader: false
  837. );
  838. $zip->addFile('sample.txt', 'Sample String Data');
  839. }
  840. public function testAddFileCallbackStrict(): void
  841. {
  842. $this->expectException(SimulationFileUnknownException::class);
  843. $zip = new ZipStream(
  844. sendHttpHeaders: false,
  845. operationMode: OperationMode::SIMULATE_STRICT,
  846. defaultEnableZeroHeader: false
  847. );
  848. $zip->addFileFromCallback('sample.txt', callback: function () {
  849. return '';
  850. });
  851. }
  852. public function testAddFileCallbackLax(): void
  853. {
  854. $zip = new ZipStream(
  855. operationMode: OperationMode::SIMULATE_LAX,
  856. defaultEnableZeroHeader: false,
  857. sendHttpHeaders: false,
  858. );
  859. $zip->addFileFromCallback('sample.txt', callback: function () {
  860. return 'Sample String Data';
  861. });
  862. $size = $zip->finish();
  863. $this->assertEquals($size, 142);
  864. }
  865. public function testExecuteSimulation(): void
  866. {
  867. $zip = new ZipStream(
  868. operationMode: OperationMode::SIMULATE_STRICT,
  869. defaultCompressionMethod: CompressionMethod::STORE,
  870. defaultEnableZeroHeader: false,
  871. sendHttpHeaders: false,
  872. outputStream: $this->tempfileStream,
  873. );
  874. $zip->addFileFromCallback(
  875. 'sample.txt',
  876. exactSize: 18,
  877. callback: function () {
  878. return 'Sample String Data';
  879. }
  880. );
  881. $zip->addFileFromCallback(
  882. '.gitkeep',
  883. exactSize: 0,
  884. callback: function () {
  885. return '';
  886. }
  887. );
  888. $size = $zip->finish();
  889. $this->assertEquals(filesize($this->tempfile), 0);
  890. $zip->executeSimulation();
  891. clearstatcache();
  892. $this->assertEquals(filesize($this->tempfile), $size);
  893. $tmpDir = $this->validateAndExtractZip($this->tempfile);
  894. $files = $this->getRecursiveFileList($tmpDir);
  895. $this->assertSame(['.gitkeep', 'sample.txt'], $files);
  896. }
  897. public function testExecuteSimulationBeforeFinish(): void
  898. {
  899. $this->expectException(RuntimeException::class);
  900. $zip = new ZipStream(
  901. operationMode: OperationMode::SIMULATE_LAX,
  902. defaultEnableZeroHeader: false,
  903. sendHttpHeaders: false,
  904. outputStream: $this->tempfileStream,
  905. );
  906. $zip->executeSimulation();
  907. }
  908. private function addLargeFileFileFromPath(CompressionMethod $compressionMethod, $zeroHeader, $zip64): void
  909. {
  910. [$tmp, $stream] = $this->getTmpFileStream();
  911. $zip = new ZipStream(
  912. outputStream: $stream,
  913. sendHttpHeaders: false,
  914. defaultEnableZeroHeader: $zeroHeader,
  915. enableZip64: $zip64,
  916. );
  917. [$tmpExample, $streamExample] = $this->getTmpFileStream();
  918. for ($i = 0; $i <= 10000; $i++) {
  919. fwrite($streamExample, sha1((string) $i));
  920. if ($i % 100 === 0) {
  921. fwrite($streamExample, "\n");
  922. }
  923. }
  924. fclose($streamExample);
  925. $shaExample = sha1_file($tmpExample);
  926. $zip->addFileFromPath('sample.txt', $tmpExample);
  927. unlink($tmpExample);
  928. $zip->finish();
  929. fclose($stream);
  930. $tmpDir = $this->validateAndExtractZip($tmp);
  931. $files = $this->getRecursiveFileList($tmpDir);
  932. $this->assertSame(['sample.txt'], $files);
  933. $this->assertSame(sha1_file($tmpDir . '/sample.txt'), $shaExample, "SHA-1 Mismatch Method: {$compressionMethod->value}");
  934. unlink($tmp);
  935. }
  936. }