1<?php
2declare(strict_types=1);
3namespace TYPO3\PharStreamWrapper\Phar;
4
5/*
6 * This file is part of the TYPO3 project.
7 *
8 * It is free software; you can redistribute it and/or modify it under the terms
9 * of the MIT License (MIT). For the full copyright and license information,
10 * please read the LICENSE file that was distributed with this source code.
11 *
12 * The TYPO3 project - inspiring people to share!
13 */
14
15class Reader
16{
17    /**
18     * @var string
19     */
20    private $fileName;
21
22    /**
23     * Mime-type in order to use zlib, bzip2 or no compression.
24     * In case ext-fileinfo is not present only the relevant types
25     * 'application/x-gzip' and 'application/x-bzip2' are assigned
26     * to this class property.
27     *
28     * @var string
29     */
30    private $fileType;
31
32    /**
33     * @param string $fileName
34     */
35    public function __construct(string $fileName)
36    {
37        if (strpos($fileName, '://') !== false) {
38            throw new ReaderException(
39                'File name must not contain stream prefix',
40                1539623708
41            );
42        }
43
44        $this->fileName = $fileName;
45        $this->fileType = $this->determineFileType();
46    }
47
48    /**
49     * @return Container
50     */
51    public function resolveContainer(): Container
52    {
53        $data = $this->extractData($this->resolveStream() . $this->fileName);
54
55        if ($data['stubContent'] === null) {
56            throw new ReaderException(
57                'Cannot resolve stub',
58                1547807881
59            );
60        }
61        if ($data['manifestContent'] === null || $data['manifestLength'] === null) {
62            throw new ReaderException(
63                'Cannot resolve manifest',
64                1547807882
65            );
66        }
67        if (strlen($data['manifestContent']) < $data['manifestLength']) {
68            throw new ReaderException(
69                sprintf(
70                    'Exected manifest length %d, got %d',
71                    strlen($data['manifestContent']),
72                    $data['manifestLength']
73                ),
74                1547807883
75            );
76        }
77
78        return new Container(
79            Stub::fromContent($data['stubContent']),
80            Manifest::fromContent($data['manifestContent'])
81        );
82    }
83
84    /**
85     * @param string $fileName e.g. '/path/file.phar' or 'compress.zlib:///path/file.phar'
86     * @return array
87     */
88    private function extractData(string $fileName): array
89    {
90        $stubContent = null;
91        $manifestContent = null;
92        $manifestLength = null;
93
94        $resource = fopen($fileName, 'r');
95        if (!is_resource($resource)) {
96            throw new ReaderException(
97                sprintf('Resource %s could not be opened', $fileName),
98                1547902055
99            );
100        }
101
102        while (!feof($resource)) {
103            $line = fgets($resource);
104            // stop processing in case the system fails to read from a stream
105            if ($line === false) {
106                break;
107            }
108            // stop reading file when manifest can be extracted
109            if ($manifestLength !== null && $manifestContent !== null && strlen($manifestContent) >= $manifestLength) {
110                break;
111            }
112
113            $manifestPosition = strpos($line, '__HALT_COMPILER();');
114
115            // first line contains start of manifest
116            if ($stubContent === null && $manifestContent === null && $manifestPosition !== false) {
117                $stubContent = substr($line, 0, $manifestPosition - 1);
118                $manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line);
119                $manifestLength = $this->resolveManifestLength($manifestContent);
120            // line contains start of stub
121            } elseif ($stubContent === null) {
122                $stubContent = $line;
123            // line contains start of manifest
124            } elseif ($manifestContent === null && $manifestPosition !== false) {
125                $manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line);
126                $manifestLength = $this->resolveManifestLength($manifestContent);
127            // manifest has been started (thus is cannot be stub anymore), add content
128            } elseif ($manifestContent !== null) {
129                $manifestContent .= $line;
130                $manifestLength = $this->resolveManifestLength($manifestContent);
131            // stub has been started (thus cannot be manifest here, yet), add content
132            } elseif ($stubContent !== null) {
133                $stubContent .= $line;
134            }
135        }
136        fclose($resource);
137
138        return [
139            'stubContent' => $stubContent,
140            'manifestContent' => $manifestContent,
141            'manifestLength' => $manifestLength,
142        ];
143    }
144
145    /**
146     * Resolves stream in order to handle compressed Phar archives.
147     *
148     * @return string
149     */
150    private function resolveStream(): string
151    {
152        if ($this->fileType === 'application/x-gzip' || $this->fileType === 'application/gzip') {
153            return 'compress.zlib://';
154        } elseif ($this->fileType === 'application/x-bzip2') {
155            return 'compress.bzip2://';
156        }
157        return '';
158    }
159
160    /**
161     * @return string
162     */
163    private function determineFileType()
164    {
165        if (class_exists('\\finfo')) {
166            $fileInfo = new \finfo();
167            return $fileInfo->file($this->fileName, FILEINFO_MIME_TYPE);
168        }
169        return $this->determineFileTypeByHeader();
170    }
171
172    /**
173     * In case ext-fileinfo is not present only the relevant types
174     * 'application/x-gzip' and 'application/x-bzip2' are resolved.
175     *
176     * @return string
177     */
178    private function determineFileTypeByHeader(): string
179    {
180        $resource = fopen($this->fileName, 'r');
181        if (!is_resource($resource)) {
182            throw new ReaderException(
183                sprintf('Resource %s could not be opened', $this->fileName),
184                1557753055
185            );
186        }
187        $header = fgets($resource, 4);
188        fclose($resource);
189        $mimeType = '';
190        if (strpos($header, "\x42\x5a\x68") === 0) {
191            $mimeType = 'application/x-bzip2';
192        } elseif (strpos($header, "\x1f\x8b") === 0) {
193            $mimeType = 'application/x-gzip';
194        }
195        return $mimeType;
196    }
197
198    /**
199     * @param string $content
200     * @return int|null
201     */
202    private function resolveManifestLength(string $content)
203    {
204        if (strlen($content) < 4) {
205            return null;
206        }
207        return static::resolveFourByteLittleEndian($content, 0);
208    }
209
210    /**
211     * @param string $content
212     * @param int $start
213     * @return int
214     */
215    public static function resolveFourByteLittleEndian(string $content, int $start): int
216    {
217        $payload = substr($content, $start, 4);
218        if (!is_string($payload)) {
219            throw new ReaderException(
220                sprintf('Cannot resolve value at offset %d', $start),
221                1539614260
222            );
223        }
224
225        $value = unpack('V', $payload);
226        if (!isset($value[1])) {
227            throw new ReaderException(
228                sprintf('Cannot resolve value at offset %d', $start),
229                1539614261
230            );
231        }
232        return $value[1];
233    }
234
235    /**
236     * @param string $content
237     * @param int $start
238     * @return int
239     */
240    public static function resolveTwoByteBigEndian(string $content, int $start): int
241    {
242        $payload = substr($content, $start, 2);
243        if (!is_string($payload)) {
244            throw new ReaderException(
245                sprintf('Cannot resolve value at offset %d', $start),
246                1539614263
247            );
248        }
249
250        $value = unpack('n', $payload);
251        if (!isset($value[1])) {
252            throw new ReaderException(
253                sprintf('Cannot resolve value at offset %d', $start),
254                1539614264
255            );
256        }
257        return $value[1];
258    }
259}
260