summaryrefslogtreecommitdiff
path: root/src/ZipStream.php
diff options
context:
space:
mode:
Diffstat (limited to 'src/ZipStream.php')
-rw-r--r--src/ZipStream.php207
1 files changed, 165 insertions, 42 deletions
diff --git a/src/ZipStream.php b/src/ZipStream.php
index ea25a87..10326a6 100644
--- a/src/ZipStream.php
+++ b/src/ZipStream.php
@@ -7,11 +7,9 @@ const VERSION = '0.3.0';
final class Methods {
const STORE = 0;
- const DEFLATE = 2;
+ const DEFLATE = 8;
};
-namespace Pablotron\ZipStream\Errors;
-
class Error extends \Exception { };
final class FileError extends Error {
public $file_name;
@@ -31,8 +29,6 @@ final class PathError extends Error {
}
};
-namespace Pablotron\ZipStream\Writers;
-
interface Writer {
public function set(string $key, string $val);
public function open();
@@ -82,7 +78,7 @@ final class FileWriter implements Writer {
# open output file
$this->fh = @fopen($this->path, 'wb');
if (!$this->fh) {
- throw new Errors\FileError($path, "couldn't open file");
+ throw new FileError($path, "couldn't open file");
}
# set state
@@ -92,7 +88,7 @@ final class FileWriter implements Writer {
public function write(string $data) {
# check state
if ($this->state != self::STATE_OPEN) {
- throw new Errors\Error("invalid output state");
+ throw new Error("invalid output state");
}
# write data
@@ -101,7 +97,7 @@ final class FileWriter implements Writer {
# check for error
if ($len === false) {
$this->state = self::STATE_ERROR;
- throw new Errors\FileError($this->path, 'fwrite() failed');
+ throw new FileError($this->path, 'fwrite() failed');
}
}
@@ -110,7 +106,7 @@ final class FileWriter implements Writer {
if ($this->state == self::STATE_CLOSED) {
return;
} else if ($this->state != self::STATE_OPEN) {
- throw new Errors\Error("invalid output state");
+ throw new Error("invalid output state");
}
# close file handle
@@ -121,7 +117,6 @@ final class FileWriter implements Writer {
}
};
-namespace Pablotron\ZipStream;
#
# Convert a UNIX timestamp to a DOS time/date.
#
@@ -157,6 +152,37 @@ final class DateTime {
}
};
+final class Hasher {
+ public $hash;
+ private $ctx;
+
+ public function __construct() {
+ $this->ctx = hash_init('crc32b');
+ }
+
+ public function write($data) {
+ # update hash context
+ hash_update($this->ctx, $data);
+ }
+
+ public function close() {
+ $d = hash_final($this->ctx, true);
+ $this->ctx = null;
+
+ # encode hash as uint32_t
+ # (FIXME: endian issue?)
+ $this->hash = (
+ (ord($d[0]) << 24) |
+ (ord($d[1]) << 16) |
+ (ord($d[2]) << 8) |
+ (ord($d[3]))
+ );
+
+ # return encoded result
+ return $this->hash;
+ }
+};
+
final class Entry {
const STATE_INIT = 0;
const STATE_DATA = 1;
@@ -175,7 +201,7 @@ final class Entry {
private $len,
$date_time,
- $hash_context,
+ $hasher,
$state;
public function __construct(
@@ -200,7 +226,7 @@ final class Entry {
$this->date_time = new DateTime($time);
# init hash context
- $this->hash_context = hash_init('crc32b');
+ $this->hasher = new Hasher();
# sanity check path
$this->check_path($name);
@@ -210,14 +236,14 @@ final class Entry {
try {
# check entry state
if ($this->state != self::STATE_DATA) {
- throw new Errors\Error("invalid entry state");
+ throw new Error("invalid entry state");
}
# update output size
$this->uncompressed_size += strlen($data);
# update hash context
- hash_update($this->hash_context, $data);
+ $this->hasher->write($data);
if ($this->method === Methods::DEFLATE) {
$compressed_data = gzdeflate($data);
@@ -226,7 +252,7 @@ final class Entry {
$compressed_data = $data;
$this->compressed_size += strlen($data);
} else {
- throw new Errors\Error('invalid entry method');
+ throw new Error('invalid entry method');
}
# write compressed data to output
@@ -244,7 +270,7 @@ final class Entry {
public function write_local_header() {
# check state
if ($this->state != self::STATE_INIT) {
- throw new Errors\Error("invalid entry state");
+ throw new Error("invalid entry state");
}
# get entry header, update entry length
@@ -260,7 +286,7 @@ final class Entry {
return strlen($data);
}
- const ENTRY_VERSION_NEEDED = 62;
+ const ENTRY_VERSION_NEEDED = 45;
const ENTRY_BIT_FLAGS = 0b100000001000;
private function get_local_header() {
@@ -294,12 +320,12 @@ final class Entry {
# check state
if ($this->state != self::STATE_DATA) {
$this->state = self::STATE_ERROR;
- throw new Errors\Error("invalid entry state");
+ throw new Error("invalid entry state");
}
# finalize hash context
- $this->hash = hash_final($this->hash_context, true);
- $this->hash_context = null;
+ $this->hash = $this->hasher->close();
+ $this->hasher = null;
# get footer
$data = $this->get_local_footer();
@@ -327,6 +353,12 @@ final class Entry {
# central header methods #
##########################
+ public function write_central_header() {
+ $data = $this->get_central_header();
+ $this->output->write($data);
+ return strlen($data);
+ }
+
private function get_central_extra_data() {
$r = [];
@@ -359,14 +391,13 @@ final class Entry {
return $r;
}
-
private function get_central_header() {
$extra_data = $this->get_central_extra_data();
# get sizes and offset
- $compressed_size = ($this->compressed_size >= 0xFFFFFFFF) ? 0xFFFFFFFF : $compressed_size;
- $uncompressed_size = ($this->uncompressed_size >= 0xFFFFFFFF) ? 0xFFFFFFFF : $uncompressed_size;
- $pos = ($this->pos >= 0xFFFFFFFF) ? 0xFFFFFFFF : $pos;
+ $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;
# pack and return central header
return pack('VvvvvvvVVVvvvvvVV',
@@ -397,42 +428,42 @@ final class Entry {
private function check_path(string $path) {
# make sure path is non-null
if (!$path) {
- throw new Errors\PathError($path, "null path");
+ throw new PathError($path, "null path");
}
# check for empty path
if (!strlen($path)) {
- throw new Errors\PathError($path, "empty path");
+ throw new PathError($path, "empty path");
}
# check for long path
if (strlen($path) > 65535) {
- throw new Errors\PathError($path, "path too long");
+ throw new PathError($path, "path too long");
}
# check for leading slash
if (!$path[0] == '/') {
- throw new Errors\PathError($path, "path contains leading slash");
+ throw new PathError($path, "path contains leading slash");
}
# check for trailing slash
if (preg_match('/\\$/', $path)) {
- throw new Errors\PathError($path, "path contains trailing slash");
+ throw new PathError($path, "path contains trailing slash");
}
# check for double slashes
if (preg_match('/\/\//', $path)) {
- throw new Errors\PathError($path, "path contains double slashes");
+ throw new PathError($path, "path contains double slashes");
}
# check for backslashes
- if (preg_match('/\\/', $path)) {
- throw new Errors\PathError($path, "path contains backslashes");
+ if (preg_match('/\\\\/', $path)) {
+ throw new PathError($path, "path contains backslashes");
}
# check for relative path
if (preg_match('/\.\./', $path)) {
- throw new Errors\PathError($path, "relative path");
+ throw new PathError($path, "relative path");
}
}
};
@@ -480,7 +511,7 @@ final class ZipStream {
$this->output = $args['output'];
} else {
# no output set, create default response writer
- $this->output = new Writers\HTTPResponseWriter();
+ $this->output = new HTTPResponseWriter();
}
# set output metadata
@@ -516,7 +547,7 @@ final class ZipStream {
# get file mtime
$time = @filemtime($src_path);
if ($time === false) {
- throw new Errors\FileError($src_path, "couldn't get file mtime");
+ throw new FileError($src_path, "couldn't get file mtime");
}
# save file mtime
@@ -529,7 +560,7 @@ final class ZipStream {
# open input stream
$fh = @fopen($src_path, 'rb');
if (!$fh) {
- throw new Errors\FileError($src_path, "couldn't open file");
+ throw new FileError($src_path, "couldn't open file");
}
# read input
@@ -549,7 +580,7 @@ final class ZipStream {
# check for error
if ($buf === false) {
- throw new Errors\Error("file read error");
+ throw new Error("file read error");
}
# write chunk to entry
@@ -570,12 +601,12 @@ final class ZipStream {
) {
# check state
if ($this->state != self::STATE_INIT) {
- throw new Errors\Error("invalid output state");
+ throw new Error("invalid output state");
}
# check for duplicate path
if (isset($this->paths[$dst_path])) {
- throw new Errors\Error("duplicate path: $dst_path");
+ throw new Error("duplicate path: $dst_path");
}
$this->paths[$dst_path] = true;
@@ -626,11 +657,35 @@ final class ZipStream {
public function close() {
try {
if ($this->state != self::STATE_INIT) {
- throw new Errors\Error("invalid archive state");
+ throw new Error("invalid archive state");
}
- # TODO: write cdr
- # TODO: write archive footer
+ # cache cdr offset, write cdr, get cdr length
+ $cdr_pos = $this->pos;
+ $cdr_len = array_reduce($this->entries, function($r, $e) {
+ return $r + $e->write_central_header();
+ }, 0);
+
+ # update position
+ $this->pos += $cdr_len;
+
+ # cache zip64 end of cdr position
+ $zip64_cdr_pos = $this->pos;
+
+ # write zip64 end cdr record
+ $data = $this->get_zip64_end_of_central_directory_record($cdr_pos, $cdr_len);
+ $this->output->write($data);
+ $this->pos += strlen($data);
+
+ # write zip64 end cdr locator
+ $data = $this->get_zip64_end_of_central_directory_locator($zip64_cdr_pos);
+ $this->output->write($data);
+ $this->pos += strlen($data);
+
+ # write end cdr record
+ $data = $this->get_end_of_central_directory_record($cdr_pos, $cdr_len);
+ $this->output->write($data);
+ $this->pos += strlen($data);
# close output
$this->output->close();
@@ -654,6 +709,74 @@ final class ZipStream {
return $zip->close();
}
+ ####################################
+ # central directory record methods #
+ ####################################
+
+ const VERSION_NEEDED = 45;
+
+ private function get_zip64_end_of_central_directory_record(
+ int $cdr_pos,
+ int $cdr_len
+ ) {
+ $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)
+ # zip64 extensible data sector (variable size)
+ # (FIXME: is extensible data sector needed?)
+ );
+ }
+
+ private function get_zip64_end_of_central_directory_locator(
+ int $zip64_cdr_pos
+ ) {
+ return pack('VVPV',
+ 0x07064b50, # zip64 end of central dir locator signature (4 bytes)
+ 0, # number of the disk with the start of the zip64 end of central directory (4 bytes)
+ $zip64_cdr_pos, # relative offset of the zip64 end of central directory record (8 bytes)
+ 1 # total number of disks (4 bytes)
+ );
+ }
+
+ private function get_end_of_central_directory_record(
+ int $cdr_pos,
+ int $cdr_len
+ ) {
+ # clamp num_entries
+ $num_entries = count($this->entries);
+ if ($num_entries >= 0xFFFF) {
+ $num_entries = 0xFFFF;
+ }
+
+ # clamp cdr_len and cdr_pos
+ $cdr_len = ($cdr_len >= 0xFFFFFFFF) ? 0xFFFFFFFF : $cdr_len;
+ $cdr_pos = ($cdr_pos >= 0xFFFFFFFF) ? 0xFFFFFFFF : $cdr_pos;
+
+ # get comment
+ $comment = $this->args['comment'];
+
+ return pack('VvvvvVVv',
+ 0x06054b50, # end of central dir signature (4 bytes)
+ 0, # number of this disk (2 bytes)
+ 0, # disk with the start of the central directory (2 bytes)
+ $num_entries, # number of entries in the central directory on this disk (2 bytes)
+ $num_entries, # number of entries in the central directory (2 bytes)
+ $cdr_len, # size of the central directory (4 bytes)
+ $cdr_pos, # offset of start of central directory with respect to the starting disk number (4 bytes)
+ strlen($comment) # .ZIP file comment length (2 bytes)
+ ) . $comment;
+ }
+
###################
# utility methods #
###################