123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608 |
- <?php
- declare(strict_types=1);
- namespace ZipStream;
- use Psr\Http\Message\StreamInterface;
- use ZipStream\Exception\OverflowException;
- use ZipStream\Option\Archive as ArchiveOptions;
- use ZipStream\Option\File as FileOptions;
- use ZipStream\Option\Version;
- /**
- * ZipStream
- *
- * Streamed, dynamically generated zip archives.
- *
- * Usage:
- *
- * Streaming zip archives is a simple, three-step process:
- *
- * 1. Create the zip stream:
- *
- * $zip = new ZipStream('example.zip');
- *
- * 2. Add one or more files to the archive:
- *
- * * add first file
- * $data = file_get_contents('some_file.gif');
- * $zip->addFile('some_file.gif', $data);
- *
- * * add second file
- * $data = file_get_contents('some_file.gif');
- * $zip->addFile('another_file.png', $data);
- *
- * 3. Finish the zip stream:
- *
- * $zip->finish();
- *
- * You can also add an archive comment, add comments to individual files,
- * and adjust the timestamp of files. See the API documentation for each
- * method below for additional information.
- *
- * Example:
- *
- * // create a new zip stream object
- * $zip = new ZipStream('some_files.zip');
- *
- * // list of local files
- * $files = array('foo.txt', 'bar.jpg');
- *
- * // read and add each file to the archive
- * foreach ($files as $path)
- * $zip->addFile($path, file_get_contents($path));
- *
- * // write archive footer to stream
- * $zip->finish();
- */
- class ZipStream
- {
- /**
- * This number corresponds to the ZIP version/OS used (2 bytes)
- * From: https://www.iana.org/assignments/media-types/application/zip
- * The upper byte (leftmost one) indicates the host system (OS) for the
- * file. Software can use this information to determine
- * the line record format for text files etc. The current
- * mappings are:
- *
- * 0 - MS-DOS and OS/2 (F.A.T. file systems)
- * 1 - Amiga 2 - VAX/VMS
- * 3 - *nix 4 - VM/CMS
- * 5 - Atari ST 6 - OS/2 H.P.F.S.
- * 7 - Macintosh 8 - Z-System
- * 9 - CP/M 10 thru 255 - unused
- *
- * The lower byte (rightmost one) indicates the version number of the
- * software used to encode the file. The value/10
- * indicates the major version number, and the value
- * mod 10 is the minor version number.
- * Here we are using 6 for the OS, indicating OS/2 H.P.F.S.
- * to prevent file permissions issues upon extract (see #84)
- * 0x603 is 00000110 00000011 in binary, so 6 and 3
- */
- public const ZIP_VERSION_MADE_BY = 0x603;
- /**
- * The following signatures end with 0x4b50, which in ASCII is PK,
- * the initials of the inventor Phil Katz.
- * See https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers
- */
- public const FILE_HEADER_SIGNATURE = 0x04034b50;
- public const CDR_FILE_SIGNATURE = 0x02014b50;
- public const CDR_EOF_SIGNATURE = 0x06054b50;
- public const DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
- public const ZIP64_CDR_EOF_SIGNATURE = 0x06064b50;
- public const ZIP64_CDR_LOCATOR_SIGNATURE = 0x07064b50;
- /**
- * Global Options
- *
- * @var ArchiveOptions
- */
- public $opt;
- /**
- * @var array
- */
- public $files = [];
- /**
- * @var Bigint
- */
- public $cdr_ofs;
- /**
- * @var Bigint
- */
- public $ofs;
- /**
- * @var bool
- */
- protected $need_headers;
- /**
- * @var null|String
- */
- protected $output_name;
- /**
- * Create a new ZipStream object.
- *
- * Parameters:
- *
- * @param String $name - Name of output file (optional).
- * @param ArchiveOptions $opt - Archive Options
- *
- * Large File Support:
- *
- * By default, the method addFileFromPath() will send send files
- * larger than 20 megabytes along raw rather than attempting to
- * compress them. You can change both the maximum size and the
- * compression behavior using the largeFile* options above, with the
- * following caveats:
- *
- * * For "small" files (e.g. files smaller than largeFileSize), the
- * memory use can be up to twice that of the actual file. In other
- * words, adding a 10 megabyte file to the archive could potentially
- * occupy 20 megabytes of memory.
- *
- * * Enabling compression on large files (e.g. files larger than
- * large_file_size) is extremely slow, because ZipStream has to pass
- * over the large file once to calculate header information, and then
- * again to compress and send the actual data.
- *
- * Examples:
- *
- * // create a new zip file named 'foo.zip'
- * $zip = new ZipStream('foo.zip');
- *
- * // create a new zip file named 'bar.zip' with a comment
- * $opt->setComment = 'this is a comment for the zip file.';
- * $zip = new ZipStream('bar.zip', $opt);
- *
- * Notes:
- *
- * In order to let this library send HTTP headers, a filename must be given
- * _and_ the option `sendHttpHeaders` must be `true`. This behavior is to
- * allow software to send its own headers (including the filename), and
- * still use this library.
- */
- public function __construct(?string $name = null, ?ArchiveOptions $opt = null)
- {
- $this->opt = $opt ?: new ArchiveOptions();
- $this->output_name = $name;
- $this->need_headers = $name && $this->opt->isSendHttpHeaders();
- $this->cdr_ofs = new Bigint();
- $this->ofs = new Bigint();
- }
- /**
- * addFile
- *
- * Add a file to the archive.
- *
- * @param String $name - path of file in archive (including directory).
- * @param String $data - contents of file
- * @param FileOptions $options
- *
- * File Options:
- * time - Last-modified timestamp (seconds since the epoch) of
- * this file. Defaults to the current time.
- * comment - Comment related to this file.
- * method - Storage method for file ("store" or "deflate")
- *
- * Examples:
- *
- * // add a file named 'foo.txt'
- * $data = file_get_contents('foo.txt');
- * $zip->addFile('foo.txt', $data);
- *
- * // add a file named 'bar.jpg' with a comment and a last-modified
- * // time of two hours ago
- * $data = file_get_contents('bar.jpg');
- * $opt->setTime = time() - 2 * 3600;
- * $opt->setComment = 'this is a comment about bar.jpg';
- * $zip->addFile('bar.jpg', $data, $opt);
- */
- public function addFile(string $name, string $data, ?FileOptions $options = null): void
- {
- $options = $options ?: new FileOptions();
- $options->defaultTo($this->opt);
- $file = new File($this, $name, $options);
- $file->processData($data);
- }
- /**
- * addFileFromPath
- *
- * Add a file at path to the archive.
- *
- * Note that large files may be compressed differently than smaller
- * files; see the "Large File Support" section above for more
- * information.
- *
- * @param String $name - name of file in archive (including directory path).
- * @param String $path - path to file on disk (note: paths should be encoded using
- * UNIX-style forward slashes -- e.g '/path/to/some/file').
- * @param FileOptions $options
- *
- * File Options:
- * time - Last-modified timestamp (seconds since the epoch) of
- * this file. Defaults to the current time.
- * comment - Comment related to this file.
- * method - Storage method for file ("store" or "deflate")
- *
- * Examples:
- *
- * // add a file named 'foo.txt' from the local file '/tmp/foo.txt'
- * $zip->addFileFromPath('foo.txt', '/tmp/foo.txt');
- *
- * // add a file named 'bigfile.rar' from the local file
- * // '/usr/share/bigfile.rar' with a comment and a last-modified
- * // time of two hours ago
- * $path = '/usr/share/bigfile.rar';
- * $opt->setTime = time() - 2 * 3600;
- * $opt->setComment = 'this is a comment about bar.jpg';
- * $zip->addFileFromPath('bigfile.rar', $path, $opt);
- *
- * @return void
- * @throws \ZipStream\Exception\FileNotFoundException
- * @throws \ZipStream\Exception\FileNotReadableException
- */
- public function addFileFromPath(string $name, string $path, ?FileOptions $options = null): void
- {
- $options = $options ?: new FileOptions();
- $options->defaultTo($this->opt);
- $file = new File($this, $name, $options);
- $file->processPath($path);
- }
- /**
- * addFileFromStream
- *
- * Add an open stream to the archive.
- *
- * @param String $name - path of file in archive (including directory).
- * @param resource $stream - contents of file as a stream resource
- * @param FileOptions $options
- *
- * File Options:
- * time - Last-modified timestamp (seconds since the epoch) of
- * this file. Defaults to the current time.
- * comment - Comment related to this file.
- *
- * Examples:
- *
- * // create a temporary file stream and write text to it
- * $fp = tmpfile();
- * fwrite($fp, 'The quick brown fox jumped over the lazy dog.');
- *
- * // add a file named 'streamfile.txt' from the content of the stream
- * $x->addFileFromStream('streamfile.txt', $fp);
- *
- * @return void
- */
- public function addFileFromStream(string $name, $stream, ?FileOptions $options = null): void
- {
- $options = $options ?: new FileOptions();
- $options->defaultTo($this->opt);
- $file = new File($this, $name, $options);
- $file->processStream(new DeflateStream($stream));
- }
- /**
- * addFileFromPsr7Stream
- *
- * Add an open stream to the archive.
- *
- * @param String $name - path of file in archive (including directory).
- * @param StreamInterface $stream - contents of file as a stream resource
- * @param FileOptions $options
- *
- * File Options:
- * time - Last-modified timestamp (seconds since the epoch) of
- * this file. Defaults to the current time.
- * comment - Comment related to this file.
- *
- * Examples:
- *
- * $stream = $response->getBody();
- * // add a file named 'streamfile.txt' from the content of the stream
- * $x->addFileFromPsr7Stream('streamfile.txt', $stream);
- *
- * @return void
- */
- public function addFileFromPsr7Stream(
- string $name,
- StreamInterface $stream,
- ?FileOptions $options = null
- ): void {
- $options = $options ?: new FileOptions();
- $options->defaultTo($this->opt);
- $file = new File($this, $name, $options);
- $file->processStream($stream);
- }
- /**
- * finish
- *
- * Write zip footer to stream.
- *
- * Example:
- *
- * // add a list of files to the archive
- * $files = array('foo.txt', 'bar.jpg');
- * foreach ($files as $path)
- * $zip->addFile($path, file_get_contents($path));
- *
- * // write footer to stream
- * $zip->finish();
- * @return void
- *
- * @throws OverflowException
- */
- public function finish(): void
- {
- // add trailing cdr file records
- foreach ($this->files as $cdrFile) {
- $this->send($cdrFile);
- $this->cdr_ofs = $this->cdr_ofs->add(Bigint::init(strlen($cdrFile)));
- }
- // Add 64bit headers (if applicable)
- if (count($this->files) >= 0xFFFF ||
- $this->cdr_ofs->isOver32() ||
- $this->ofs->isOver32()) {
- if (!$this->opt->isEnableZip64()) {
- throw new OverflowException();
- }
- $this->addCdr64Eof();
- $this->addCdr64Locator();
- }
- // add trailing cdr eof record
- $this->addCdrEof();
- // The End
- $this->clear();
- }
- /**
- * Create a format string and argument list for pack(), then call
- * pack() and return the result.
- *
- * @param array $fields
- * @return string
- */
- public static function packFields(array $fields): string
- {
- $fmt = '';
- $args = [];
- // populate format string and argument list
- foreach ($fields as [$format, $value]) {
- if ($format === 'P') {
- $fmt .= 'VV';
- if ($value instanceof Bigint) {
- $args[] = $value->getLow32();
- $args[] = $value->getHigh32();
- } else {
- $args[] = $value;
- $args[] = 0;
- }
- } else {
- if ($value instanceof Bigint) {
- $value = $value->getLow32();
- }
- $fmt .= $format;
- $args[] = $value;
- }
- }
- // prepend format string to argument list
- array_unshift($args, $fmt);
- // build output string from header and compressed data
- return pack(...$args);
- }
- /**
- * Send string, sending HTTP headers if necessary.
- * Flush output after write if configure option is set.
- *
- * @param String $str
- * @return void
- */
- public function send(string $str): void
- {
- if ($this->need_headers) {
- $this->sendHttpHeaders();
- }
- $this->need_headers = false;
- $outputStream = $this->opt->getOutputStream();
- if ($outputStream instanceof StreamInterface) {
- $outputStream->write($str);
- } else {
- fwrite($outputStream, $str);
- }
- if ($this->opt->isFlushOutput()) {
- // flush output buffer if it is on and flushable
- $status = ob_get_status();
- if (isset($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) {
- ob_flush();
- }
- // Flush system buffers after flushing userspace output buffer
- flush();
- }
- }
- /**
- * Is this file larger than large_file_size?
- *
- * @param string $path
- * @return bool
- */
- public function isLargeFile(string $path): bool
- {
- if (!$this->opt->isStatFiles()) {
- return false;
- }
- $stat = stat($path);
- return $stat['size'] > $this->opt->getLargeFileSize();
- }
- /**
- * Save file attributes for trailing CDR record.
- *
- * @param File $file
- * @return void
- */
- public function addToCdr(File $file): void
- {
- $file->ofs = $this->ofs;
- $this->ofs = $this->ofs->add($file->getTotalLength());
- $this->files[] = $file->getCdrFile();
- }
- /**
- * Send ZIP64 CDR EOF (Central Directory Record End-of-File) record.
- *
- * @return void
- */
- protected function addCdr64Eof(): void
- {
- $num_files = count($this->files);
- $cdr_length = $this->cdr_ofs;
- $cdr_offset = $this->ofs;
- $fields = [
- ['V', static::ZIP64_CDR_EOF_SIGNATURE], // ZIP64 end of central file header signature
- ['P', 44], // Length of data below this header (length of block - 12) = 44
- ['v', static::ZIP_VERSION_MADE_BY], // Made by version
- ['v', Version::ZIP64], // Extract by version
- ['V', 0x00], // disk number
- ['V', 0x00], // no of disks
- ['P', $num_files], // no of entries on disk
- ['P', $num_files], // no of entries in cdr
- ['P', $cdr_length], // CDR size
- ['P', $cdr_offset], // CDR offset
- ];
- $ret = static::packFields($fields);
- $this->send($ret);
- }
- /**
- * Send HTTP headers for this stream.
- *
- * @return void
- */
- protected function sendHttpHeaders(): void
- {
- // grab content disposition
- $disposition = $this->opt->getContentDisposition();
- if ($this->output_name) {
- // Various different browsers dislike various characters here. Strip them all for safety.
- $safe_output = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->output_name));
- // Check if we need to UTF-8 encode the filename
- $urlencoded = rawurlencode($safe_output);
- $disposition .= "; filename*=UTF-8''{$urlencoded}";
- }
- $headers = [
- 'Content-Type' => $this->opt->getContentType(),
- 'Content-Disposition' => $disposition,
- 'Pragma' => 'public',
- 'Cache-Control' => 'public, must-revalidate',
- 'Content-Transfer-Encoding' => 'binary',
- ];
- $call = $this->opt->getHttpHeaderCallback();
- foreach ($headers as $key => $val) {
- $call("$key: $val");
- }
- }
- /**
- * Send ZIP64 CDR Locator (Central Directory Record Locator) record.
- *
- * @return void
- */
- protected function addCdr64Locator(): void
- {
- $cdr_offset = $this->ofs->add($this->cdr_ofs);
- $fields = [
- ['V', static::ZIP64_CDR_LOCATOR_SIGNATURE], // ZIP64 end of central file header signature
- ['V', 0x00], // Disc number containing CDR64EOF
- ['P', $cdr_offset], // CDR offset
- ['V', 1], // Total number of disks
- ];
- $ret = static::packFields($fields);
- $this->send($ret);
- }
- /**
- * Send CDR EOF (Central Directory Record End-of-File) record.
- *
- * @return void
- */
- protected function addCdrEof(): void
- {
- $num_files = count($this->files);
- $cdr_length = $this->cdr_ofs;
- $cdr_offset = $this->ofs;
- // grab comment (if specified)
- $comment = $this->opt->getComment();
- $fields = [
- ['V', static::CDR_EOF_SIGNATURE], // end of central file header signature
- ['v', 0x00], // disk number
- ['v', 0x00], // no of disks
- ['v', min($num_files, 0xFFFF)], // no of entries on disk
- ['v', min($num_files, 0xFFFF)], // no of entries in cdr
- ['V', $cdr_length->getLowFF()], // CDR size
- ['V', $cdr_offset->getLowFF()], // CDR offset
- ['v', strlen($comment)], // Zip Comment size
- ];
- $ret = static::packFields($fields) . $comment;
- $this->send($ret);
- }
- /**
- * Clear all internal variables. Note that the stream object is not
- * usable after this.
- *
- * @return void
- */
- protected function clear(): void
- {
- $this->files = [];
- $this->ofs = new Bigint();
- $this->cdr_ofs = new Bigint();
- $this->opt = new ArchiveOptions();
- }
- }
|