1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace morgue\zip; |
4
|
|
|
|
5
|
|
|
use morgue\archive\ArchiveEntry; |
6
|
|
|
|
7
|
|
|
final class LocalFileHeader |
8
|
|
|
{ |
9
|
|
|
const SIGNATURE = 0x504b0304; |
10
|
|
|
|
11
|
|
|
/// Minimum length of this entry if neither file name not extra field are set |
12
|
|
|
const MIN_LENGTH = 30; |
13
|
|
|
|
14
|
|
|
/// Maximum length of this entry file name and extra field have the maximum allowed length |
15
|
|
|
const MAX_LENGTH = self::MIN_LENGTH + self::FILE_NAME_MAX_LENGTH + self::EXTRA_FIELD_MAX_LENGTH; |
16
|
|
|
|
17
|
|
|
/// File name can not be longer than this (the length field has only 2 bytes) |
18
|
|
|
const FILE_NAME_MAX_LENGTH = (255 * 255) - 1; |
19
|
|
|
|
20
|
|
|
/// Extra field can not be longer than this (the length field has only 2 bytes) |
21
|
|
|
const EXTRA_FIELD_MAX_LENGTH = (255 * 255) - 1; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var int |
25
|
|
|
*/ |
26
|
|
|
private $versionNeededToExtract; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var int |
30
|
|
|
*/ |
31
|
|
|
private $generalPurposeBitFlags; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var int |
35
|
|
|
*/ |
36
|
|
|
private $compressionMethod; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var int |
40
|
|
|
*/ |
41
|
|
|
private $lastModificationFileDate; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var int |
45
|
|
|
*/ |
46
|
|
|
private $lastModificationFileTime; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var \DateTimeInterface |
50
|
|
|
*/ |
51
|
|
|
private $lastModification; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var int |
55
|
|
|
*/ |
56
|
|
|
private $crc32; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var int |
60
|
|
|
*/ |
61
|
|
|
private $compressedSize; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var int |
65
|
|
|
*/ |
66
|
|
|
private $uncompressedSize; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @var int |
70
|
|
|
*/ |
71
|
|
|
private $fileNameLength; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @var int |
75
|
|
|
*/ |
76
|
|
|
private $extraFieldLength; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @var string |
80
|
|
|
*/ |
81
|
|
|
private $fileName = ""; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @var string |
85
|
|
|
*/ |
86
|
|
|
private $extraField = ""; |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @var bool |
90
|
|
|
*/ |
91
|
|
|
private $requireAdditionalData = false; |
92
|
|
|
|
93
|
322 |
|
public function __construct( |
94
|
|
|
int $versionNeededToExtract, |
95
|
|
|
int $generalPurposeBitFlags, |
96
|
|
|
int $compressionMethod, |
97
|
|
|
int $lastModificationFileTime, |
98
|
|
|
int $lastModificationFileDate, |
99
|
|
|
int $crc32, |
100
|
|
|
int $compressedSize, |
101
|
|
|
int $uncompressedSize, |
102
|
|
|
string $fileName = null, |
103
|
|
|
string $extraField = null |
104
|
|
|
) { |
105
|
322 |
|
$this->versionNeededToExtract = $versionNeededToExtract; |
106
|
322 |
|
$this->generalPurposeBitFlags = $generalPurposeBitFlags; |
107
|
322 |
|
$this->compressionMethod = $compressionMethod; |
108
|
322 |
|
$this->lastModificationFileTime = $lastModificationFileTime; |
109
|
322 |
|
$this->lastModificationFileDate = $lastModificationFileDate; |
110
|
322 |
|
$this->crc32 = $crc32; |
111
|
322 |
|
$this->compressedSize = $compressedSize; |
112
|
322 |
|
$this->uncompressedSize = $uncompressedSize; |
113
|
|
|
|
114
|
322 |
|
if ($fileName !== null) { |
115
|
|
|
$this->fileName = $fileName; |
116
|
|
|
$this->fileNameLength = \strlen($this->fileName); |
117
|
|
|
} |
118
|
322 |
|
if ($extraField !== null) { |
119
|
|
|
$this->extraField = $extraField; |
120
|
|
|
$this->extraFieldLength = \strlen($this->extraField); |
121
|
|
|
} |
122
|
322 |
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Create the binary on disk representation |
126
|
|
|
* |
127
|
|
|
* @return string |
128
|
|
|
*/ |
129
|
|
|
public function marshal() : string |
130
|
|
|
{ |
131
|
|
|
return \pack( |
132
|
|
|
'NvvvvvVVVvv', |
133
|
|
|
self::SIGNATURE, |
134
|
|
|
$this->versionNeededToExtract, |
135
|
|
|
$this->generalPurposeBitFlags, |
136
|
|
|
$this->compressionMethod, |
137
|
|
|
$this->lastModificationFileTime, |
138
|
|
|
$this->lastModificationFileDate, |
139
|
|
|
$this->crc32, |
140
|
|
|
$this->compressedSize, |
141
|
|
|
$this->uncompressedSize, |
142
|
|
|
\strlen($this->fileName), |
143
|
|
|
\strlen($this->extraField) |
144
|
|
|
) |
145
|
|
|
. $this->fileName |
146
|
|
|
. $this->extraField |
147
|
|
|
; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Parse the local file header from a binary string. |
152
|
|
|
* Check $requireAdditionalData to check if parseAdditionalData() must be called to parse additional fields. |
153
|
|
|
* |
154
|
|
|
* @param string $input |
155
|
|
|
* @param int $offset Start at this position inside the string |
156
|
|
|
* @return static |
157
|
|
|
*/ |
158
|
321 |
|
public static function parse(string $input, int $offset = 0) |
159
|
|
|
{ |
160
|
321 |
|
if (\strlen($input) < ($offset+self::MIN_LENGTH)) { |
161
|
|
|
throw new \InvalidArgumentException("Not enough data to parse local file header!"); |
162
|
|
|
} |
163
|
|
|
|
164
|
321 |
|
$parsed = \unpack( |
165
|
|
|
'Nsignature' |
166
|
|
|
. '/vversionNeededToExtract' |
167
|
|
|
. '/vgeneralPurposeBitFlags' |
168
|
|
|
. '/vcompressionMethod' |
169
|
|
|
. '/vlastModificationFileTime' |
170
|
|
|
. '/vlastModificationFileDate' |
171
|
|
|
. '/Vcrc32' |
172
|
|
|
. '/VcompressedSize' |
173
|
|
|
. '/VuncompressedSize' |
174
|
|
|
. '/vfileNameLength' |
175
|
321 |
|
. '/vextraFieldLength', |
176
|
321 |
|
($offset ? \substr($input, $offset) : $input) |
177
|
|
|
); |
178
|
321 |
|
if ($parsed['signature'] !== self::SIGNATURE) { |
179
|
|
|
throw new \InvalidArgumentException("Invalid signature for local file header!"); |
180
|
|
|
} |
181
|
|
|
|
182
|
321 |
|
$localFileHeader = new static( |
183
|
321 |
|
$parsed['versionNeededToExtract'], |
184
|
321 |
|
$parsed['generalPurposeBitFlags'], |
185
|
321 |
|
$parsed['compressionMethod'], |
186
|
321 |
|
$parsed['lastModificationFileTime'], |
187
|
321 |
|
$parsed['lastModificationFileDate'], |
188
|
321 |
|
$parsed['crc32'], |
189
|
321 |
|
$parsed['compressedSize'], |
190
|
321 |
|
$parsed['uncompressedSize'] |
191
|
|
|
); |
192
|
321 |
|
$localFileHeader->fileNameLength = $parsed['fileNameLength']; |
193
|
321 |
|
$localFileHeader->extraFieldLength = $parsed['extraFieldLength']; |
194
|
321 |
|
$localFileHeader->requireAdditionalData = $localFileHeader->fileNameLength + $localFileHeader->extraFieldLength; |
195
|
|
|
|
196
|
321 |
|
return $localFileHeader; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* After a new object has been created by parse(), this method must be called to initialize the file name and extra field entries which have dynamic field length. |
201
|
|
|
* The required number of bytes is written to the $requireAdditionalData attribute by parse(). |
202
|
|
|
* |
203
|
|
|
* @param string $input |
204
|
|
|
* @param int $offset |
205
|
|
|
* @return int |
206
|
|
|
*/ |
207
|
321 |
|
public function parseAdditionalData(string $input, int $offset = 0) : int |
208
|
|
|
{ |
209
|
321 |
|
if (!$this->requireAdditionalData) { |
210
|
|
|
throw new \BadMethodCallException("No additional data required!"); |
211
|
|
|
} |
212
|
|
|
|
213
|
321 |
|
if (\strlen($input) < ($offset + $this->fileNameLength + $this->extraFieldLength)) { |
214
|
|
|
throw new \InvalidArgumentException("Not enough input to parse additional data!"); |
215
|
|
|
} |
216
|
|
|
|
217
|
321 |
|
$this->fileName = \substr($input, $offset, $this->fileNameLength); |
218
|
321 |
|
$this->extraField = bin2hex(\substr($input, $offset+$this->fileNameLength, $this->extraFieldLength)); |
219
|
321 |
|
$this->requireAdditionalData = null; |
220
|
|
|
|
221
|
321 |
|
return $this->fileNameLength + $this->extraFieldLength; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Initialize a new local file header from the supplied archive entry object |
226
|
|
|
* |
227
|
|
|
* @param ArchiveEntry $archiveEntry |
228
|
|
|
* @return LocalFileHeader |
229
|
|
|
*/ |
230
|
|
|
public static function createFromArchiveEntry(ArchiveEntry $archiveEntry) : self |
231
|
|
|
{ |
232
|
|
|
list($modificationTime, $modificationDate) = dateTime2Dos($archiveEntry->getModificationTime()); |
233
|
|
|
|
234
|
|
|
return new self( |
235
|
|
|
0, |
236
|
|
|
0, |
237
|
|
|
COMPRESSION_METHOD_REVERSE_MAPPING[$archiveEntry->getTargetCompressionMethod()], |
238
|
|
|
$modificationTime, |
239
|
|
|
$modificationDate, |
240
|
|
|
$archiveEntry->getChecksumCrc32(), |
241
|
|
|
$archiveEntry->getTargetSize(), |
242
|
|
|
$archiveEntry->getUncompressedSize(), |
243
|
|
|
$archiveEntry->getName(), |
244
|
|
|
null |
245
|
|
|
); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* The number of bytes the fields with variable length require. |
250
|
|
|
* |
251
|
|
|
* @return int |
252
|
|
|
*/ |
253
|
321 |
|
public function getVariableLength(): int |
254
|
|
|
{ |
255
|
321 |
|
return $this->fileNameLength + $this->extraFieldLength; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* @return int |
260
|
|
|
*/ |
261
|
1 |
|
public function getVersionNeededToExtract(): int |
262
|
|
|
{ |
263
|
1 |
|
return $this->versionNeededToExtract; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param int $versionNeededToExtract |
268
|
|
|
* @return LocalFileHeader |
269
|
|
|
*/ |
270
|
1 |
|
public function setVersionNeededToExtract(int $versionNeededToExtract): LocalFileHeader |
271
|
|
|
{ |
272
|
1 |
|
$obj = clone $this; |
273
|
1 |
|
$obj->versionNeededToExtract = $versionNeededToExtract; |
274
|
1 |
|
return $obj; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* @return int |
279
|
|
|
*/ |
280
|
1 |
|
public function getGeneralPurposeBitFlags(): int |
281
|
|
|
{ |
282
|
1 |
|
return $this->generalPurposeBitFlags; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* @param int $generalPurposeBitFlags |
287
|
|
|
* @return LocalFileHeader |
288
|
|
|
*/ |
289
|
1 |
|
public function setGeneralPurposeBitFlags(int $generalPurposeBitFlags): LocalFileHeader |
290
|
|
|
{ |
291
|
1 |
|
$obj = clone $this; |
292
|
1 |
|
$obj->generalPurposeBitFlags = $generalPurposeBitFlags; |
293
|
1 |
|
return $obj; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* @return int |
298
|
|
|
*/ |
299
|
1 |
|
public function getCompressionMethod(): int |
300
|
|
|
{ |
301
|
1 |
|
return $this->compressionMethod; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* @param int $compressionMethod |
306
|
|
|
* @return LocalFileHeader |
307
|
|
|
*/ |
308
|
1 |
|
public function setCompressionMethod(int $compressionMethod): LocalFileHeader |
309
|
|
|
{ |
310
|
1 |
|
$obj = clone $this; |
311
|
1 |
|
$obj->compressionMethod = $compressionMethod; |
312
|
1 |
|
return $obj; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* @return int |
317
|
|
|
*/ |
318
|
1 |
|
public function getLastModificationFileDate(): int |
319
|
|
|
{ |
320
|
1 |
|
return $this->lastModificationFileDate; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* @param int $lastModificationFileDate |
325
|
|
|
* @return LocalFileHeader |
326
|
|
|
*/ |
327
|
1 |
|
public function setLastModificationFileDate(int $lastModificationFileDate): LocalFileHeader |
328
|
|
|
{ |
329
|
1 |
|
$obj = clone $this; |
330
|
1 |
|
$obj->lastModificationFileDate = $lastModificationFileDate; |
331
|
1 |
|
return $obj; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* @return int |
336
|
|
|
*/ |
337
|
1 |
|
public function getLastModificationFileTime(): int |
338
|
|
|
{ |
339
|
1 |
|
return $this->lastModificationFileTime; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* @param int $lastModificationFileTime |
344
|
|
|
* @return LocalFileHeader |
345
|
|
|
*/ |
346
|
1 |
|
public function setLastModificationFileTime(int $lastModificationFileTime): LocalFileHeader |
347
|
|
|
{ |
348
|
1 |
|
$obj = clone $this; |
349
|
1 |
|
$obj->lastModificationFileTime = $lastModificationFileTime; |
350
|
1 |
|
return $obj; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* @return \DateTimeInterface |
355
|
|
|
* @throws \Exception |
356
|
|
|
*/ |
357
|
|
|
public function getLastModification(): \DateTimeInterface |
358
|
|
|
{ |
359
|
|
|
return dos2DateTime($this->lastModificationFileTime, $this->lastModificationFileDate); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* @param \DateTimeInterface $lastModification |
364
|
|
|
* @return LocalFileHeader |
365
|
|
|
*/ |
366
|
|
|
public function setLastModification(\DateTimeInterface $lastModification): LocalFileHeader |
367
|
|
|
{ |
368
|
|
|
$obj = clone $this; |
369
|
|
|
list($obj->lastModificationFileTime, $obj->lastModificationFileDate) = dateTime2Dos($lastModification); |
370
|
|
|
return $obj; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* @return int |
375
|
|
|
*/ |
376
|
1 |
|
public function getCrc32(): int |
377
|
|
|
{ |
378
|
1 |
|
return $this->crc32; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* @param int $crc32 |
383
|
|
|
* @return LocalFileHeader |
384
|
|
|
*/ |
385
|
1 |
|
public function setCrc32(int $crc32): LocalFileHeader |
386
|
|
|
{ |
387
|
1 |
|
$obj = clone $this; |
388
|
1 |
|
$obj->crc32 = $crc32; |
389
|
1 |
|
return $obj; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* @return int |
394
|
|
|
*/ |
395
|
1 |
|
public function getCompressedSize(): int |
396
|
|
|
{ |
397
|
1 |
|
return $this->compressedSize; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* @param int $compressedSize |
402
|
|
|
* @return LocalFileHeader |
403
|
|
|
*/ |
404
|
1 |
|
public function setCompressedSize(int $compressedSize): LocalFileHeader |
405
|
|
|
{ |
406
|
1 |
|
$obj = clone $this; |
407
|
1 |
|
$obj->compressedSize = $compressedSize; |
408
|
1 |
|
return $obj; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* @return int |
413
|
|
|
*/ |
414
|
1 |
|
public function getUncompressedSize(): int |
415
|
|
|
{ |
416
|
1 |
|
return $this->uncompressedSize; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* @param int $uncompressedSize |
421
|
|
|
* @return LocalFileHeader |
422
|
|
|
*/ |
423
|
1 |
|
public function setUncompressedSize(int $uncompressedSize): LocalFileHeader |
424
|
|
|
{ |
425
|
1 |
|
$obj = clone $this; |
426
|
1 |
|
$obj->uncompressedSize = $uncompressedSize; |
427
|
1 |
|
return $obj; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* @return int |
432
|
|
|
*/ |
433
|
1 |
|
public function getFileNameLength(): int |
434
|
|
|
{ |
435
|
1 |
|
return $this->fileNameLength; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* @return int |
440
|
|
|
*/ |
441
|
1 |
|
public function getExtraFieldLength(): int |
442
|
|
|
{ |
443
|
1 |
|
return $this->extraFieldLength; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
/** |
447
|
|
|
* @return string |
448
|
|
|
*/ |
449
|
1 |
|
public function getFileName(): string |
450
|
|
|
{ |
451
|
1 |
|
return $this->fileName; |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
/** |
455
|
|
|
* @param string $fileName |
456
|
|
|
* @return LocalFileHeader |
457
|
|
|
*/ |
458
|
1 |
View Code Duplication |
public function setFileName(string $fileName): LocalFileHeader |
|
|
|
|
459
|
|
|
{ |
460
|
1 |
|
$obj = clone $this; |
461
|
1 |
|
$obj->fileName = $fileName; |
462
|
1 |
|
$obj->fileNameLength = \strlen($fileName); |
463
|
1 |
|
return $obj; |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* @return string |
468
|
|
|
*/ |
469
|
1 |
|
public function getExtraField(): string |
470
|
|
|
{ |
471
|
1 |
|
return $this->extraField; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* @param string $extraField |
476
|
|
|
* @return LocalFileHeader |
477
|
|
|
*/ |
478
|
1 |
View Code Duplication |
public function setExtraField(string $extraField): LocalFileHeader |
|
|
|
|
479
|
|
|
{ |
480
|
1 |
|
$obj = clone $this; |
481
|
1 |
|
$obj->extraField = $extraField; |
482
|
1 |
|
$obj->extraFieldLength = \strlen($extraField); |
483
|
1 |
|
return $obj; |
484
|
|
|
} |
485
|
|
|
} |
486
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.