├── phpstan.neon ├── src ├── Struct │ ├── CompressionInfoStruct.php │ ├── RarMainHeadStruct.php │ ├── RarArchiveStruct.php │ ├── RarVolumeHeaderStruct.php │ ├── RarExtTimeStruct.php │ └── RarFileHeadStruct.php ├── Flag │ ├── RarVolumeHeaderFlag.php │ └── RarFileBitFlag.php ├── RarArchive.php ├── Converter │ ├── DateTimeConverter.php │ └── BitConverter.php ├── RarFileReader.php ├── BinaryFileReader.php ├── RarEntry.php ├── Rar4FileReader.php └── Rar5FileReader.php ├── CHANGELOG.md ├── phpcs.xml ├── LICENSE ├── .github └── workflows │ └── build.yml ├── README.md ├── composer.json └── .cs.php /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src -------------------------------------------------------------------------------- /src/Struct/CompressionInfoStruct.php: -------------------------------------------------------------------------------- 1 | entries; 23 | } 24 | 25 | /** 26 | * Add entry. 27 | * 28 | * @param RarEntry $entry The rar entry 29 | * 30 | * @return self 31 | */ 32 | public function addEntry(RarEntry $entry): self 33 | { 34 | $clone = clone $this; 35 | $clone->entries[] = $entry; 36 | 37 | return $clone; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.4.0] - 2023-09-09 11 | 12 | ### Changed 13 | 14 | - Update requirements 15 | 16 | ## [0.3.0] - 2023-05-30 17 | 18 | ### Added 19 | 20 | - Add support for RAR 5.0 archive format (https://www.rarlab.com/technote.htm) 21 | - Add changelog 22 | 23 | ### Changed 24 | 25 | - Change RarEntry::$packedSize data type from float to int 26 | - Change RarEntry::$unpackedSize data type from string to int 27 | - Upgrade dependencies 28 | 29 | ### Removed 30 | 31 | - Remove support for 32-bit platforms 32 | 33 | ## [0.2.0] - 2021-01-06 34 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ./src 10 | ./tests 11 | 12 | 13 | 14 | 15 | warning 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Converter/DateTimeConverter.php: -------------------------------------------------------------------------------- 1 | > 5); 24 | $hour = (($time & 0x0F800) >> 11); 25 | 26 | $day = ($date & 0x1F); 27 | $month = (($date & 0x01E0) >> 5); 28 | $year = 1980 + (($date & 0xFE00) >> 9); 29 | 30 | return (new DateTimeImmutable())->setDate($year, $month, $day)->setTime($hour, $minute, $second); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Struct/RarVolumeHeaderStruct.php: -------------------------------------------------------------------------------- 1 | The [PECL RAR package](https://www.php.net/manual/en/book.rar.php) is **NOT** required 25 | 26 | ## Installation 27 | 28 | ``` 29 | composer require selective/rar 30 | ``` 31 | 32 | ## Usage 33 | 34 | ### Open RAR file 35 | 36 | ```php 37 | use Selective\Rar\RarFileReader; 38 | use SplFileObject; 39 | 40 | $rarFileReader = new RarFileReader(); 41 | $rarArchive = $rarFileReader->openFile(new SplFileObject('test.rar')); 42 | 43 | foreach ($rarArchive->getEntries() as $entry) { 44 | echo $entry->getName() . "\n"; 45 | } 46 | ``` 47 | 48 | ### Open in-memory RAR file 49 | 50 | ```php 51 | use Selective\Rar\RarFileReader; 52 | use SplTempFileObject; 53 | 54 | $file = new SplTempFileObject(); 55 | $file->fwrite('my binary rar content'); 56 | 57 | $rarFileReader = new RarFileReader(); 58 | $rarArchive = $rarFileReader->openFile($file); 59 | 60 | foreach ($rarArchive->getEntries() as $entry) { 61 | echo $entry->getName() . "\n"; 62 | } 63 | ``` 64 | 65 | ## License 66 | 67 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 68 | -------------------------------------------------------------------------------- /src/Struct/RarFileHeadStruct.php: -------------------------------------------------------------------------------- 1 | setUsingCache(false) 7 | ->setRiskyAllowed(true) 8 | ->setRules( 9 | [ 10 | '@PSR1' => true, 11 | '@PSR2' => true, 12 | // custom rules 13 | 'psr_autoloading' => true, 14 | 'align_multiline_comment' => ['comment_type' => 'phpdocs_only'], // psr-5 15 | 'phpdoc_to_comment' => false, 16 | 'no_superfluous_phpdoc_tags' => false, 17 | 'array_indentation' => true, 18 | 'array_syntax' => ['syntax' => 'short'], 19 | 'cast_spaces' => ['space' => 'none'], 20 | 'concat_space' => ['spacing' => 'one'], 21 | 'compact_nullable_type_declaration' => true, 22 | 'declare_equal_normalize' => ['space' => 'single'], 23 | 'general_phpdoc_annotation_remove' => [ 24 | 'annotations' => [ 25 | 'author', 26 | 'package', 27 | ], 28 | ], 29 | 'increment_style' => ['style' => 'post'], 30 | 'list_syntax' => ['syntax' => 'short'], 31 | 'echo_tag_syntax' => ['format' => 'long'], 32 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 33 | 'phpdoc_align' => false, 34 | 'phpdoc_no_empty_return' => false, 35 | 'phpdoc_order' => true, // psr-5 36 | 'phpdoc_no_useless_inheritdoc' => false, 37 | 'protected_to_private' => false, 38 | 'yoda_style' => [ 39 | 'equal' => false, 40 | 'identical' => false, 41 | 'less_and_greater' => false 42 | ], 43 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 44 | 'ordered_imports' => [ 45 | 'sort_algorithm' => 'alpha', 46 | 'imports_order' => ['class', 'const', 'function'], 47 | ], 48 | 'single_line_throw' => false, 49 | 'declare_strict_types' => false, 50 | 'blank_line_between_import_groups' => true, 51 | 'fully_qualified_strict_types' => true, 52 | 'no_null_property_initialization' => false, 53 | 'nullable_type_declaration_for_default_null_value' => false, 54 | 'operator_linebreak' => [ 55 | 'only_booleans' => true, 56 | 'position' => 'beginning', 57 | ], 58 | 'global_namespace_import' => [ 59 | 'import_classes' => true, 60 | 'import_constants' => null, 61 | 'import_functions' => null 62 | ], 63 | 'class_definition' => [ 64 | 'space_before_parenthesis' => true, 65 | ], 66 | 'trailing_comma_in_multiline' => [ 67 | 'after_heredoc' => true, 68 | 'elements' => ['array_destructuring', 'arrays', 'match'] 69 | ], 70 | 'function_declaration' => [ 71 | 'closure_fn_spacing' => 'none', 72 | ] 73 | ] 74 | ) 75 | ->setFinder( 76 | PhpCsFixer\Finder::create() 77 | ->in(__DIR__ . '/src') 78 | ->in(__DIR__ . '/tests') 79 | ->name('*.php') 80 | ->ignoreDotFiles(true) 81 | ->ignoreVCS(true) 82 | ); 83 | -------------------------------------------------------------------------------- /src/RarFileReader.php: -------------------------------------------------------------------------------- 1 | rar4 = new Rar4FileReader(); 30 | $this->rar5 = new Rar5FileReader(); 31 | } 32 | 33 | /** 34 | * Open RAR file. 35 | * 36 | * @param SplFileObject $file The rar file 37 | * 38 | * @return RarArchive The RAR archive 39 | */ 40 | public function openFile(SplFileObject $file): RarArchive 41 | { 42 | $file->rewind(); 43 | 44 | return $this->createRarArchive($this->createRarArchiveStruct($file)); 45 | } 46 | 47 | /** 48 | * Create RarArchive instance. 49 | * 50 | * @param RarArchiveStruct $rarArchiveStruct The archive struct 51 | * 52 | * @return RarArchive The RAR archive 53 | */ 54 | private function createRarArchive(RarArchiveStruct $rarArchiveStruct): RarArchive 55 | { 56 | $rarArchive = new RarArchive(); 57 | 58 | foreach ($rarArchiveStruct->files as $file) { 59 | $entry = new RarEntry(); 60 | 61 | $rarArchive = $rarArchive->addEntry( 62 | $entry 63 | ->withAttr($file->fileAttr) 64 | ->withCrc($file->fileCRC) 65 | ->withFileTime($file->fileTime) 66 | ->withHostOs($file->hostOS) 67 | ->withMethod($file->method) 68 | ->withName($file->fileName) 69 | ->withPackedSize($file->packSize) 70 | ->withUnpackedSize($file->unpackSize) 71 | ->withVersion($file->unpVer) 72 | ->withIsDirectory(false) 73 | ->withIsEncrypted(false) 74 | ); 75 | } 76 | 77 | return $rarArchive; 78 | } 79 | 80 | /** 81 | * Create struct instance. 82 | * 83 | * @param SplFileObject $file The rar file 84 | * 85 | * @return RarArchiveStruct The result 86 | */ 87 | private function createRarArchiveStruct(SplFileObject $file): RarArchiveStruct 88 | { 89 | $rarFile = new RarArchiveStruct(); 90 | 91 | $this->readSignature($file, $rarFile); 92 | 93 | if ($rarFile->version === 4) { 94 | $this->rar4->readRarFile($file, $rarFile); 95 | } 96 | 97 | if ($rarFile->version === 5) { 98 | $this->rar5->readRarFile($file, $rarFile); 99 | } 100 | 101 | return $rarFile; 102 | } 103 | 104 | private function readSignature(SplFileObject $file, RarArchiveStruct $rarFile): void 105 | { 106 | $signature = bin2hex((string)$file->fread(7)); 107 | 108 | if ($signature === '526172211a0700') { 109 | // Rar 4 signature 110 | $rarFile->version = 4; 111 | $rarFile->signature = $signature; 112 | 113 | return; 114 | } 115 | 116 | $file->fseek(0); 117 | $signature = bin2hex((string)$file->fread(8)); 118 | 119 | if ($signature === '526172211a070100') { 120 | // Rar 5 signature 121 | $rarFile->version = 5; 122 | $rarFile->signature = $signature; 123 | 124 | return; 125 | } 126 | 127 | throw new UnexpectedValueException('This is not a valid RAR file'); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/BinaryFileReader.php: -------------------------------------------------------------------------------- 1 | getBigInt((string)$file->fread(4)); 20 | } 21 | 22 | /** 23 | * Reade 2 bytes and convert to int. 24 | * 25 | * @param SplFileObject $file The file 26 | * 27 | * @return int The value 28 | */ 29 | public function readInt(SplFileObject $file): int 30 | { 31 | return $this->getInt((string)$file->fread(2)); 32 | } 33 | 34 | /** 35 | * Reade 1 byte and convert to int. 36 | * 37 | * @param SplFileObject $file The file 38 | * 39 | * @return int The value 40 | */ 41 | public function readByte(SplFileObject $file): int 42 | { 43 | return ord((string)$file->fread(1)); 44 | } 45 | 46 | public function readVint(SplFileObject $file): int 47 | { 48 | $shift = 0; 49 | $low = 0; 50 | $high = 0; 51 | $count = 1; 52 | 53 | while (!$file->eof() && $count <= 10) { 54 | $byte = ord((string)$file->fread(1)); 55 | 56 | if ($count < 5) { 57 | $low += ($byte & 0x7F) << $shift; 58 | } elseif ($count === 5) { 59 | $low += ($byte & 0x0F) << $shift; // 4 bits 60 | $high += ($byte >> 4) & 0x07; // 3 bits 61 | $shift = -4; 62 | } else { 63 | $high += ($byte & 0x7F) << $shift; 64 | } 65 | 66 | if (($byte & 0x80) === 0) { 67 | if ($low < 0) { 68 | $low += 0x100000000; 69 | } 70 | if ($high < 0) { 71 | $high += 0x100000000; 72 | } 73 | 74 | return ($high !== 0) ? $this->int64($low, $high) : $low; 75 | } 76 | 77 | $shift += 7; 78 | $count++; 79 | } 80 | 81 | return 0; 82 | } 83 | 84 | /** 85 | * Convert 2 bytes to unsigned little-endian int. 86 | * 87 | * @param string $data The data 88 | * 89 | * @return int The value 90 | */ 91 | public function getInt(string $data): int 92 | { 93 | if ($data === '') { 94 | return 0; 95 | } 96 | 97 | // 2 bytes 98 | return (int)$this->unpack('v', $data)[1]; 99 | } 100 | 101 | /** 102 | * Convert 4 bytes to unsigned little-endian big int. 103 | * 104 | * @param string $data The data 105 | * 106 | * @return int The value 107 | */ 108 | private function getBigInt(string $data): int 109 | { 110 | // 4 bytes 111 | return (int)$this->unpack('V', $data)[1]; 112 | } 113 | 114 | /** 115 | * Unpack data from binary string. 116 | * 117 | * @param string $format The format 118 | * @param string $data The data 119 | * 120 | * @throws UnexpectedValueException 121 | * 122 | * @return array The unpacked data 123 | */ 124 | private function unpack(string $format, string $data): array 125 | { 126 | if ($data === '') { 127 | return []; 128 | } 129 | 130 | $result = unpack($format, $data); 131 | 132 | if ($result === false) { 133 | throw new UnexpectedValueException('The format string contains errors'); 134 | } 135 | 136 | return (array)$result; 137 | } 138 | 139 | private function int64(int $low, int $high): int 140 | { 141 | return $low + ($high * 0x100000000); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/RarEntry.php: -------------------------------------------------------------------------------- 1 | attr; 75 | } 76 | 77 | /** 78 | * Get value. 79 | * 80 | * @return string The crc 81 | */ 82 | public function getCrc(): string 83 | { 84 | return $this->crc; 85 | } 86 | 87 | /** 88 | * Get value. 89 | * 90 | * @return DateTimeImmutable The fileTime 91 | */ 92 | public function getFileTime(): DateTimeImmutable 93 | { 94 | return $this->fileTime; 95 | } 96 | 97 | /** 98 | * Get value. 99 | * 100 | * @return int The hostOs 101 | */ 102 | public function getHostOs(): int 103 | { 104 | return $this->hostOs; 105 | } 106 | 107 | /** 108 | * Get value. 109 | * 110 | * @return int The method 111 | */ 112 | public function getMethod(): int 113 | { 114 | return $this->method; 115 | } 116 | 117 | /** 118 | * Get value. 119 | * 120 | * @return string The name 121 | */ 122 | public function getName(): string 123 | { 124 | return $this->name; 125 | } 126 | 127 | /** 128 | * Get value. 129 | * 130 | * @return float The packedSize 131 | */ 132 | public function getPackedSize(): float 133 | { 134 | return $this->packedSize; 135 | } 136 | 137 | /** 138 | * Get value. 139 | * 140 | * @return int The unpackedSize 141 | */ 142 | public function getUnpackedSize(): int 143 | { 144 | return $this->unpackedSize; 145 | } 146 | 147 | /** 148 | * Get value. 149 | * 150 | * @return int The version 151 | */ 152 | public function getVersion(): int 153 | { 154 | return $this->version; 155 | } 156 | 157 | /** 158 | * Get value. 159 | * 160 | * @return bool The isDirectory 161 | */ 162 | public function isDirectory(): bool 163 | { 164 | return $this->isDirectory; 165 | } 166 | 167 | /** 168 | * Get value. 169 | * 170 | * @return bool The isEncrypted 171 | */ 172 | public function isEncrypted(): bool 173 | { 174 | return $this->isEncrypted; 175 | } 176 | 177 | /** 178 | * Set value. 179 | * 180 | * @param int $attr The attr 181 | * 182 | * @return self 183 | */ 184 | public function withAttr(int $attr): self 185 | { 186 | $clone = clone $this; 187 | $clone->attr = $attr; 188 | 189 | return $clone; 190 | } 191 | 192 | /** 193 | * Set value. 194 | * 195 | * @param string $crc The crc 196 | * 197 | * @return self 198 | */ 199 | public function withCrc(string $crc): self 200 | { 201 | $clone = clone $this; 202 | $clone->crc = $crc; 203 | 204 | return $clone; 205 | } 206 | 207 | /** 208 | * Set value. 209 | * 210 | * @param DateTimeImmutable $fileTime The fileTime 211 | * 212 | * @return self 213 | */ 214 | public function withFileTime(DateTimeImmutable $fileTime): self 215 | { 216 | $clone = clone $this; 217 | $clone->fileTime = $fileTime; 218 | 219 | return $clone; 220 | } 221 | 222 | /** 223 | * Set value. 224 | * 225 | * @param int $hostOs The host OS 226 | * 227 | * @return self 228 | */ 229 | public function withHostOs(int $hostOs): self 230 | { 231 | $clone = clone $this; 232 | $clone->hostOs = $hostOs; 233 | 234 | return $clone; 235 | } 236 | 237 | /** 238 | * Set value. 239 | * 240 | * @param int $method The method 241 | * 242 | * @return self 243 | */ 244 | public function withMethod(int $method): self 245 | { 246 | $clone = clone $this; 247 | $clone->method = $method; 248 | 249 | return $clone; 250 | } 251 | 252 | /** 253 | * Set value. 254 | * 255 | * @param string $name The name 256 | * 257 | * @return self 258 | */ 259 | public function withName(string $name): self 260 | { 261 | $clone = clone $this; 262 | $clone->name = $name; 263 | 264 | return $clone; 265 | } 266 | 267 | /** 268 | * Set value. 269 | * 270 | * @param int $packedSize The packedSize 271 | * 272 | * @return self 273 | */ 274 | public function withPackedSize(int $packedSize): self 275 | { 276 | $clone = clone $this; 277 | $clone->packedSize = $packedSize; 278 | 279 | return $clone; 280 | } 281 | 282 | /** 283 | * Set value. 284 | * 285 | * @param int $unpackedSize The unpackedSize 286 | * 287 | * @return self 288 | */ 289 | public function withUnpackedSize(int $unpackedSize): self 290 | { 291 | $clone = clone $this; 292 | $clone->unpackedSize = $unpackedSize; 293 | 294 | return $clone; 295 | } 296 | 297 | /** 298 | * Set value. 299 | * 300 | * @param int $version The version 301 | * 302 | * @return self 303 | */ 304 | public function withVersion(int $version): self 305 | { 306 | $clone = clone $this; 307 | $clone->version = $version; 308 | 309 | return $clone; 310 | } 311 | 312 | /** 313 | * Set value. 314 | * 315 | * @param bool $isDirectory The isDirectory 316 | * 317 | * @return self 318 | */ 319 | public function withIsDirectory(bool $isDirectory): self 320 | { 321 | $clone = clone $this; 322 | $clone->isDirectory = $isDirectory; 323 | 324 | return $clone; 325 | } 326 | 327 | /** 328 | * Set value. 329 | * 330 | * @param bool $isEncrypted The isEncrypted 331 | * 332 | * @return self 333 | */ 334 | public function withIsEncrypted(bool $isEncrypted): self 335 | { 336 | $clone = clone $this; 337 | $clone->isEncrypted = $isEncrypted; 338 | 339 | return $clone; 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/Rar4FileReader.php: -------------------------------------------------------------------------------- 1 | fileReader = new BinaryFileReader(); 45 | $this->bit = new BitConverter(); 46 | $this->dateTime = new DateTimeConverter(); 47 | } 48 | 49 | public function readRarFile(SplFileObject $file, RarArchiveStruct $rarFile): void 50 | { 51 | while (!$file->eof()) { 52 | $volumeHeader = $this->readRarVolumeHeader($file, $rarFile); 53 | 54 | if ($volumeHeader->type === RarVolumeHeaderFlag::MAIN_HEAD) { 55 | $rarFile->mainHead = $this->readRarMainHead($file); 56 | $rarFile->volumeHeaders[] = $volumeHeader; 57 | 58 | // Jump to next block 59 | $file->fseek($volumeHeader->headerSize + $volumeHeader->blockSize); 60 | 61 | continue; 62 | } 63 | 64 | if ($volumeHeader->type === RarVolumeHeaderFlag::FILE_HEAD) { 65 | $rarFile->files[] = $this->readFileHead($file, $volumeHeader); 66 | continue; 67 | } 68 | if ($volumeHeader->type === RarVolumeHeaderFlag::ENDARC_HEAD) { 69 | break; 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Read file header. 76 | * 77 | * @param SplFileObject $file The file 78 | * @param RarVolumeHeaderStruct $rarVolumeHeader The header 79 | * 80 | * @return RarFileHeadStruct The result 81 | */ 82 | private function readFileHead(SplFileObject $file, RarVolumeHeaderStruct $rarVolumeHeader): RarFileHeadStruct 83 | { 84 | $fileHead = new RarFileHeadStruct(); 85 | 86 | // Compressed file size 87 | $fileHead->packSize = $rarVolumeHeader->addSize; 88 | 89 | // Lower uncompressed file size 90 | $fileHead->lowUnpackSize = $this->fileReader->readBigInt($file); 91 | 92 | // Operating system used for archiving 93 | $fileHead->hostOS = $this->fileReader->readByte($file); 94 | 95 | // File CRC 96 | $fileHead->fileCRC = strtoupper(bin2hex(strrev((string)$file->fread(4)))); 97 | 98 | // Date and time in standard MS DOS format 99 | $fileHead->fileTime = $this->dateTime->getGetDateTimeFromDosDateTime( 100 | $this->fileReader->readInt($file), 101 | $this->fileReader->readInt($file) 102 | ); 103 | 104 | // RAR version 105 | // Version number is encoded as: 10 * Major version + minor version. 106 | $fileHead->unpVer = $this->fileReader->readByte($file); 107 | 108 | // Packing method: 0x30 - 0x35 109 | $fileHead->method = $this->fileReader->readByte($file); 110 | 111 | // File name size 112 | $fileHead->nameSize = $this->fileReader->readInt($file); 113 | 114 | // File attributes 115 | $fileHead->fileAttr = $this->fileReader->readBigInt($file); 116 | 117 | // Optional value, presents only if bit 0x100 in HEAD_FLAGS is set. 118 | if ($this->bit->isFlagSet($rarVolumeHeader->flags, RarFileBitFlag::HIGH_PACK_AND_UNP_SIZE)) { 119 | // High 4 bytes of 64-bit value of compressed file size. 120 | $fileHead->highPackSize = $this->fileReader->readBigInt($file); 121 | // High 4 bytes of 64-bit value of uncompressed file size. 122 | $fileHead->highUnpackSize = $this->fileReader->readBigInt($file); 123 | } 124 | 125 | $fileHead->unpackSize = $this->bit->toInt64( 126 | (string)$fileHead->highUnpackSize, 127 | (string)$fileHead->lowUnpackSize 128 | ); 129 | 130 | // $gb = $fileHead->unpackSize / 1024 / 1024 / 1024; 131 | 132 | // File name - string of NAME_SIZE bytes size 133 | $fileHead->fileName = (string)$file->fread($fileHead->nameSize); 134 | 135 | // Optional 136 | if ($this->bit->isFlagSet($rarVolumeHeader->flags, RarFileBitFlag::HEADER_WITH_SALT)) { 137 | $fileHead->salt = (string)$file->fread(8); 138 | } 139 | 140 | // Optional 141 | if ($this->bit->isFlagSet($rarVolumeHeader->flags, RarFileBitFlag::EXTENDED_TIME_FIELD)) { 142 | $fileHead->extTime = $this->readExtTime($file, $fileHead->fileTime); 143 | } 144 | $start2 = $file->ftell(); 145 | 146 | // $packedData = (string)$file->fread($fileHead->packSize); 147 | 148 | // Jump to end of block 149 | $file->fseek($start2 + $rarVolumeHeader->addSize - 1); 150 | $headerByte = $this->fileReader->readByte($file); 151 | 152 | if ($headerByte === RarVolumeHeaderFlag::ENDARC_HEAD) { 153 | $file->fseek($start2 + $rarVolumeHeader->addSize - 3); 154 | } else { 155 | $file->fseek($start2 + $rarVolumeHeader->addSize); 156 | } 157 | 158 | return $fileHead; 159 | } 160 | 161 | /** 162 | * Read file time record. 163 | * 164 | * https://www.rarlab.com/technote.htm#timerecord 165 | * https://github.com/markokr/rarfile/blob/master/rarfile.py#L2686 166 | * https://github.com/vadmium/vadmium.github.com/blob/master/rar.md 167 | * https://github.com/larrykoubiak/minirar/blob/master/rar_time.c#L8 168 | * 169 | * @param SplFileObject $file The file 170 | * @param DateTimeImmutable $fileTime The base file time 171 | * 172 | * @return RarExtTimeStruct The result 173 | */ 174 | private function readExtTime(SplFileObject $file, DateTimeImmutable $fileTime): RarExtTimeStruct 175 | { 176 | $extTime = new RarExtTimeStruct(); 177 | 178 | // The block is in every case 5 bytes long (i.e. 0x00 0xF0 0x70 0x38 0x39 or 0x00 0xF0 0x32 0x24 0x45) 179 | 180 | // Flags and rest of data can be missing 181 | $extTime->flags = 0; 182 | if ($file->ftell() + 2 >= $file->getSize()) { 183 | return $extTime; 184 | } 185 | 186 | // 2 bytes 187 | $extTime->flags = $this->fileReader->readInt($file); 188 | 189 | if ($this->bit->isFlagSet($extTime->flags, 0x0001)) { 190 | // Timestamp 32-bit 191 | $extTime->isUnixFormat = true; 192 | } else { 193 | // Windows file time format (64-bit) 194 | $extTime->isUnixFormat = false; 195 | } 196 | 197 | $pos = (int)$file->ftell(); 198 | 199 | $extTime->mtime = $this->parseExtTime($extTime->flags >> 3 * 4, $file, $fileTime); 200 | 201 | $file->fseek($pos); 202 | $extTime->ctime = $this->parseExtTime($extTime->flags >> 2 * 4, $file); 203 | 204 | $file->fseek($pos); 205 | $extTime->atime = $this->parseExtTime($extTime->flags >> 1 * 4, $file); 206 | 207 | $file->fseek($pos); 208 | $extTime->arctime = $this->parseExtTime($extTime->flags >> 0 * 4, $file); 209 | 210 | // 2 bytes + 3 bytes = 5 bytes (fix) 211 | $file->fseek($pos + 3); 212 | 213 | return $extTime; 214 | } 215 | 216 | /** 217 | * Parse EXT_TIME to date time. 218 | * 219 | * @param int $flag The flag 220 | * @param SplFileObject $file The file 221 | * @param DateTimeImmutable|null $baseTime The base time 222 | * 223 | * @return DateTimeImmutable|null The date time or null 224 | */ 225 | private function parseExtTime( 226 | int $flag, 227 | SplFileObject $file, 228 | ?DateTimeImmutable $baseTime = null 229 | ): ?DateTimeImmutable { 230 | // Must be valid 231 | if (!$this->bit->isFlagSet($flag, 0x0008)) { 232 | return null; 233 | } 234 | 235 | if ($baseTime === null) { 236 | $baseTime = $this->dateTime->getGetDateTimeFromDosDateTime( 237 | $this->fileReader->readInt($file), 238 | $this->fileReader->readInt($file) 239 | ); 240 | } 241 | 242 | // load second fractions 243 | $reminder = 0; 244 | $count = $flag & 3; 245 | for ($i = 0; $i < $count; $i++) { 246 | $byte = $this->fileReader->readByte($file); 247 | $reminder = ($byte << 16) | ($reminder >> 8); 248 | } 249 | 250 | // Convert 100ns units to microseconds 251 | $usec = $reminder; // 10 252 | if ($usec > 1000000) { 253 | $usec = 999999; 254 | } 255 | 256 | // Dostime has room for 30 seconds only, correct if needed 257 | // 0x4000: If set, adds 1 s to the DOS time 258 | if ($flag & 4 && $baseTime->format('s') < 59) { 259 | return $baseTime->modify('+1 s'); 260 | } else { 261 | // Replace microseconds 262 | return $baseTime->modify(sprintf('+%s ms', $usec)); 263 | } 264 | } 265 | 266 | /** 267 | * Read main head. 268 | * 269 | * @param SplFileObject $file The file 270 | * 271 | * @return RarMainHeadStruct The result 272 | */ 273 | private function readRarMainHead(SplFileObject $file): RarMainHeadStruct 274 | { 275 | $mainHead = new RarMainHeadStruct(); 276 | $mainHead->highPosAv = $this->fileReader->readInt($file); 277 | $mainHead->posAv = $this->fileReader->readBigInt($file); 278 | $mainHead->encryptVer = $this->fileReader->readByte($file); 279 | 280 | return $mainHead; 281 | } 282 | 283 | /** 284 | * Read volume header. 285 | * 286 | * @param SplFileObject $file The file 287 | * @param RarArchiveStruct $rarFile 288 | * 289 | * @return RarVolumeHeaderStruct The result 290 | */ 291 | private function readRarVolumeHeader(SplFileObject $file, RarArchiveStruct $rarFile): RarVolumeHeaderStruct 292 | { 293 | $volumeHeader = new RarVolumeHeaderStruct(); 294 | 295 | $volumeHeader->crc = strtoupper(bin2hex(strrev((string)$file->fread(2)))); 296 | 297 | if ($rarFile->version === 5) { 298 | // @todo 299 | // Main archive header 300 | // The header size indicates how many total bytes the header requires 301 | $volumeHeader->size = $this->fileReader->readVint($file); 302 | $volumeHeader->type = $this->fileReader->readVint($file); 303 | 304 | return $volumeHeader; 305 | } 306 | 307 | // The header type field determines how the remaining bytes should be interpreted. 308 | $volumeHeader->type = $this->fileReader->readByte($file); 309 | $volumeHeader->flags = $this->fileReader->readInt($file); 310 | 311 | // The header size indicates how many total bytes the header requires 312 | $volumeHeader->size = $this->fileReader->readInt($file); 313 | 314 | $volumeHeader->blockSize = $volumeHeader->size; 315 | $volumeHeader->hasAdd = ($volumeHeader->flags & 0x8000) != 0; 316 | $volumeHeader->headerSize = $volumeHeader->hasAdd ? 11 : 7; 317 | $volumeHeader->bodySize = $volumeHeader->blockSize; 318 | 319 | $volumeHeader->addSize = 0; 320 | if ($volumeHeader->hasAdd) { 321 | // Compressed size 322 | $volumeHeader->addSize = $this->fileReader->readBigInt($file); 323 | $volumeHeader->bodySize = $volumeHeader->blockSize - $volumeHeader->headerSize; 324 | $volumeHeader->blockSize = $volumeHeader->size + $volumeHeader->addSize; 325 | } 326 | 327 | return $volumeHeader; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/Rar5FileReader.php: -------------------------------------------------------------------------------- 1 | fileReader = new BinaryFileReader(); 37 | $this->bit = new BitConverter(); 38 | } 39 | 40 | public function readRarFile(SplFileObject $file, RarArchiveStruct $rarFile): void 41 | { 42 | // Header type 1 43 | $this->readRar5MainArchiveHeader($file); 44 | 45 | while (!$file->eof()) { 46 | $start = (int)$file->ftell(); 47 | 48 | // File CRC uint32 49 | $file->fread(4); 50 | 51 | // Header size 52 | $this->fileReader->readVint($file); 53 | 54 | $headerType = $this->fileReader->readVint($file); 55 | if ($headerType > 5) { 56 | throw new UnexpectedValueException(sprintf('Invalid header type: %s', $headerType)); 57 | } 58 | 59 | // 0 = Main archive header 60 | // 2 = File header 61 | // 3 = Service header (CMT = comments, QO, ACL, STM, RR) 62 | if ($headerType === 2 || $headerType === 3) { 63 | $file->fseek($start); 64 | 65 | // File header and service header 66 | $fileHeader = $this->readRar5FileHeader($file); 67 | 68 | // Append only files 69 | if ($headerType === 2 && $fileHeader->isDirectory === false) { 70 | $rarFile->files[] = $fileHeader; 71 | } 72 | } 73 | 74 | // 5 = End of archive 75 | if ($headerType === 5) { 76 | // Move the offset to the end of the file 77 | $file->fseek(0, SEEK_END); 78 | break; 79 | } 80 | } 81 | } 82 | 83 | private function readRar5MainArchiveHeader(SplFileObject $file): RarVolumeHeaderStruct 84 | { 85 | // https://www.rarlab.com/technote.htm#mainhead 86 | 87 | $volumeHeader = new RarVolumeHeaderStruct(); 88 | 89 | // CRC uint32 90 | $volumeHeader->crc = strtoupper(bin2hex(strrev((string)$file->fread(4)))); 91 | 92 | // The header size indicates how many total bytes the header requires 93 | $volumeHeader->size = $this->fileReader->readVint($file); 94 | $volumeHeader->type = $this->fileReader->readVint($file); 95 | 96 | if ($volumeHeader->type != 1) { 97 | throw new UnexpectedValueException(sprintf('Invalid main archive header type: %s', $volumeHeader->type)); 98 | } 99 | 100 | // Flags common for all headers 101 | $volumeHeader->flags = $this->fileReader->readVint($file); 102 | 103 | // 0x0001 Volume. Archive is a part of multivolume set. 104 | $extraAreaSize = 0; 105 | if ($this->bit->isFlagSet($volumeHeader->flags, 0x0001)) { 106 | // Size of extra area. Optional field, present only if 0x0001 header flag is set. 107 | $extraAreaSize = $this->fileReader->readVint($file); 108 | } 109 | 110 | // Archive flags 111 | // 0x0001 Volume. Archive is a part of multi-volume set. 112 | // 0x0002 Volume number field is present. This flag is present in all volumes except first. 113 | // 0x0004 Solid archive. 114 | // 0x0008 Recovery record is present. 115 | // 0x0010 Locked archive. 116 | $archiveFlags = $this->fileReader->readVint($file); 117 | 118 | if ($this->bit->isFlagSet($archiveFlags, 0x0002)) { 119 | // Multivolume RAR archive (starts with the second file) 120 | $this->fileReader->readVint($file); 121 | } 122 | 123 | // Add offset for next block (if any) 124 | if ($extraAreaSize) { 125 | $file->fseek($extraAreaSize, SEEK_CUR); 126 | } 127 | 128 | return $volumeHeader; 129 | } 130 | 131 | private function readRar5FileHeader(SplFileObject $file): RarFileHeadStruct 132 | { 133 | $fileHeader = new RarFileHeadStruct(); 134 | 135 | $fileHeader->unpVer = 5; 136 | 137 | // CRC 138 | $file->fread(4); 139 | 140 | // Size 141 | $this->fileReader->readVint($file); 142 | 143 | // Type 144 | $this->fileReader->readVint($file); 145 | 146 | // Flags common for all headers 147 | $headerFlags = $this->fileReader->readVint($file); 148 | 149 | // Size of extra area. Optional field, present only if 0x0001 header flag is set. 150 | $extraAreaSize = 0; 151 | if ($this->bit->isFlagSet($headerFlags, 0x0001)) { 152 | $extraAreaSize = $this->fileReader->readVint($file); 153 | } 154 | 155 | // Compressed file size 156 | $fileHeader->packSize = 0; 157 | 158 | // Size of data area. Optional field, present only if 0x0002 header flag is set. 159 | // For file header this field contains the packed file size. 160 | if ($this->bit->isFlagSet($headerFlags, 0x0002)) { 161 | $fileHeader->packSize = $this->fileReader->readVint($file); 162 | } 163 | 164 | // Flags specific for these header types: 165 | // 0x0001 Directory file system object (file header only). 166 | // 0x0002 Time field in Unix format is present. 167 | // 0x0004 CRC32 field is present. 168 | // 0x0008 Unpacked size is unknown. 169 | $fileFlags = $this->fileReader->readVint($file); 170 | 171 | if ($this->bit->isFlagSet($fileFlags, 0x0001)) { 172 | // This is just a directory, not a file 173 | $fileHeader->isDirectory = true; 174 | } 175 | 176 | // Unpacked file or service data size 177 | $fileHeader->unpackSize = $this->fileReader->readVint($file); 178 | $fileHeader->lowUnpackSize = $fileHeader->unpackSize; 179 | 180 | // Operating system specific file attributes in case of file header. 181 | // Might be either used for data specific needs or just reserved and set to 0 for service header. 182 | $fileHeader->fileAttr = $this->fileReader->readVint($file); 183 | 184 | // File modification time in Unix time format. 185 | // Optional, present if 0x0002 file flag is set. 186 | if ($this->bit->isFlagSet($fileFlags, 0x0002)) { 187 | // Convert uint32 (4 bytes) to Unix timestamp 188 | $fileTimeUnix = $this->fileReader->readBigInt($file); 189 | $fileHeader->fileTime = (new DateTimeImmutable())->setTimestamp($fileTimeUnix); 190 | } 191 | 192 | // 0x0004 CRC32 field is present. 193 | if ($this->bit->isFlagSet($fileFlags, 0x0004)) { 194 | $fileHeader->fileCRC = strtoupper(bin2hex(strrev((string)$file->fread(4)))); 195 | } 196 | 197 | // compression information 198 | $compressionInfoRaw = $this->fileReader->readVint($file); 199 | $fileHeader->method = $this->parseCompressionInfo($compressionInfoRaw)->method; 200 | 201 | // 0 = Windows, 1 = Unix 202 | $fileHeader->hostOS = $this->fileReader->readVint($file); 203 | 204 | // File or service header name length 205 | $fileHeader->nameSize = $this->fileReader->readVint($file); 206 | 207 | // Variable length field containing Name length bytes in UTF-8 format without trailing zero 208 | $fileHeader->fileName = (string)$file->fread($fileHeader->nameSize); 209 | 210 | // Optional area containing additional header fields, present only if 0x0001 header flag is set 211 | if ($this->bit->isFlagSet($headerFlags, 0x0001) && $extraAreaSize) { 212 | $this->readExtraArea($file, $fileHeader, $extraAreaSize); 213 | } 214 | 215 | // Optional data area, present only if 0x0002 header flag is set. 216 | // Store file data in case of file header or service data for service header. 217 | // Depending on the compression method value in Compression information can 218 | // be either uncompressed (compression method 0) or compressed. 219 | if ($this->bit->isFlagSet($headerFlags, 0x0002) && $fileHeader->packSize) { 220 | // Move to end of compresses data 221 | $file->fseek($fileHeader->packSize, SEEK_CUR); 222 | } 223 | 224 | return $fileHeader; 225 | } 226 | 227 | private function parseCompressionInfo(int $raw): CompressionInfoStruct 228 | { 229 | $info = new CompressionInfoStruct(); 230 | 231 | // Lower 6 bits (0x003f mask) contain the version of compression algorithm, resulting in possible 0 - 63 values. 232 | // Currently values 0 and 1 are possible. Version 0 archives can be unpacked by RAR 5.0 and newer. 233 | // Version 1 archives can be unpacked by RAR 7.0 and newer. 234 | $info->version = $raw & 0x003F; 235 | 236 | // 7th bit (0x0040) defines the solid flag. If it is set, 237 | // RAR continues to use the compression dictionary left after processing preceding files. 238 | // It can be set only for file headers and is never set for service headers. 239 | $info->solid = ($raw & 0x0040) !== 0; 240 | 241 | // Bits 8 - 10 (0x0380 mask) define the compression method. 242 | // Currently only values 0 - 5 are used. 0 means no compression. 243 | $info->method = ($raw & 0x0380) >> 7; 244 | 245 | // Bits 11 - 15 (0x7c00) specify the minimum dictionary size required to extract data. 246 | // If we define these bits as N, the dictionary size is 128 KB * 2^N. 247 | // So value 0 means 128 KB, 1 - 256 KB, ..., 15 - 4096 MB, ..., 19 - 64 GB. 23 means 1 TB, 248 | // which is the theoretical maximum allowed by this field. 249 | // Actual compression and decompression implementation might have a lower limit. 250 | // Values above 15 are used only if compression algorithm version is 1. 251 | $info->dictionarySizeClass = ($raw & 0x7C00) >> 10; 252 | 253 | // Base dictionary size: 128 KB × 2^N 254 | $info->dictionarySize = 128 * 1024 * (2 ** $info->dictionarySizeClass); 255 | 256 | if ($info->version === 1) { 257 | // Bits 16 - 20 (0xf8000) are present only if version of compression algorithm is 1. 258 | // Value in these bits is multiplied to the dictionary size in bits 11 - 15 and divided by 32, 259 | // the result is added to dictionary size. 260 | // It allows to specify up to 31 intermediate dictionary sizes between neighbouring power of 2 values. 261 | $info->dictionarySizeMultiplier = ($raw & 0xF8000) >> 15; 262 | 263 | // Bit 21 (0x100000) is present only if version of compression algorithm is 1. 264 | // It indicates that even though the dictionary size flags are in version 1 format, 265 | // the actual compression algorithm is version 0. 266 | // It is helpful when we append version 1 files to existing version 0 solid stream 267 | // and need to increase the dictionary size for version 0 files not touching their compressed data. 268 | $info->forcedVersion0 = ($raw & 0x100000) !== 0; 269 | 270 | if ($info->dictionarySizeMultiplier > 0) { 271 | $info->dictionarySize += intdiv($info->dictionarySize * $info->dictionarySizeMultiplier, 32); 272 | } 273 | } 274 | 275 | // stringify 276 | $dictSizeKB = (int)($info->dictionarySize / 1024); 277 | 278 | if ($dictSizeKB >= 1024) { 279 | $unit = 'm'; 280 | $dictSize = $dictSizeKB / 1024; 281 | } else { 282 | $unit = 'k'; 283 | $dictSize = $dictSizeKB; 284 | } 285 | 286 | $info->methodName = sprintf( 287 | 'v%d:m%d:%d%s', 288 | $info->version, 289 | $info->method, 290 | $dictSize, 291 | $unit 292 | ); 293 | 294 | return $info; 295 | } 296 | 297 | private function readExtraArea(SplFileObject $file, RarFileHeadStruct $fileHeader, int $extraAreaSize): void 298 | { 299 | $extraOffset = $file->ftell(); 300 | 301 | // extra area size 2 302 | $this->fileReader->readVint($file); 303 | $extraAreaType = $this->fileReader->readVint($file); 304 | 305 | // file time (extra record) 306 | if ($extraAreaType === 3) { 307 | $this->readExtraAreaFileTime($file, $fileHeader); 308 | } 309 | 310 | // Jump to end of extra record 311 | $file->fseek($extraOffset + $extraAreaSize); 312 | } 313 | 314 | private function readExtraAreaFileTime(SplFileObject $file, RarFileHeadStruct $fileHeader): void 315 | { 316 | $timeFlags = $this->fileReader->readVint($file); 317 | 318 | if (!$this->bit->isFlagSet($timeFlags, 0x0002)) { 319 | return; 320 | } 321 | 322 | // Time is stored in Unix time_t format if this flags 323 | // is set and in Windows FILETIME format otherwise 324 | $isUnixTime = $this->bit->isFlagSet($timeFlags, 0x0001); 325 | 326 | if ($isUnixTime) { 327 | $fileTimeUnix = $this->fileReader->readBigInt($file); 328 | $fileHeader->fileTime = (new DateTimeImmutable())->setTimestamp($fileTimeUnix); 329 | } else { 330 | // Convert bytes to a 64-bit integer 331 | $fileTime = ((array)unpack('P', (string)$file->fread(8)))[1]; 332 | 333 | // Adjust Windows FILETIME to Unix timestamp format 334 | $fileTimeUnix = ($fileTime - 116444736000000000) / 10000000; 335 | $fileHeader->fileTime = (new DateTimeImmutable())->setTimestamp((int)$fileTimeUnix); 336 | } 337 | } 338 | } 339 | --------------------------------------------------------------------------------