aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ZipStream.php641
1 files changed, 610 insertions, 31 deletions
diff --git a/src/ZipStream.php b/src/ZipStream.php
index 33218b6..0f3a452 100644
--- a/src/ZipStream.php
+++ b/src/ZipStream.php
@@ -1,17 +1,53 @@
<?php
declare(strict_types = 1);
+/**
+ * Streamed, dynamically generated zip archives.
+ *
+ * @author Paul Duncan <pabs@pablotron.org>
+ * @copyright 2007-2018 Paul Duncan <pabs@pablotron.org>
+ * @license MIT
+ * @package Pablotron\ZipStream
+ */
namespace Pablotron\ZipStream;
+/**
+ * Current version of ZipStream.
+ */
const VERSION = '0.3.0';
+/**
+ * Version needed to extract.
+ * @internal
+ */
+const VERSION_NEEDED = 45;
+
+/**
+ * Valid compression methods.
+ */
final class Methods {
+ /** Store file without compression. */
const STORE = 0;
+
+ /** Store file using DEFLATE compression. */
const DEFLATE = 8;
};
+/**
+ * Base class for all exceptions raised by ZipStream.
+ */
class Error extends \Exception { };
+
+/**
+ * Deflate context error.
+ */
+class DeflateError extends Error { };
+
+/**
+ * File related error.
+ */
final class FileError extends Error {
+ /** @var string Name of fail in which error occurred. */
public $file_name;
public function __construct(string $file_name, string $message) {
@@ -20,7 +56,11 @@ final class FileError extends Error {
}
};
+/**
+ * Path validation error.
+ */
final class PathError extends Error {
+ /** @var string string Invalid file name. */
public $file_name;
public function __construct(string $file_name, string $message) {
@@ -29,35 +69,126 @@ final class PathError extends Error {
}
};
+/**
+ * Abstract interface for stream output.
+ *
+ * @api
+ *
+ * @see FileWriter, HTTPResponseWriter
+ */
interface Writer {
+ /**
+ * Set metadata for generated archive.
+ *
+ * @param string $key Metadata key (one of "name" or "type").
+ * @param string $val Metadata value.
+ *
+ * @return void
+ */
public function set(string $key, string $val) : void;
- public function open() : void ;
+
+ /**
+ * Flush metadata and begin streaming archive contents.
+ *
+ * @return void
+ */
+ public function open() : void;
+
+ /**
+ * Write archive contents.
+ *
+ * @param string $data Archive file data.
+ *
+ * @return void
+ */
public function write(string $data) : void;
+
+ /**
+ * Finish writing archive data.
+ *
+ * @return void
+ */
public function close() : void;
};
+/**
+ * Stream generated archive as an HTTP response.
+ *
+ * @api
+ *
+ * Streams generated zip archive as an HTTP response. This is the
+ * default writer used by ZipStream if none is provided.
+ *
+ * @see Writer
+ */
final class HTTPResponseWriter implements Writer {
+ /**
+ * @var array Hash of metadata.
+ * @internal
+ */
private $args = [];
+ /**
+ * Set metadata for generated archive.
+ *
+ * @param string $key Metadata key (one of "name" or "type").
+ * @param string $val Metadata value.
+ *
+ * @return void
+ */
public function set(string $key, string $val) : void {
$this->args[$key] = $val;
}
+ /**
+ * Flush metadata and begin streaming archive contents.
+ *
+ * @todo
+ * @return void
+ */
public function open() : void {
# TODO: send http headers
}
+ /**
+ * Write archive contents.
+ *
+ * @param string $data Archive file data.
+ *
+ * @return void
+ */
public function write(string $data) : void {
echo $data;
}
+ /**
+ * Finish writing archive data.
+ *
+ * @return void
+ */
public function close() : void {
# ignore
}
};
+/**
+ * @api
+ *
+ * Stream generated archive to a local file.
+ *
+ * Streams generated zip archive to a local file. This is the
+ * default writer used by ZipStream if none is provided.
+ *
+ * @see Writer
+ */
final class FileWriter implements Writer {
+ /** @var string Output file path. */
public $path;
+
+ /**
+ * @var resource Output file handle.
+ * @internal
+ */
private $fh;
const FILE_WRITER_STATE_INIT = 0;
@@ -65,15 +196,35 @@ final class FileWriter implements Writer {
const FILE_WRITER_STATE_CLOSED = 2;
const FILE_WRITER_STATE_ERROR = 3;
- public function __construct($path) {
+ /**
+ * @api
+ *
+ * Create a new FileWriter.
+ *
+ * @param string $path Output file path.
+ */
+ public function __construct(string $path) {
$this->state = self::FILE_WRITER_STATE_INIT;
$this->path = $path;
}
+ /**
+ * Set metadata for generated archive.
+ *
+ * @param string $key Metadata key (one of "name" or "type").
+ * @param string $val Metadata value.
+ *
+ * @return void
+ */
public function set(string $key, string $val) : void {
# ignore metadata
}
+ /**
+ * Flush metadata and begin streaming archive contents.
+ *
+ * @return void
+ */
public function open() : void {
# open output file
$this->fh = @fopen($this->path, 'wb');
@@ -85,6 +236,13 @@ final class FileWriter implements Writer {
$this->state = self::FILE_WRITER_STATE_OPEN;
}
+ /**
+ * Write archive contents.
+ *
+ * @param string $data Archive file data.
+ *
+ * @return void
+ */
public function write(string $data) : void {
# check state
if ($this->state != self::FILE_WRITER_STATE_OPEN) {
@@ -101,6 +259,11 @@ final class FileWriter implements Writer {
}
}
+ /**
+ * Finish writing archive data.
+ *
+ * @return void
+ */
public function close() : void {
# check state
if ($this->state == self::FILE_WRITER_STATE_CLOSED) {
@@ -117,10 +280,16 @@ final class FileWriter implements Writer {
}
};
-#
-# Convert a UNIX timestamp to a DOS time/date.
-#
+/**
+ * Convert a UNIX timestamp into DOS date and time components.
+ * @internal
+ */
final class DateTime {
+ /**
+ * @var int $time Input UNIX timestamp.
+ * @var int $dos_time Output DOS timestamp.
+ * @var int $dos_date Output DOS datestamp.
+ */
public $time,
$dos_time,
$dos_date;
@@ -134,8 +303,22 @@ final class DateTime {
'seconds' => 0,
];
- public function __construct($time) {
+ /**
+ * Convert a UNIX timestamp into DOS date and time components.
+ *
+ * @param int $time Input UNIX timestamp.
+ *
+ * @example
+ * # create DateTime with current timestamp
+ * $dt = new DateTime(time());
+ *
+ * # print DOS date and time
+ * echo "DOS time: {$dt->dos_time}\n";
+ * echo "DOS date: {$dt->dos_date}\n";
+ */
+ public function __construct(int $time) {
$this->time = $time;
+
# get date array for timestamp
$d = getdate($time);
@@ -152,15 +335,32 @@ final class DateTime {
}
};
+/**
+ * CRC32b hash context wrapper.
+ * @internal
+ */
final class Hasher {
+ /** @var int $hash Output hash result. */
public $hash;
+
+ /** @var object $ctx Internal hash context. */
private $ctx;
+ /**
+ * Create a new Hasher.
+ */
public function __construct() {
$this->ctx = hash_init('crc32b');
}
- public function write($data) : void {
+ /**
+ * Write data to Hasher.
+ *
+ * @param string $data Input data.
+ * @return void
+ * @throw Error if called after close().
+ */
+ public function write(string $data) : void {
if ($this->ctx !== null) {
# update hash context
hash_update($this->ctx, $data);
@@ -169,6 +369,14 @@ final class Hasher {
}
}
+ /**
+ * Create hash of input data.
+ *
+ * Finalize the internal has context and return a CRC32b hash of the
+ * given input data.
+ *
+ * @return int CRC32b hash of input data.
+ */
public function close() : int {
if ($this->ctx !== null) {
# finalize hash context
@@ -190,67 +398,182 @@ final class Hasher {
}
};
+/**
+ * Abstract base class for data filter methods.
+ *
+ * @internal
+ * @see StoreFilter, DeflateFilter
+ */
abstract class DataFilter {
+ /** @var Writer output writer */
private $output;
+ /**
+ * Create a new DataFilter bound to the given Writer.
+ *
+ * @param Writer $output Output writer.
+ */
public function __construct(Writer &$output) {
$this->output = $output;
}
+ /**
+ * Write data to data filter.
+ *
+ * @param string $data Output data.
+ * @return int Number of bytes written.
+ */
public function write(string $data) : int {
$this->output->write($data);
return strlen($data);
}
+ /**
+ * Close this data filter.
+ *
+ * @return int Number of bytes written.
+ */
public abstract function close() : int;
}
+/**
+ * Data filter for files using the store compression method.
+ *
+ * @internal
+ * @see DataFilter, DeflateFilter
+ */
final class StoreFilter extends DataFilter {
+ /**
+ * Close this data filter.
+ *
+ * @return int Number of bytes written.
+ */
public function close() : int {
return 0;
}
};
+/**
+ * Data filter for files using the deflate compression method.
+ *
+ * @internal
+ *
+ * @see DataFilter, StoreFilter
+ */
final class DeflateFilter extends DataFilter {
+ /** @var object $ctx Deflate context. */
private $ctx;
+ /**
+ * Create a new DeflateFilter bound to the given Writer.
+ *
+ * @param Writer $output Output writer.
+ *
+ * @throw DeflateError If initializing deflate context fails.
+ */
public function __construct(Writer &$output) {
+ # init parent
parent::__construct($output);
+ # init deflate context, check for error
$this->ctx = deflate_init(ZLIB_ENCODING_RAW);
if ($this->ctx === false) {
- throw new Error('deflate_init() failed');
+ throw new DeflateError('deflate_init() failed');
}
}
+ /**
+ * Write data to data filter.
+ *
+ * @param string $data Output data.
+ *
+ * @return int Number of bytes written.
+ *
+ * @throw DeflateError If writing to deflate context fails.
+ * @throw Error If this filter was already closed.
+ */
public function write(string $data) : int {
+ # check state
+ if (!$this->ctx) {
+ # filter already closed
+ throw new Error('Filter already closed');
+ }
+
+ # write data to deflate context, check for error
$compressed_data = deflate_add($this->ctx, $data, ZLIB_NO_FLUSH);
if ($compressed_data === false) {
- throw new Error('deflate_add() failed');
+ throw new DeflateError('deflate_add() failed');
}
+ # pass data to parent
return parent::write($compressed_data);
}
+ /**
+ * Close this data filter.
+ *
+ * @return int Number of bytes written.
+ *
+ * @throw DeflateError If writing to deflate context fails.
+ * @throw Error If this filter was already closed.
+ */
public function close() : int {
+ # check state
+ if (!$this->ctx) {
+ # filter already closed
+ throw new Error('Filter already closed');
+ }
+
+ # finalize context, flush remaining data
$compressed_data = deflate_add($this->ctx, '', ZLIB_FINISH);
if ($compressed_data === false) {
- throw new Error('deflate_add() failed');
+ throw new DeflateError('deflate_add() failed');
}
# clear deflate context
$this->ctx = null;
+ # write remaining data, return number of bytes written
return parent::write($compressed_data);
}
};
+/**
+ * Internal representation for a zip file.
+ *
+ * @internal
+ *
+ * @see DataFilter, StoreFilter
+ */
final class Entry {
const ENTRY_STATE_INIT = 0;
const ENTRY_STATE_DATA = 1;
const ENTRY_STATE_CLOSED = 2;
const ENTRY_STATE_ERROR = 3;
+ /**
+ * @var Writer $output
+ * Reference to output writer.
+ * @var int $pos
+ * Offset from start of file of entry local header.
+ * @var string $name
+ * Name of file.
+ * @var int $method
+ * Compression method.
+ * @var int $time
+ * File creation time (UNIX timestamp).
+ * @var string $comment
+ * File comment.
+ * @var int $uncompressed_size
+ * Raw file size, in bytes. Only valid after file has been
+ * read completely.
+ * @var int $compressed_size
+ * Compressed file size, in bytes. Only valid after file has
+ * been read completely.
+ * @var int $hash
+ * CRC32b hash of file contents. Only valid after file has
+ * been read completely.
+ */
public $output,
$pos,
$name,
@@ -261,11 +584,43 @@ final class Entry {
$compressed_size,
$hash;
+ /**
+ * @var int $len
+ * File length, in bytes. Only valid after file has been read
+ * completely.
+ * @var DateTime $date_time
+ * Date and time converter for this file.
+ * @var Hasher $hasher
+ * Internal hasher for this file.
+ * @var int $state
+ * Entry state.
+ */
private $len,
$date_time,
$hasher,
$state;
+ /**
+ * Create a new entry object.
+ *
+ * @internal
+ *
+ * @param Writer $output
+ * Reference to output writer.
+ * @param int $pos
+ * Offset from start of file of entry local header.
+ * @param string $name
+ * Name of file.
+ * @param int $method
+ * Compression method.
+ * @param int $time
+ * File creation time (UNIX timestamp).
+ * @param string $comment
+ * File comment.
+ *
+ * @throw Error if compression method is unknown.
+ * @throw PathError if compression method is unknown.
+ */
public function __construct(
Writer &$output,
int $pos,
@@ -274,6 +629,9 @@ final class Entry {
int $time,
string $comment
) {
+ # set state
+ $this->state = self::ENTRY_STATE_INIT;
+
$this->output = $output;
$this->pos = $pos;
$this->name = $name;
@@ -283,7 +641,6 @@ final class Entry {
$this->uncompressed_size = 0;
$this->compressed_size = 0;
- $this->state = self::ENTRY_STATE_INIT;
$this->len = 0;
$this->date_time = new DateTime($time);
@@ -303,6 +660,17 @@ final class Entry {
$this->check_path($name);
}
+ /**
+ * Write data to file entry.
+ *
+ * @api
+ *
+ * @param string $data Output file data.
+ *
+ * @throw Error if entry state is invalid.
+ *
+ * @return int Number of bytes written to output.
+ */
public function write(string &$data) : int {
try {
# check entry state
@@ -331,6 +699,15 @@ final class Entry {
# local header methods #
########################
+ /**
+ * Write local file header.
+ *
+ * @internal
+ *
+ * @throw Error if entry state is invalid.
+ *
+ * @return int Number of bytes written to output.
+ */
public function write_local_header() : int {
# check state
if ($this->state != self::ENTRY_STATE_INIT) {
@@ -353,6 +730,11 @@ final class Entry {
const ENTRY_VERSION_NEEDED = 45;
const ENTRY_BIT_FLAGS = 0b100000001000;
+ /**
+ * Create local file header for this entry.
+ *
+ * @return string Packed local file header.
+ */
private function get_local_header() : string {
# build extra data
$extra_data = pack('vv',
@@ -363,7 +745,7 @@ final class Entry {
# build and return local file header
return pack('VvvvvvVVVvv',
0x04034b50, # local file header signature (4 bytes)
- self::ENTRY_VERSION_NEEDED, # version needed to extract (2 bytes)
+ VERSION_NEEDED, # version needed to extract (2 bytes)
self::ENTRY_BIT_FLAGS, # general purpose bit flag (2 bytes)
$this->method, # compression method (2 bytes)
$this->date_time->dos_time, # last mod file time (2 bytes)
@@ -380,6 +762,15 @@ final class Entry {
# local footer methods #
########################
+ /**
+ * Write local file footer (e.g. data descriptor).
+ *
+ * @internal
+ *
+ * @throw Error if entry state is invalid.
+ *
+ * @return int Number of bytes written to output.
+ */
public function write_local_footer() : int {
# check state
if ($this->state != self::ENTRY_STATE_DATA) {
@@ -407,6 +798,11 @@ final class Entry {
return strlen($data);
}
+ /**
+ * Create local file footer (data descriptor) for this entry.
+ *
+ * @return string Packed local file footer.
+ */
private function get_local_footer() : string {
return pack('VVPP',
0x08074b50, # data descriptor signature (4 bytes)
@@ -420,25 +816,40 @@ final class Entry {
# central header methods #
##########################
+ /**
+ * Write central directory entry for this file to output writer.
+ *
+ * @internal
+ *
+ * @return int Number of bytes written to output.
+ */
public function write_central_header() : int {
$data = $this->get_central_header();
$this->output->write($data);
return strlen($data);
}
+ /**
+ * Create extra data for central directory entry for this file.
+ *
+ * @return string Packed extra data for central directory entry.
+ */
private function get_central_extra_data() : string {
$r = [];
+ # check uncompressed size for overflow
if ($this->uncompressed_size >= 0xFFFFFFFF) {
# append 64-bit uncompressed size
$r[] = pack('P', $this->uncompressed_size);
}
+ # check compressed size for overflow
if ($this->compressed_size >= 0xFFFFFFFF) {
# append 64-bit compressed size
$r[] = pack('P', $this->compressed_size);
}
+ # check offset for overflow
if ($this->pos >= 0xFFFFFFFF) {
# append 64-bit file offset
$r[] = pack('P', $this->pos);
@@ -448,6 +859,7 @@ final class Entry {
$r = join('', $r);
if (strlen($r) > 0) {
+ # has overflow, so generate zip64 info
$r = pack('vv',
0x01, # zip64 ext. info extra tag (2 bytes)
strlen($r) # size of this extra block (2 bytes)
@@ -458,10 +870,15 @@ final class Entry {
return $r;
}
+ /**
+ * Create central directory entry for this file.
+ *
+ * @return string Packed central directory entry.
+ */
private function get_central_header() : string {
$extra_data = $this->get_central_extra_data();
- # get sizes and offset
+ # get compressed size, uncompressed size, and offset
$compressed_size = ($this->compressed_size >= 0xFFFFFFFF) ? 0xFFFFFFFF : $this->compressed_size;
$uncompressed_size = ($this->uncompressed_size >= 0xFFFFFFFF) ? 0xFFFFFFFF : $this->uncompressed_size;
$pos = ($this->pos >= 0xFFFFFFFF) ? 0xFFFFFFFF : $this->pos;
@@ -469,8 +886,8 @@ final class Entry {
# pack and return central header
return pack('VvvvvvvVVVvvvvvVV',
0x02014b50, # central file header signature (4 bytes)
- self::ENTRY_VERSION_NEEDED, # FIXME: version made by (2 bytes)
- self::ENTRY_VERSION_NEEDED, # version needed to extract (2 bytes)
+ VERSION_NEEDED, # FIXME: version made by (2 bytes)
+ VERSION_NEEDED, # version needed to extract (2 bytes)
self::ENTRY_BIT_FLAGS, # general purpose bit flag (2 bytes)
$this->method, # compression method (2 bytes)
$this->date_time->dos_time, # last mod file time (2 bytes)
@@ -492,6 +909,27 @@ final class Entry {
# utility methods #
###################
+ /**
+ * Check file path.
+ *
+ * Verifies that the given file path satisfies the following
+ * constraints:
+ *
+ * * Path is not null.
+ * * Path not empty.
+ * * Path is less than 65535 bytes in length.
+ * * Path does not contain a leading slash.
+ * * Path does not contain a trailing slash.
+ * * Path does not contain double slashes.
+ * * Path does not contain backslashes.
+ * * Path is not a relative path.
+ *
+ * @param string $path Input file path.
+ *
+ * @return void
+ *
+ * @throw PathError if path is invalid.
+ */
private function check_path(string $path) : void {
# make sure path is non-null
if (!$path) {
@@ -529,12 +967,19 @@ final class Entry {
}
# check for relative path
- if (preg_match('/\.\./', $path)) {
+ if (preg_match('/^\.\.|\\/\.\.\\/|\.\.$/', $path)) {
throw new PathError($path, "relative path");
}
}
};
+/**
+ * Dynamically generate streamed zip file.
+ *
+ * @api
+ *
+ * {@example ../examples/01-simple.php}
+ */
final class ZipStream {
const STREAM_STATE_INIT = 0;
const STREAM_STATE_ENTRY = 1;
@@ -544,13 +989,25 @@ final class ZipStream {
# stream chunk size
const READ_BUF_SIZE = 8192;
+ /** @var string Output archive name. */
public $name;
+
+ /**
+ * @var array $args Hash of options.
+ * @var Writer $output output Writer.
+ * @var int $pos Current byte offset in output stream.
+ * @var array $entries Array of archive entries.
+ */
private $args,
$output,
$pos = 0,
$entries = [],
$paths = [];
+ /**
+ * Default archive options.
+ * @internal
+ */
static $ARCHIVE_DEFAULTS = [
'method' => Methods::DEFLATE,
'comment' => '',
@@ -558,10 +1015,22 @@ final class ZipStream {
'header' => true,
];
+ /**
+ * Default file options.
+ * @internal
+ */
static $FILE_DEFAULTS = [
'comment' => '',
];
+ /**
+ * Create a new ZipStream object.
+ *
+ * @param string $name Output archive name.
+ * @param array $args Hash of output options (optional).
+ *
+ * {@example ../examples/01-simple.php}
+ */
public function __construct(string $name, array &$args = []) {
try {
$this->state = self::STREAM_STATE_INIT;
@@ -591,17 +1060,41 @@ final class ZipStream {
}
}
+ /**
+ * Add file to output archive.
+ *
+ * @param string $dst_path Destination path in output archive.
+ * @param string $data File contents.
+ * @param array $args File options (optional).
+ *
+ * @return void
+ *
+ * {@example ../examples/01-simple.php}
+ */
public function add_file(
string $dst_path,
string $data,
array $args = []
- ) {
+ ) : void {
$this->add($dst_path, function(Entry &$e) use (&$data) {
# write data
$e->write($data);
}, array_merge(self::$FILE_DEFAULTS, $args));
}
+ /**
+ * Add file on the local file system to output archive.
+ *
+ * @param string $dst_path Destination path in output archive.
+ * @param string $src_path Path to input file.
+ * @param array $args File options (optional).
+ *
+ * @return void
+ *
+ * {@example ../examples/02-add_file_from_path.php}
+ *
+ * @throw FileError if the file could not be opened or read.
+ */
public function add_file_from_path(
string $dst_path,
string $src_path,
@@ -632,6 +1125,20 @@ final class ZipStream {
$this->add_stream($dst_path, $fh, $args);
}
+ /**
+ * Add contents of resource stream to output archive.
+ *
+ * @param string $dst_path Destination path in output archive.
+ * @param resource $src Input resource stream.
+ * @param array $args File options (optional).
+ *
+ * @return void
+ *
+ * {@example ../examples/03-add_stream.php}
+ *
+ * @throw Error if $src is not a resource.
+ * @throw Error if the resource could not be read.
+ */
public function add_stream(
string $dst_path,
&$src,
@@ -650,7 +1157,7 @@ final class ZipStream {
# check for error
if ($buf === false) {
- throw new Error("file read error");
+ throw new Error("fread() error");
}
# write chunk to entry
@@ -664,6 +1171,20 @@ final class ZipStream {
}, $args);
}
+ /**
+ * Dynamically write file contents to output archive.
+ *
+ * @param string $dst_path Destination path in output archive.
+ * @param callable $cb Write callback.
+ * @param array $args File options (optional).
+ *
+ * @return void
+ *
+ * {@example ../examples/04-add.php}
+ *
+ * @throw Error if the archive is in an invalid state.
+ * @throw Error if the destination path already exists.
+ */
public function add(
string $dst_path,
callable $cb,
@@ -724,6 +1245,15 @@ final class ZipStream {
}
}
+ /**
+ * Finalize the output stream.
+ *
+ * @return int Total number of bytes written.
+ *
+ * @throw Error if the archive is in an invalid state.
+ *
+ * {@example ../examples/01-simple.php}
+ */
public function close() : int {
try {
if ($this->state != self::STREAM_STATE_INIT) {
@@ -768,6 +1298,15 @@ final class ZipStream {
}
}
+ /**
+ * Create an archive and send it using a single function.
+ *
+ * @param string $name Name of output archive.
+ * @param callable $cb Context callback.
+ * @param array $args Hash of archive options (optional).
+ *
+ * {@example ../examples/05-send.php}
+ */
public static function send(
string $name,
callable $cb,
@@ -787,30 +1326,44 @@ final class ZipStream {
# central directory record methods #
####################################
- const VERSION_NEEDED = 45;
-
+ /**
+ * Get Zip64 end of Central Directory Record (CDR)
+ *
+ * @param int $cdr_pos CDR offset, in bytes.
+ * @param int $cdr_len Size of CDR, in bytes.
+ *
+ * @return string Packed Zip64 end of Central Directory Record.
+ */
private function get_zip64_end_of_central_directory_record(
int $cdr_pos,
int $cdr_len
) : string {
+ # get entry count
$num_entries = count($this->entries);
return pack('VPvvVVPPPP',
0x06064b50, # zip64 end of central dir signature (4 bytes)
44, # size of zip64 end of central directory record (8 bytes)
- self::VERSION_NEEDED, # FIXME: version made by (2 bytes)
- self::VERSION_NEEDED, # version needed to extract (2 bytes)
- 0, # number of this disk (4 bytes)
- 0, # number of the disk with the start of the central directory (4 bytes)
- $num_entries, # total number of entries in the central directory on this disk (8 bytes)
- $num_entries, # total number of entries in the central directory (8 bytes)
- $cdr_len, # size of the central directory (8 bytes)
- $cdr_pos # offset of start of central directory with respect to the starting disk number (8 bytes)
+ VERSION_NEEDED, # FIXME: version made by (2 bytes)
+ VERSION_NEEDED, # version needed to extract (2 bytes)
+ 0, # number of this disk (4 bytes)
+ 0, # number of the disk with the start of the central directory (4 bytes)
+ $num_entries, # total number of entries in the central directory on this disk (8 bytes)
+ $num_entries, # total number of entries in the central directory (8 bytes)
+ $cdr_len, # size of the central directory (8 bytes)
+ $cdr_pos # offset of start of central directory with respect to the starting disk number (8 bytes)
# zip64 extensible data sector (variable size)
# (FIXME: is extensible data sector needed?)
);
}
+ /**
+ * Get Zip64 End of Central Directory Record (CDR) Locator.
+ *
+ * @param int $zip64_cdr_pos Zip64 End of CDR offset, in bytes.
+ *
+ * @return string Packed Zip64 End of CDR Locator.
+ */
private function get_zip64_end_of_central_directory_locator(
int $zip64_cdr_pos
) : string {
@@ -822,22 +1375,34 @@ final class ZipStream {
);
}
+ /**
+ * Get End of Central Directory Record (CDR) Locator.
+ *
+ * @param int $cdr_pos CDR offset, in bytes.
+ * @param int $cdr_len CDR size, in bytes.
+ *
+ * @return string End of CDR Record.
+ */
private function get_end_of_central_directory_record(
int $cdr_pos,
int $cdr_len
) : string {
- # clamp num_entries
+ # get entry count
$num_entries = count($this->entries);
if ($num_entries >= 0xFFFF) {
+ # clamp entry count
$num_entries = 0xFFFF;
}
- # clamp cdr_len and cdr_pos
+ # get/clamp cdr_len and cdr_pos
$cdr_len = ($cdr_len >= 0xFFFFFFFF) ? 0xFFFFFFFF : $cdr_len;
$cdr_pos = ($cdr_pos >= 0xFFFFFFFF) ? 0xFFFFFFFF : $cdr_pos;
- # get comment
+ # get comment, check length
$comment = $this->args['comment'];
+ if (strlen($comment) >= 65535) {
+ throw new Error('comment too long');
+ }
return pack('VvvvvVVv',
0x06054b50, # end of central dir signature (4 bytes)
@@ -855,6 +1420,13 @@ final class ZipStream {
# utility methods #
###################
+ /**
+ * Get UNIX timestamp for given entry.
+ *
+ * @param array $args Entry options.
+ *
+ * @return int UNIX timestamp.
+ */
private function get_entry_time(array &$args) : int {
if (isset($args['time'])) {
return $args['time'];
@@ -865,6 +1437,13 @@ final class ZipStream {
}
}
+ /**
+ * Get compression method for given entry.
+ *
+ * @param array $args Entry options.
+ *
+ * @return int Compression method.
+ */
private function get_entry_method(array &$args) : int {
if (isset($args['method'])) {
return $args['method'];