Skip to content

Commit 5a13212

Browse files
committed
Incremental block by block compression for ZIP
1 parent 726dfdd commit 5a13212

File tree

1 file changed

+120
-4
lines changed

1 file changed

+120
-4
lines changed

src/Zip.php

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,88 @@ public function addFile($file, $fileinfo = '')
295295
throw new ArchiveIOException('Archive has been closed, files can no longer be added');
296296
}
297297

298-
$data = @file_get_contents($file);
299-
if ($data === false) {
298+
$fp = @fopen($file, 'rb');
299+
if ($fp === false) {
300300
throw new ArchiveIOException('Could not open file for reading: '.$file);
301301
}
302302

303-
// FIXME could we stream writing compressed data? gzwrite on a fopen handle?
304-
$this->addData($fileinfo, $data);
303+
$offset = $this->dataOffset();
304+
$name = $fileinfo->getPath();
305+
$time = $fileinfo->getMtime();
306+
307+
// write local file header (temporary CRC and size)
308+
$this->writebytes($this->makeLocalFileHeader(
309+
$time,
310+
0,
311+
0,
312+
0,
313+
$name,
314+
(bool) $this->complevel
315+
));
316+
317+
// we store no encryption header
318+
319+
// prepare info, compress and write data to archive
320+
$deflate_context = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => $this->complevel]);
321+
$crc_context = hash_init('crc32b');
322+
$size = $csize = 0;
323+
324+
while (!feof($fp)) {
325+
$block = fread($fp, 512);
326+
327+
if ($this->complevel) {
328+
$is_first_block = $size === 0;
329+
$is_last_block = feof($fp);
330+
331+
if ($is_last_block) {
332+
$c_block = deflate_add($deflate_context, $block, ZLIB_FINISH);
333+
// get rid of the compression footer
334+
$c_block = substr($c_block, 0, -4);
335+
} else {
336+
$c_block = deflate_add($deflate_context, $block, ZLIB_NO_FLUSH);
337+
}
338+
339+
// get rid of the compression header
340+
if ($is_first_block) {
341+
$c_block = substr($c_block, 2);
342+
}
343+
344+
$csize += strlen($c_block);
345+
$this->writebytes($c_block);
346+
} else {
347+
$this->writebytes($block);
348+
}
349+
350+
$size += strlen($block);
351+
hash_update($crc_context, $block);
352+
}
353+
fclose($fp);
354+
355+
// update the local file header with the computed CRC and size
356+
$crc = hexdec(hash_final($crc_context));
357+
$csize = $this->complevel ? $csize : $size;
358+
$this->writebytesAt($this->makeCrcAndSize(
359+
$crc,
360+
$size,
361+
$csize,
362+
), $offset + self::LOCAL_FILE_HEADER_CRC_OFFSET);
363+
364+
// we store no data descriptor
365+
366+
// add info to central file directory
367+
$this->ctrl_dir[] = $this->makeCentralFileRecord(
368+
$offset,
369+
$time,
370+
$crc,
371+
$size,
372+
$csize,
373+
$name,
374+
(bool) $this->complevel
375+
);
376+
377+
if(is_callable($this->callback)) {
378+
call_user_func($this->callback, $fileinfo);
379+
}
305380
}
306381

307382
/**
@@ -709,6 +784,29 @@ protected function writebytes($data)
709784
return $written;
710785
}
711786

787+
/**
788+
* Write to the open filepointer or memory at the specified offset
789+
*
790+
* @param string $data
791+
* @param int $offset
792+
* @throws ArchiveIOException
793+
* @return int number of bytes written
794+
*/
795+
protected function writebytesAt($data, $offset) {
796+
if (!$this->file) {
797+
$this->memory .= substr_replace($this->memory, $data, $offset);
798+
$written = strlen($data);
799+
} else {
800+
@fseek($this->fh, $offset);
801+
$written = @fwrite($this->fh, $data);
802+
@fseek($this->fh, 0, SEEK_END);
803+
}
804+
if ($written === false) {
805+
throw new ArchiveIOException('Failed to write to archive stream');
806+
}
807+
return $written;
808+
}
809+
712810
/**
713811
* Current data pointer position
714812
*
@@ -825,6 +923,8 @@ protected function makeCentralFileRecord($offset, $ts, $crc, $len, $clen, $name,
825923
return $header;
826924
}
827925

926+
const LOCAL_FILE_HEADER_CRC_OFFSET = 14;
927+
828928
/**
829929
* Returns a local file header for the given data
830930
*
@@ -865,6 +965,22 @@ protected function makeLocalFileHeader($ts, $crc, $len, $clen, $name, $comp = nu
865965
return $header;
866966
}
867967

968+
/**
969+
* Returns only a part of the local file header containing the CRC, size and compressed size.
970+
* Used to update these fields for an already written header.
971+
*
972+
* @param int $crc CRC32 checksum of the uncompressed data
973+
* @param int $len length of the uncompressed data
974+
* @param int $clen length of the compressed data
975+
* @return string
976+
*/
977+
protected function makeCrcAndSize($crc, $len, $clen) {
978+
$header = pack('V', $crc); // crc-32
979+
$header .= pack('V', $clen); // compressed size
980+
$header .= pack('V', $len); // uncompressed size
981+
return $header;
982+
}
983+
868984
/**
869985
* Returns an allowed filename and an extra field header
870986
*

0 commit comments

Comments
 (0)