├── 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 |
--------------------------------------------------------------------------------