ZipStreamTest.php 38 KB

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