PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.8.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.8.1
4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Backup / FileHeader.php
wp-staging / Backup Last commit date
Ajax 1 month ago BackgroundProcessing 1 year ago Dto 1 month ago Entity 4 months ago Exceptions 1 year ago FileHeader 1 month ago Interfaces 6 months ago Job 1 month ago Request 1 year ago Service 1 month ago Storage 1 month ago Task 1 month ago Traits 10 months ago Utils 3 months ago AfterRestore.php 5 months ago BackupDeleter.php 2 months ago BackupDownload.php 8 months ago BackupFileIndex.php 1 year ago BackupGlitchReason.php 1 year ago BackupHeader.php 1 month ago BackupRepairer.php 6 months ago BackupRetentionHandler.php 1 month ago BackupScheduler.php 1 month ago BackupServiceProvider.php 1 month ago BackupValidator.php 6 months ago FileHeader.php 1 month ago FileHeaderAttribute.php 2 years ago WithBackupIdentifier.php 1 year ago
FileHeader.php
774 lines
1 <?php
2
3 namespace WPStaging\Backup;
4
5 use WPStaging\Backup\FileHeader\ExtraFieldCodec;
6 use WPStaging\Backup\FileHeader\ExtraFieldType;
7 use WPStaging\Backup\Interfaces\IndexLineInterface;
8 use WPStaging\Backup\Traits\EncodingErrorHandler;
9 use WPStaging\Framework\Filesystem\PathIdentifier;
10 use WPStaging\Framework\Job\Exception\FileValidationException;
11 use WPStaging\Framework\Traits\EndOfLinePlaceholderTrait;
12 use WPStaging\Framework\Traits\FormatTrait;
13 use WPStaging\Framework\Utils\DataEncoder;
14
15 class FileHeader implements IndexLineInterface
16 {
17 use EndOfLinePlaceholderTrait;
18 use FormatTrait;
19 use EncodingErrorHandler;
20
21 /**
22 * Packed Hex Code of `WPSTG`
23 * This constant represents an 48bit unsigned integer packed as a hex string.
24 * It is appended as it is to the start of the file header.
25 *
26 * Example:
27 * $hex = '47f6600b0200';
28 * to make it 8 bytes
29 * $hex = $hex . '0000';
30 * $bin = hex2bin($hex);
31 * $int = unpack('P', $bin)[1];
32 * echo $int; //8780838471 the original string
33 * 87 -> W
34 * 80 -> P
35 * 83 -> S
36 * 84 -> T
37 * 71 -> G
38 * @var string
39 */
40 const START_SIGNATURE = '47f6600b0200';
41
42 /** @var int */
43 const FILE_HEADER_FIXED_SIZE = 72;
44
45 /** @var int */
46 const INDEX_HEADER_FIXED_SIZE = 72;
47
48 /**
49 * @var string
50 * The File Header format without the start signature to make it compatible with 32bit PHP.
51 * This is a format string used with the DataEncoder class to encode/decode binary data. The format represents how integer values are packed into a binary header structure for backup files. Each character pair in the
52 * string represents the bit size of each field in the header (4=32bit, 5=40bit, 2=16bit, etc.), used when encoding arrays of integers into hexadecimal format for the file header.
53 *
54 * Example:
55 * $format = '44552424';
56 * $intArray = [123456789, 123456789, 123456789, 123456789];
57 * $hex = $encoder->intArrayToHex($format, $intArray); */
58 const FILE_HEADER_FORMAT = '44552424';
59
60 /** @var string */
61 const INDEX_HEADER_FORMAT = '644552424';
62
63 /** @var string */
64 const CRC32_CHECKSUM_ALGO = 'crc32b';
65
66 /** @var string */
67 private $startSignature;
68
69 /** @var int */
70 private $modifiedTime;
71
72 /** @var string */
73 private $crc32Checksum;
74
75 /** @var int */
76 private $crc32;
77
78 /** @var int */
79 private $compressedSize;
80
81 /** @var int */
82 private $uncompressedSize;
83
84 /** @var int */
85 private $attributes;
86
87 /** @var int */
88 private $extraFieldLength;
89
90 /** @var int */
91 private $fileNameLength;
92
93 /** @var int */
94 private $filePathLength;
95
96 /** @var int */
97 private $startOffset;
98
99 /** @var string */
100 private $filePath;
101
102 /** @var string */
103 private $fileName;
104
105 /** @var string */
106 private $extraField;
107
108 /** @var DataEncoder */
109 private $encoder;
110
111 private $pathIdentifier;
112
113 public function __construct(DataEncoder $encoder, PathIdentifier $pathIdentifier)
114 {
115 $this->encoder = $encoder;
116 $this->pathIdentifier = $pathIdentifier;
117 $this->resetHeader();
118 }
119
120 /**
121 * Log encoding errors with context about the file being processed
122 *
123 * @param string $method The method where the error occurred
124 * @param string $errorMessage The error message from DataEncoder
125 * @return void
126 */
127 private function logEncodingError(string $method, string $errorMessage)
128 {
129 $fileName = $this->getIdentifiablePath();
130 $context = [
131 'file' => $fileName ?: 'unknown',
132 'method' => $method,
133 'modifiedTime' => $this->modifiedTime,
134 'crc32' => $this->crc32,
135 'compressedSize' => $this->compressedSize,
136 'uncompressedSize' => $this->uncompressedSize,
137 'attributes' => $this->attributes,
138 ];
139
140 $logMessageTemplate = 'DataEncoder error in %s for file "' . ($fileName ?: 'unknown') .
141 '": %s. Using fallback values to continue backup.';
142
143 $this->logEncodingErrorWithContext($errorMessage, $context, $logMessageTemplate);
144 }
145
146 /**
147 * Apply fallback values for null properties to allow backup to continue
148 *
149 * @return void
150 */
151 private function applyFallbackValues()
152 {
153 // Apply fallback values for properties that might be null
154 if ($this->modifiedTime === null) {
155 $this->modifiedTime = time(); // Use current time as fallback
156 }
157
158 if ($this->crc32 === null) {
159 $this->crc32 = 0; // Use 0 as fallback for CRC32
160 }
161
162 if ($this->compressedSize === null) {
163 $this->compressedSize = 0; // Use 0 as fallback
164 }
165
166 if ($this->uncompressedSize === null) {
167 $this->uncompressedSize = 0; // Use 0 as fallback
168 }
169
170 if ($this->attributes === null) {
171 $this->attributes = 0; // Use 0 as fallback
172 }
173
174 if ($this->startOffset === null) {
175 $this->startOffset = 0; // Use 0 as fallback
176 }
177
178 // Ensure string lengths are not null
179 if ($this->filePathLength === null) {
180 $this->filePathLength = strlen($this->filePath ?: '');
181 }
182
183 if ($this->fileNameLength === null) {
184 $this->fileNameLength = strlen($this->fileName ?: '');
185 }
186
187 if ($this->extraFieldLength === null) {
188 $this->extraFieldLength = strlen($this->extraField ?: '');
189 }
190 }
191
192 /**
193 * Helper method to safely encode integer array with error handling and fallback values
194 *
195 * @param string $format The format string for encoding
196 * @param array $intArray The array of integers to encode
197 * @param string $method The calling method name for logging
198 * @return string The encoded hex string
199 */
200 private function encodeIntArrayToHex(string $format, array $intArray, string $method): string
201 {
202 try {
203 return $this->encoder->intArrayToHex($format, $intArray);
204 } catch (\InvalidArgumentException $e) {
205 // Log the error with context about which file is causing the issue
206 $this->logEncodingError($method, $e->getMessage());
207
208 // Use fallback values to allow backup to continue
209 $this->applyFallbackValues();
210
211 // Rebuild the array with current property values after fallback application
212 if ($method === 'getFileHeader' || $method === 'getUncompressedFileHeader') {
213 $fallbackArray = [
214 $this->modifiedTime,
215 $this->crc32,
216 $this->compressedSize,
217 $this->uncompressedSize,
218 $this->attributes,
219 $this->filePathLength,
220 $this->fileNameLength,
221 $this->extraFieldLength,
222 ];
223 } elseif ($method === 'getIndexHeader') {
224 $fallbackArray = [
225 $this->startOffset,
226 $this->modifiedTime,
227 $this->crc32,
228 $this->compressedSize,
229 $this->uncompressedSize,
230 $this->attributes,
231 $this->filePathLength,
232 $this->fileNameLength,
233 $this->extraFieldLength,
234 ];
235 } else {
236 // Default fallback - use the original array but replace nulls
237 $fallbackArray = $intArray;
238 foreach ($fallbackArray as $index => $value) {
239 if ($value === null) {
240 $fallbackArray[$index] = 0;
241 }
242 }
243 }
244
245 // Retry with fallback values
246 return $this->encoder->intArrayToHex($format, $fallbackArray);
247 }
248 }
249
250 /**
251 * @param string $filePath
252 * @param string $identifiablePath
253 * @return void
254 */
255 public function readFile(string $filePath, string $identifiablePath)
256 {
257 $fileInfo = new \SplFileInfo($filePath);
258 $this->setFileName($fileInfo->getFilename());
259
260 $convertedPath = $this->pathIdentifier->transformIdentifiableToPath($identifiablePath);
261 $convertedPathName = basename($convertedPath);
262
263 $path = substr($identifiablePath, 0, -strlen($convertedPathName));
264 $this->setFilePath($path);
265 $this->setExtraField("");
266 $this->setUncompressedSize($fileInfo->getSize());
267 $this->setCompressedSize($fileInfo->getSize());
268 $this->setModifiedTime($fileInfo->getMTime());
269 $this->setAttributes(0);
270 $this->setCrc32Checksum(hash_file(self::CRC32_CHECKSUM_ALGO, $filePath));
271 }
272
273 /**
274 * @param string $index
275 * @return void
276 * @throws \UnexpectedValueException
277 */
278 public function decodeFileHeader(string $index)
279 {
280 $index = $this->trimTrailingLineBreak($index);
281 $fixedHeader = substr($index, 0, self::FILE_HEADER_FIXED_SIZE);
282 $dynamicHeader = substr($index, self::FILE_HEADER_FIXED_SIZE);
283 if (strpos($fixedHeader, self::START_SIGNATURE) !== 0) {
284 throw new \UnexpectedValueException('Invalid file header');
285 }
286
287 $header = $this->encoder->hexToIntArray(self::FILE_HEADER_FORMAT, substr($fixedHeader, 12, self::FILE_HEADER_FIXED_SIZE - 12));
288 $this->setModifiedTime($header[0]);
289 $this->setCrc32($header[1]);
290 $this->setCompressedSize($header[2]);
291 $this->setUncompressedSize($header[3]);
292 $this->setAttributes($header[4]);
293 $this->filePathLength = $header[5];
294 $this->fileNameLength = $header[6];
295 $this->extraFieldLength = $header[7];
296 $this->setFilePath(substr($dynamicHeader, 0, $this->filePathLength));
297 $this->setFileName(substr($dynamicHeader, $this->filePathLength, $this->fileNameLength));
298 $this->setExtraField($this->replacePlaceholdersWithEOLs(substr($dynamicHeader, $this->filePathLength + $this->fileNameLength, $this->extraFieldLength)));
299 }
300
301 /**
302 * @param string $index
303 * @return void
304 */
305 public function decodeIndexHeader(string $index)
306 {
307 $index = $this->trimTrailingLineBreak($index);
308 $fixedHeader = substr($index, 0, self::INDEX_HEADER_FIXED_SIZE);
309 $dynamicHeader = substr($index, self::INDEX_HEADER_FIXED_SIZE);
310 $header = $this->encoder->hexToIntArray(self::INDEX_HEADER_FORMAT, $fixedHeader);
311
312 $this->setStartOffset($header[0]);
313 $this->setModifiedTime($header[1]);
314 $this->setCrc32($header[2]);
315 $this->setCompressedSize($header[3]);
316 $this->setUncompressedSize($header[4]);
317 $this->setAttributes($header[5]);
318 $this->filePathLength = $header[6];
319 $this->fileNameLength = $header[7];
320 $this->extraFieldLength = $header[8];
321 $this->setFilePath(substr($dynamicHeader, 0, $this->filePathLength));
322 $this->setFileName(substr($dynamicHeader, $this->filePathLength, $this->fileNameLength));
323 $this->setExtraField($this->replacePlaceholdersWithEOLs(substr($dynamicHeader, $this->filePathLength + $this->fileNameLength, $this->extraFieldLength)));
324 }
325
326 /**
327 * Remove only line delimiters that may be appended when reading headers line-by-line.
328 *
329 * Binary header data can legally end with bytes that rtrim() would treat as whitespace
330 * (notably "\0"), so we only strip "\n" and an optional preceding "\r".
331 */
332 private function trimTrailingLineBreak(string $line): string
333 {
334 if (substr($line, -2) === "\r\n") {
335 return substr($line, 0, -2);
336 }
337
338 if (substr($line, -1) === "\n") {
339 return substr($line, 0, -1);
340 }
341
342 return $line;
343 }
344
345 /**
346 * For compatibility with IndexLineInterface
347 * @param string $indexLine
348 * @return IndexLineInterface
349 */
350 public function readIndexLine(string $indexLine): IndexLineInterface
351 {
352 $this->decodeIndexHeader($indexLine);
353
354 return $this;
355 }
356
357 /**
358 * For compatibility with IndexLineInterface
359 * @param string $indexLine
360 * @return bool
361 */
362 public function isIndexLine(string $indexLine): bool
363 {
364 if (strlen($indexLine) <= self::INDEX_HEADER_FIXED_SIZE) {
365 return false;
366 }
367
368 return true;
369 }
370
371 public function getFileHeader(): string
372 {
373 $fixedHeader = $this->encodeIntArrayToHex(self::FILE_HEADER_FORMAT, [
374 $this->modifiedTime,
375 $this->crc32,
376 $this->compressedSize,
377 $this->uncompressedSize,
378 $this->attributes,
379 $this->filePathLength,
380 $this->fileNameLength,
381 $this->extraFieldLength,
382 ], 'getFileHeader');
383
384 $fileHeader = self::START_SIGNATURE . $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
385 $fileHeader = $this->replaceEOLsWithPlaceholders($fileHeader);
386
387 return $fileHeader;
388 }
389
390 /**
391 * Used for repairing the file content in compressed file i.e. removing header inside the content. Issue: https://github.com/wp-staging/wp-staging-pro/issues/4241
392 * @return string
393 */
394 public function getUncompressedFileHeader(): string
395 {
396 // Force current attribute to be without compression, preserving them first to restore them later
397 $oldAttributes = $this->attributes;
398 $this->setIsCompressed(false);
399
400 $fixedHeader = $this->encodeIntArrayToHex(self::FILE_HEADER_FORMAT, [
401 $this->modifiedTime,
402 $this->crc32,
403 // Usually, it refers to the compressed size, but we need to set it to the uncompressed size because we initially add the file without compression and perform compression later.
404 // This is done to remove this file header within file content through search replace
405 $this->uncompressedSize,
406 $this->uncompressedSize,
407 $this->attributes,
408 $this->filePathLength,
409 $this->fileNameLength,
410 $this->extraFieldLength,
411 ], 'getUncompressedFileHeader');
412
413 $fileHeader = self::START_SIGNATURE . $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
414 $fileHeader = $this->replaceEOLsWithPlaceholders($fileHeader);
415
416 $this->setAttributes($oldAttributes);
417
418 return $fileHeader;
419 }
420
421 public function getIndexHeader(): string
422 {
423 $fixedHeader = $this->encodeIntArrayToHex(self::INDEX_HEADER_FORMAT, [
424 $this->startOffset,
425 $this->modifiedTime,
426 $this->crc32,
427 $this->compressedSize,
428 $this->uncompressedSize,
429 $this->attributes,
430 $this->filePathLength,
431 $this->fileNameLength,
432 $this->extraFieldLength,
433 ], 'getIndexHeader');
434
435 $fixedHeader = $fixedHeader . $this->filePath . $this->fileName . $this->extraField;
436 $fixedHeader = $this->replaceEOLsWithPlaceholders($fixedHeader);
437
438 return $fixedHeader;
439 }
440
441 /**
442 * @return void
443 */
444 public function resetHeader()
445 {
446 $this->startSignature = '';
447 $this->modifiedTime = 0;
448 $this->crc32 = 0;
449 $this->crc32Checksum = '';
450 $this->compressedSize = 0;
451 $this->uncompressedSize = 0;
452 $this->setAttributes(0);
453 $this->extraFieldLength = 0;
454 $this->fileNameLength = 0;
455 $this->filePathLength = 0;
456 $this->startOffset = 0;
457 $this->filePath = '';
458 $this->fileName = '';
459 $this->extraField = '';
460 }
461
462 public function getStartSignature(): string
463 {
464 return $this->startSignature;
465 }
466
467 /**
468 * @return void
469 */
470 public function setStartSignature(string $startSignature)
471 {
472 $this->startSignature = $startSignature;
473 }
474
475 public function getModifiedTime(): int
476 {
477 return $this->modifiedTime;
478 }
479
480 /**
481 * @return void
482 */
483 public function setModifiedTime(int $modifiedTime)
484 {
485 $this->modifiedTime = $modifiedTime;
486 }
487
488 public function getCrc32(): int
489 {
490 return $this->crc32;
491 }
492
493 /**
494 * @return void
495 */
496 public function setCrc32(int $crc32)
497 {
498 $this->crc32 = $crc32;
499 $this->crc32Checksum = bin2hex(pack('N', $crc32));
500 }
501
502 public function getCrc32Checksum(): string
503 {
504 return $this->crc32Checksum;
505 }
506
507 /**
508 * @return void
509 */
510 public function setCrc32Checksum(string $crc32Checksum)
511 {
512 $this->crc32Checksum = $crc32Checksum;
513 $this->crc32 = unpack('N', pack('H*', $this->crc32Checksum))[1];
514 }
515
516 public function getCompressedSize(): int
517 {
518 return $this->compressedSize;
519 }
520
521 /**
522 * @return void
523 */
524 public function setCompressedSize(int $compressedSize)
525 {
526 $this->compressedSize = $compressedSize;
527 }
528
529 public function getUncompressedSize(): int
530 {
531 return $this->uncompressedSize;
532 }
533
534 /**
535 * @return void
536 */
537 public function setUncompressedSize(int $uncompressedSize)
538 {
539 $this->uncompressedSize = $uncompressedSize;
540 }
541
542 public function getAttributes(): int
543 {
544 return $this->attributes;
545 }
546
547 /**
548 * @return void
549 */
550 public function setAttributes(int $attributes)
551 {
552 $this->attributes = $attributes;
553 }
554
555 public function getIsCompressed(): bool
556 {
557 if ($this->attributes & FileHeaderAttribute::COMPRESSED) {
558 return true;
559 }
560
561 return false;
562 }
563
564 /**
565 * @return void
566 */
567 public function setIsCompressed(bool $isCompressed)
568 {
569 $isCompressed ?
570 $this->attributes |= FileHeaderAttribute::COMPRESSED :
571 $this->attributes &= ~FileHeaderAttribute::COMPRESSED;
572 }
573
574 public function getIsPreviousPartRequired(): bool
575 {
576 if ($this->attributes & FileHeaderAttribute::REQUIRE_PREVIOUS_PART) {
577 return true;
578 }
579
580 return false;
581 }
582
583 /**
584 * @return void
585 */
586 public function setIsPreviousPartRequired(bool $isPreviousPartRequired)
587 {
588 $isPreviousPartRequired ?
589 $this->attributes |= FileHeaderAttribute::REQUIRE_PREVIOUS_PART :
590 $this->attributes &= ~FileHeaderAttribute::REQUIRE_PREVIOUS_PART;
591 }
592
593 public function getIsNextPartRequired(): bool
594 {
595 if ($this->attributes & FileHeaderAttribute::REQUIRE_NEXT_PART) {
596 return true;
597 }
598
599 return false;
600 }
601
602 /**
603 * @return void
604 */
605 public function setIsNextPartRequired(bool $isNextPartRequired)
606 {
607 $isNextPartRequired ?
608 $this->attributes |= FileHeaderAttribute::REQUIRE_NEXT_PART :
609 $this->attributes &= ~FileHeaderAttribute::REQUIRE_NEXT_PART;
610 }
611
612 public function getStartOffset(): int
613 {
614 return $this->startOffset;
615 }
616
617 /**
618 * @return void
619 */
620 public function setStartOffset(int $startOffset)
621 {
622 $this->startOffset = $startOffset;
623 }
624
625 public function getFilePath(): string
626 {
627 return $this->filePath;
628 }
629
630 /**
631 * @return void
632 */
633 public function setFilePath(string $filePath)
634 {
635 $this->filePath = $filePath;
636 $filePathRenamed = $this->replaceEOLsWithPlaceholders($filePath);
637 $this->filePathLength = strlen($filePathRenamed);
638 }
639
640 public function getFileName(): string
641 {
642 return $this->fileName;
643 }
644
645 /**
646 * @return void
647 */
648 public function setFileName(string $fileName)
649 {
650 $this->fileName = $fileName;
651 $renamedFile = $this->replaceEOLsWithPlaceholders($fileName);
652 $this->fileNameLength = strlen($renamedFile);
653 }
654
655 public function getExtraField(): string
656 {
657 return $this->extraField;
658 }
659
660 /**
661 * @return void
662 */
663 public function setExtraField(string $extraField)
664 {
665 $this->extraField = $extraField;
666 // Length stored on the wire reflects the EOL-placeholder-encoded form, since
667 // getFileHeader/getIndexHeader run replaceEOLsWithPlaceholders over the whole
668 // concatenated header. Mirroring filePathLength/fileNameLength keeps decoders
669 // able to slice the on-disk bytes correctly when extraField contains a newline
670 // byte (e.g. random TLV value bytes).
671 $this->extraFieldLength = strlen($this->replaceEOLsWithPlaceholders($extraField));
672 }
673
674 /**
675 * Set or replace a single TLV entry inside extraField, preserving any other
676 * entries already encoded there.
677 *
678 * @param int $type One of the ExtraFieldType constants. Must not be
679 * LEGACY_RAW, which is a parser-only sentinel.
680 * @param string $value Raw bytes for this entry.
681 * @return void
682 * @throws \UnexpectedValueException When $type is LEGACY_RAW, when the
683 * current extraField holds opaque
684 * pre-TLV bytes (which a TLV write would
685 * destroy), or when the codec rejects
686 * the new entry.
687 */
688 public function setExtraFieldEntry(int $type, string $value)
689 {
690 if ($type === ExtraFieldType::LEGACY_RAW) {
691 throw new \UnexpectedValueException(sprintf('FileHeader::setExtraFieldEntry: type 0x%02X (LEGACY_RAW) is a parser-only sentinel and cannot be written.', ExtraFieldType::LEGACY_RAW));
692 }
693
694 $codec = new ExtraFieldCodec();
695 $entries = $codec->decode($this->extraField);
696 if (isset($entries[ExtraFieldType::LEGACY_RAW])) {
697 throw new \UnexpectedValueException('FileHeader::setExtraFieldEntry: refusing to add a TLV entry to a non-TLV extraField; opaque legacy bytes would be destroyed. Reset the field with setExtraField("") first if this is intentional.');
698 }
699
700 $entries[$type] = $value;
701 $this->setExtraField($codec->encode($entries));
702 }
703
704 /**
705 * Read a single TLV entry from extraField.
706 *
707 * @param int $type One of the ExtraFieldType constants.
708 * @return string|null Raw bytes, or null if the entry is not present.
709 */
710 public function getExtraFieldEntry(int $type)
711 {
712 $entries = (new ExtraFieldCodec())->decode($this->extraField);
713 return isset($entries[$type]) ? $entries[$type] : null;
714 }
715
716 public function getIdentifiablePath(): string
717 {
718 return $this->filePath . $this->fileName;
719 }
720
721 public function getDynamicHeaderLength(): int
722 {
723 return $this->filePathLength + $this->fileNameLength + $this->extraFieldLength;
724 }
725
726 public function getContentStartOffset(): int
727 {
728 return $this->startOffset + self::FILE_HEADER_FIXED_SIZE + $this->getDynamicHeaderLength() + 1;
729 }
730
731 /**
732 * @param string $filePath
733 * @param string $pathForErrorLogging
734 * @return void
735 * @throws FileValidationException
736 */
737 public function validateFile(string $filePath, string $pathForErrorLogging = '')
738 {
739 if (empty($pathForErrorLogging)) {
740 $pathForErrorLogging = $filePath;
741 }
742
743 if (!file_exists($filePath)) {
744 throw new FileValidationException(sprintf('File doesn\'t exist: %s.', $pathForErrorLogging));
745 }
746
747 $fileSize = filesize($filePath);
748 if ($this->getUncompressedSize() !== $fileSize) {
749 throw new FileValidationException(sprintf('Filesize validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->formatSize($this->getUncompressedSize(), 2), $this->formatSize($fileSize, 2)));
750 }
751
752 $crc32Checksum = hash_file(self::CRC32_CHECKSUM_ALGO, $filePath);
753 if ($this->crc32Checksum !== $crc32Checksum) {
754 throw new FileValidationException(sprintf('CRC32 Checksum validation failed for file %s. Expected: %s. Actual: %s', $pathForErrorLogging, $this->getCrc32Checksum(), $crc32Checksum));
755 }
756 }
757
758 public function toArray(): array
759 {
760 return [
761 'startOffset' => $this->getStartOffset(),
762 'modifiedTime' => $this->getModifiedTime(),
763 'crc32' => $this->getCrc32(),
764 'crc32Checksum' => $this->getCrc32Checksum(),
765 'compressedSize' => $this->getCompressedSize(),
766 'uncompressedSize' => $this->getUncompressedSize(),
767 'filePath' => $this->getFilePath(),
768 'fileName' => $this->getFileName(),
769 'extraField' => $this->getExtraField(),
770 'isCompressed' => $this->getIsCompressed(),
771 ];
772 }
773 }
774