Skip to content

Commit 803fdf0

Browse files
committed
WIP: Calculate ZIP Size
Calculates the resulting file size with the minimal amount of work that is required. Fixes #89
1 parent 494ce19 commit 803fdf0

File tree

6 files changed

+494
-26
lines changed

6 files changed

+494
-26
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZipStream\Exception;
6+
7+
use ZipStream\Exception;
8+
9+
/**
10+
* This Exception gets invoked if a file is not as large as it was specified.
11+
*/
12+
class FileSizeIncorrectException extends Exception
13+
{
14+
/**
15+
* @internal
16+
*/
17+
public function __construct(
18+
public readonly int $expectedSize,
19+
public readonly int $actualSize
20+
) {
21+
parent::__construct("File is {$actualSize} instead of {$expectedSize} bytes large. Adjust `exactSize` parameter.");
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZipStream\Exception;
6+
7+
use ZipStream\Exception;
8+
9+
/**
10+
* This Exception gets invoked if a strict simulation is executed and the file
11+
* information can't be determined without reading the entire file.
12+
*/
13+
class SimulationFileUnknownException extends Exception
14+
{
15+
public function __construct()
16+
{
17+
parent::__construct('The details of the strict simulation file could not be determined without reading the entire file.');
18+
}
19+
}

src/File.php

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
use DateTimeInterface;
99
use DeflateContext;
1010
use RuntimeException;
11+
use ZipStream\Exception\FileSizeIncorrectException;
1112
use ZipStream\Exception\OverflowException;
1213
use ZipStream\Exception\ResourceActionException;
14+
use ZipStream\Exception\SimulationFileUnknownException;
1315
use ZipStream\Exception\StreamNotReadableException;
1416
use ZipStream\Exception\StreamNotSeekableException;
1517

@@ -32,8 +34,6 @@ class File
3234

3335
private readonly string $fileName;
3436

35-
private int $totalSize = 0;
36-
3737
/**
3838
* @var resource
3939
*/
@@ -44,15 +44,18 @@ class File
4444
*/
4545
public function __construct(
4646
string $fileName,
47-
private int $startOffset,
47+
private readonly OperationMode $operationMode,
48+
private readonly int $startOffset,
4849
private readonly CompressionMethod $compressionMethod,
4950
private readonly string $comment,
5051
private readonly DateTimeInterface $lastModificationDateTime,
5152
private readonly int $deflateLevel,
5253
private readonly ?int $maxSize,
54+
private readonly ?int $exactSize,
5355
private readonly bool $enableZip64,
5456
private readonly bool $enableZeroHeader,
5557
private readonly Closure $send,
58+
private readonly Closure $recordSentBytes,
5659
$stream,
5760
) {
5861
$this->fileName = self::filterFilename($fileName);
@@ -81,20 +84,61 @@ public function __construct(
8184

8285
public function process(): string
8386
{
84-
if (!$this->enableZeroHeader) {
87+
$forecastSize = $this->forecastSize();
88+
89+
if ($this->enableZeroHeader) {
90+
// No calculation required
91+
} elseif ($this->isSimulation() && $forecastSize) {
92+
$this->uncompressedSize = $forecastSize;
93+
$this->compressedSize = $forecastSize;
94+
} elseif ($this->operationMode === OperationMode::SIMULATE_STRICT) {
95+
throw new SimulationFileUnknownException();
96+
} else {
8597
$this->readStream(send: false);
8698
if (rewind($this->stream) === false) {
8799
throw new ResourceActionException('rewind', $this->stream);
88100
}
89101
}
90102

91103
$this->addFileHeader();
92-
$this->readStream(send: true);
93-
$this->addFileFooter();
94104

105+
$detectedSize = $forecastSize ?? $this->compressedSize;
106+
107+
if (
108+
$this->isSimulation() &&
109+
$detectedSize > 0
110+
) {
111+
($this->recordSentBytes)($detectedSize);
112+
} elseif ($this->operationMode === OperationMode::SIMULATE_STRICT) {
113+
throw new SimulationFileUnknownException();
114+
} else {
115+
$this->readStream(send: true);
116+
}
117+
118+
$this->addFileFooter();
95119
return $this->getCdrFile();
96120
}
97121

122+
private function forecastSize(): ?int
123+
{
124+
if ($this->compressionMethod !== CompressionMethod::STORE) {
125+
return null;
126+
}
127+
if ($this->exactSize) {
128+
return $this->exactSize;
129+
}
130+
$fstat = fstat($this->stream);
131+
if (!$fstat || !array_key_exists('size', $fstat) || $fstat['size'] < 1) {
132+
return null;
133+
}
134+
135+
if ($this->maxSize !== null && $this->maxSize < $fstat['size']) {
136+
return $this->maxSize;
137+
}
138+
139+
return $fstat['size'];
140+
}
141+
98142
/**
99143
* Create and send zip header for this file.
100144
*/
@@ -127,8 +171,6 @@ private function addFileHeader(): void
127171

128172

129173
($this->send)($data);
130-
131-
$this->totalSize += strlen($data);
132174
}
133175

134176
/**
@@ -239,8 +281,6 @@ private function addFileFooter(): void
239281
}
240282

241283
($this->send)($footer);
242-
243-
$this->totalSize += strlen($footer);
244284
}
245285

246286
private function readStream(bool $send): void
@@ -251,8 +291,16 @@ private function readStream(bool $send): void
251291

252292
$deflate = $this->compressionInit();
253293

254-
while (!feof($this->stream) && ($this->maxSize === null || $this->uncompressedSize < $this->maxSize)) {
255-
$readLength = min(($this->maxSize ?? PHP_INT_MAX) - $this->uncompressedSize, self::CHUNKED_READ_BLOCK_SIZE);
294+
while (
295+
!feof($this->stream) &&
296+
($this->maxSize === null || $this->uncompressedSize < $this->maxSize) &&
297+
($this->exactSize === null || $this->uncompressedSize < $this->exactSize)
298+
) {
299+
$readLength = min(
300+
($this->maxSize ?? PHP_INT_MAX) - $this->uncompressedSize,
301+
($this->exactSize ?? PHP_INT_MAX) - $this->uncompressedSize,
302+
self::CHUNKED_READ_BLOCK_SIZE
303+
);
256304

257305
$data = fread($this->stream, $readLength);
258306

@@ -272,10 +320,13 @@ private function readStream(bool $send): void
272320

273321
if ($send) {
274322
($this->send)($data);
275-
$this->totalSize += strlen($data);
276323
}
277324
}
278325

326+
if ($this->exactSize && $this->uncompressedSize !== $this->exactSize) {
327+
throw new FileSizeIncorrectException(expectedSize: $this->exactSize, actualSize: $this->uncompressedSize);
328+
}
329+
279330
$this->crc = hexdec(hash_final($hash));
280331
}
281332

@@ -334,4 +385,9 @@ private function getCdrFile(): string
334385
: $this->startOffset,
335386
);
336387
}
388+
389+
private function isSimulation(): bool
390+
{
391+
return $this->operationMode === OperationMode::SIMULATE_LAX || $this->operationMode === OperationMode::SIMULATE_STRICT;
392+
}
337393
}

src/OperationMode.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZipStream;
6+
7+
/**
8+
* ZipStream execution operation modes
9+
*/
10+
enum OperationMode
11+
{
12+
/**
13+
* Stream file into output stream
14+
*/
15+
case NORMAL;
16+
17+
/**
18+
* Simulate the zip to figure out the resulting file size
19+
*
20+
* This only supports entries where the file size is known beforehand and
21+
* deflation is disabled.
22+
*/
23+
case SIMULATE_STRICT;
24+
25+
/**
26+
* Simulate the zip to figure out the resulting file size
27+
*
28+
* If the file size is not known beforehand or deflation is enabled, the
29+
* entry streams will be read and rewound.
30+
*
31+
* If the entry does not support rewinding either, you will not be able to
32+
* use the same stream in a later operation mode like `NORMAL`.
33+
*/
34+
case SIMULATE_LAX;
35+
}

0 commit comments

Comments
 (0)