8
8
use DateTimeInterface ;
9
9
use DeflateContext ;
10
10
use RuntimeException ;
11
+ use ZipStream \Exception \FileSizeIncorrectException ;
11
12
use ZipStream \Exception \OverflowException ;
12
13
use ZipStream \Exception \ResourceActionException ;
14
+ use ZipStream \Exception \SimulationFileUnknownException ;
13
15
use ZipStream \Exception \StreamNotReadableException ;
14
16
use ZipStream \Exception \StreamNotSeekableException ;
15
17
@@ -32,28 +34,30 @@ class File
32
34
33
35
private readonly string $ fileName ;
34
36
35
- private int $ totalSize = 0 ;
36
-
37
37
/**
38
- * @var resource
38
+ * @var resource|null
39
39
*/
40
40
private $ stream ;
41
41
42
42
/**
43
- * @param resource $stream
43
+ * @param Closure $dataCallback
44
+ * @psalm-param Closure(): resource $dataCallback
44
45
*/
45
46
public function __construct (
46
47
string $ fileName ,
47
- private int $ startOffset ,
48
+ private readonly Closure $ dataCallback ,
49
+ private readonly OperationMode $ operationMode ,
50
+ private readonly int $ startOffset ,
48
51
private readonly CompressionMethod $ compressionMethod ,
49
52
private readonly string $ comment ,
50
53
private readonly DateTimeInterface $ lastModificationDateTime ,
51
54
private readonly int $ deflateLevel ,
52
55
private readonly ?int $ maxSize ,
56
+ private readonly ?int $ exactSize ,
53
57
private readonly bool $ enableZip64 ,
54
58
private readonly bool $ enableZeroHeader ,
55
59
private readonly Closure $ send ,
56
- $ stream ,
60
+ private readonly Closure $ recordSentBytes ,
57
61
) {
58
62
$ this ->fileName = self ::filterFilename ($ fileName );
59
63
$ this ->checkEncoding ();
@@ -63,36 +67,110 @@ public function __construct(
63
67
}
64
68
65
69
$ this ->selectVersion ();
70
+ }
71
+
72
+ public function cloneSimulationExecution (): self
73
+ {
74
+ return new self (
75
+ $ this ->fileName ,
76
+ $ this ->dataCallback ,
77
+ OperationMode::NORMAL ,
78
+ $ this ->startOffset ,
79
+ $ this ->compressionMethod ,
80
+ $ this ->comment ,
81
+ $ this ->lastModificationDateTime ,
82
+ $ this ->deflateLevel ,
83
+ $ this ->maxSize ,
84
+ $ this ->exactSize ,
85
+ $ this ->enableZip64 ,
86
+ $ this ->enableZeroHeader ,
87
+ $ this ->send ,
88
+ $ this ->recordSentBytes ,
89
+ );
90
+ }
91
+
92
+ public function process (): string
93
+ {
94
+ $ forecastSize = $ this ->forecastSize ();
95
+
96
+ if ($ this ->enableZeroHeader ) {
97
+ // No calculation required
98
+ } elseif ($ this ->isSimulation () && $ forecastSize ) {
99
+ $ this ->uncompressedSize = $ forecastSize ;
100
+ $ this ->compressedSize = $ forecastSize ;
101
+ } else {
102
+ $ this ->readStream (send: false );
103
+ if (rewind ($ this ->unpackStream ()) === false ) {
104
+ throw new ResourceActionException ('rewind ' , $ this ->unpackStream ());
105
+ }
106
+ }
107
+
108
+ $ this ->addFileHeader ();
109
+
110
+ $ detectedSize = $ forecastSize ?? $ this ->compressedSize ;
111
+
112
+ if (
113
+ $ this ->isSimulation () &&
114
+ $ detectedSize > 0
115
+ ) {
116
+ ($ this ->recordSentBytes )($ detectedSize );
117
+ } else {
118
+ $ this ->readStream (send: true );
119
+ }
120
+
121
+ $ this ->addFileFooter ();
122
+ return $ this ->getCdrFile ();
123
+ }
124
+
125
+ /**
126
+ * @return resource
127
+ */
128
+ private function unpackStream ()
129
+ {
130
+ if ($ this ->stream ) {
131
+ return $ this ->stream ;
132
+ }
133
+
134
+ if ($ this ->operationMode === OperationMode::SIMULATE_STRICT ) {
135
+ throw new SimulationFileUnknownException ();
136
+ }
137
+
138
+ $ this ->stream = ($ this ->dataCallback )();
66
139
67
- if (!$ this ->enableZeroHeader && !stream_get_meta_data ($ stream )['seekable ' ]) {
140
+ if (!$ this ->enableZeroHeader && !stream_get_meta_data ($ this -> stream )['seekable ' ]) {
68
141
throw new StreamNotSeekableException ();
69
142
}
70
143
if (!(
71
- str_contains (stream_get_meta_data ($ stream )['mode ' ], 'r ' )
72
- || str_contains (stream_get_meta_data ($ stream )['mode ' ], 'w+ ' )
73
- || str_contains (stream_get_meta_data ($ stream )['mode ' ], 'a+ ' )
74
- || str_contains (stream_get_meta_data ($ stream )['mode ' ], 'x+ ' )
75
- || str_contains (stream_get_meta_data ($ stream )['mode ' ], 'c+ ' )
144
+ str_contains (stream_get_meta_data ($ this -> stream )['mode ' ], 'r ' )
145
+ || str_contains (stream_get_meta_data ($ this -> stream )['mode ' ], 'w+ ' )
146
+ || str_contains (stream_get_meta_data ($ this -> stream )['mode ' ], 'a+ ' )
147
+ || str_contains (stream_get_meta_data ($ this -> stream )['mode ' ], 'x+ ' )
148
+ || str_contains (stream_get_meta_data ($ this -> stream )['mode ' ], 'c+ ' )
76
149
)) {
77
150
throw new StreamNotReadableException ();
78
151
}
79
- $ this ->stream = $ stream ;
152
+
153
+ return $ this ->stream ;
80
154
}
81
155
82
- public function process (): string
156
+ private function forecastSize (): ? int
83
157
{
84
- if (!$ this ->enableZeroHeader ) {
85
- $ this ->readStream (send: false );
86
- if (rewind ($ this ->stream ) === false ) {
87
- throw new ResourceActionException ('rewind ' , $ this ->stream );
88
- }
158
+ if ($ this ->compressionMethod !== CompressionMethod::STORE ) {
159
+ return null ;
160
+ }
161
+ if ($ this ->exactSize ) {
162
+ return $ this ->exactSize ;
163
+ }
164
+ $ fstat = fstat ($ this ->unpackStream ());
165
+ if (!$ fstat || !array_key_exists ('size ' , $ fstat ) || $ fstat ['size ' ] < 1 ) {
166
+ return null ;
89
167
}
90
168
91
- $ this ->addFileHeader ();
92
- $ this ->readStream (send: true ) ;
93
- $ this -> addFileFooter ();
169
+ if ( $ this ->maxSize !== null && $ this -> maxSize < $ fstat [ ' size ' ]) {
170
+ return $ this ->maxSize ;
171
+ }
94
172
95
- return $ this -> getCdrFile () ;
173
+ return $ fstat [ ' size ' ] ;
96
174
}
97
175
98
176
/**
@@ -127,8 +205,6 @@ private function addFileHeader(): void
127
205
128
206
129
207
($ this ->send )($ data );
130
-
131
- $ this ->totalSize += strlen ($ data );
132
208
}
133
209
134
210
/**
@@ -239,8 +315,6 @@ private function addFileFooter(): void
239
315
}
240
316
241
317
($ this ->send )($ footer );
242
-
243
- $ this ->totalSize += strlen ($ footer );
244
318
}
245
319
246
320
private function readStream (bool $ send ): void
@@ -251,10 +325,18 @@ private function readStream(bool $send): void
251
325
252
326
$ deflate = $ this ->compressionInit ();
253
327
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 );
328
+ while (
329
+ !feof ($ this ->unpackStream ()) &&
330
+ ($ this ->maxSize === null || $ this ->uncompressedSize < $ this ->maxSize ) &&
331
+ ($ this ->exactSize === null || $ this ->uncompressedSize < $ this ->exactSize )
332
+ ) {
333
+ $ readLength = min (
334
+ ($ this ->maxSize ?? PHP_INT_MAX ) - $ this ->uncompressedSize ,
335
+ ($ this ->exactSize ?? PHP_INT_MAX ) - $ this ->uncompressedSize ,
336
+ self ::CHUNKED_READ_BLOCK_SIZE
337
+ );
256
338
257
- $ data = fread ($ this ->stream , $ readLength );
339
+ $ data = fread ($ this ->unpackStream () , $ readLength );
258
340
259
341
hash_update ($ hash , $ data );
260
342
@@ -264,18 +346,21 @@ private function readStream(bool $send): void
264
346
$ data = deflate_add (
265
347
$ deflate ,
266
348
$ data ,
267
- feof ($ this ->stream ) ? ZLIB_FINISH : ZLIB_NO_FLUSH
349
+ feof ($ this ->unpackStream () ) ? ZLIB_FINISH : ZLIB_NO_FLUSH
268
350
);
269
351
}
270
352
271
353
$ this ->compressedSize += strlen ($ data );
272
354
273
355
if ($ send ) {
274
356
($ this ->send )($ data );
275
- $ this ->totalSize += strlen ($ data );
276
357
}
277
358
}
278
359
360
+ if ($ this ->exactSize && $ this ->uncompressedSize !== $ this ->exactSize ) {
361
+ throw new FileSizeIncorrectException (expectedSize: $ this ->exactSize , actualSize: $ this ->uncompressedSize );
362
+ }
363
+
279
364
$ this ->crc = hexdec (hash_final ($ hash ));
280
365
}
281
366
@@ -334,4 +419,9 @@ private function getCdrFile(): string
334
419
: $ this ->startOffset ,
335
420
);
336
421
}
422
+
423
+ private function isSimulation (): bool
424
+ {
425
+ return $ this ->operationMode === OperationMode::SIMULATE_LAX || $ this ->operationMode === OperationMode::SIMULATE_STRICT ;
426
+ }
337
427
}
0 commit comments