1<?php
2/**
3 * This file is part of FPDI
4 *
5 * @package   setasign\Fpdi
6 * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com)
7 * @license   http://opensource.org/licenses/mit-license The MIT License
8 */
9
10namespace setasign\Fpdi\PdfParser;
11
12/**
13 * A stream reader class
14 *
15 * @package setasign\Fpdi\PdfParser
16 */
17class StreamReader
18{
19    /**
20     * Creates a stream reader instance by a string value.
21     *
22     * @param string $content
23     * @param int $maxMemory
24     * @return StreamReader
25     */
26    public static function createByString($content, $maxMemory = 2097152)
27    {
28        $h = \fopen('php://temp/maxmemory:' . ((int) $maxMemory), 'r+b');
29        \fwrite($h, $content);
30        \rewind($h);
31
32        return new self($h, true);
33    }
34
35    /**
36     * Creates a stream reader instance by a filename.
37     *
38     * @param string $filename
39     * @return StreamReader
40     */
41    public static function createByFile($filename)
42    {
43        $h = \fopen($filename, 'rb');
44        return new self($h, true);
45    }
46
47    /**
48     * Defines whether the stream should be closed when the stream reader instance is deconstructed or not.
49     *
50     * @var bool
51     */
52    protected $closeStream;
53
54    /**
55     * The stream resource.
56     *
57     * @var resource
58     */
59    protected $stream;
60
61    /**
62     * The byte-offset position in the stream.
63     *
64     * @var int
65     */
66    protected $position;
67
68    /**
69     * The byte-offset position in the buffer.
70     *
71     * @var int
72     */
73    protected $offset;
74
75    /**
76     * The buffer length.
77     *
78     * @var int
79     */
80    protected $bufferLength;
81
82    /**
83     * The total length of the stream.
84     *
85     * @var int
86     */
87    protected $totalLength;
88
89    /**
90     * The buffer.
91     *
92     * @var string
93     */
94    protected $buffer;
95
96    /**
97     * StreamReader constructor.
98     *
99     * @param resource $stream
100     * @param bool $closeStream Defines whether to close the stream resource if the instance is destructed or not.
101     */
102    public function __construct($stream, $closeStream = false)
103    {
104        if (!\is_resource($stream)) {
105            throw new \InvalidArgumentException(
106                'No stream given.'
107            );
108        }
109
110        $metaData = \stream_get_meta_data($stream);
111        if (!$metaData['seekable']) {
112            throw new \InvalidArgumentException(
113                'Given stream is not seekable!'
114            );
115        }
116
117        $this->stream = $stream;
118        $this->closeStream = $closeStream;
119        $this->reset();
120    }
121
122    /**
123     * The destructor.
124     */
125    public function __destruct()
126    {
127        $this->cleanUp();
128    }
129
130    /**
131     * Closes the file handle.
132     */
133    public function cleanUp()
134    {
135        if ($this->closeStream && is_resource($this->stream)) {
136            \fclose($this->stream);
137        }
138    }
139
140    /**
141     * Returns the byte length of the buffer.
142     *
143     * @param bool $atOffset
144     * @return int
145     */
146    public function getBufferLength($atOffset = false)
147    {
148        if ($atOffset === false) {
149            return $this->bufferLength;
150        }
151
152        return $this->bufferLength - $this->offset;
153    }
154
155    /**
156     * Get the current position in the stream.
157     *
158     * @return int
159     */
160    public function getPosition()
161    {
162        return $this->position;
163    }
164
165    /**
166     * Returns the current buffer.
167     *
168     * @param bool $atOffset
169     * @return string
170     */
171    public function getBuffer($atOffset = true)
172    {
173        if ($atOffset === false) {
174            return $this->buffer;
175        }
176
177        $string = \substr($this->buffer, $this->offset);
178
179        return (string) $string;
180    }
181
182    /**
183     * Gets a byte at a specific position in the buffer.
184     *
185     * If the position is invalid the method will return false.
186     *
187     * If the $position parameter is set to null the value of $this->offset will be used.
188     *
189     * @param int|null $position
190     * @return string|bool
191     */
192    public function getByte($position = null)
193    {
194        $position = (int) ($position !== null ? $position : $this->offset);
195        if ($position >= $this->bufferLength &&
196            (!$this->increaseLength() || $position >= $this->bufferLength)
197        ) {
198            return false;
199        }
200
201        return $this->buffer[$position];
202    }
203
204    /**
205     * Returns a byte at a specific position, and set the offset to the next byte position.
206     *
207     * If the position is invalid the method will return false.
208     *
209     * If the $position parameter is set to null the value of $this->offset will be used.
210     *
211     * @param int|null $position
212     * @return string|bool
213     */
214    public function readByte($position = null)
215    {
216        if ($position !== null) {
217            $position = (int) $position;
218            // check if needed bytes are available in the current buffer
219            if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) {
220                $this->reset($position);
221                $offset = $this->offset;
222            } else {
223                $offset = $position - $this->position;
224            }
225        } else {
226            $offset = $this->offset;
227        }
228
229        if ($offset >= $this->bufferLength &&
230            ((!$this->increaseLength()) || $offset >= $this->bufferLength)
231        ) {
232            return false;
233        }
234
235        $this->offset = $offset + 1;
236        return $this->buffer[$offset];
237    }
238
239    /**
240     * Read bytes from the current or a specific offset position and set the internal pointer to the next byte.
241     *
242     * If the position is invalid the method will return false.
243     *
244     * If the $position parameter is set to null the value of $this->offset will be used.
245     *
246     * @param int $length
247     * @param int|null $position
248     * @return string
249     */
250    public function readBytes($length, $position = null)
251    {
252        $length = (int) $length;
253        if ($position !== null) {
254            // check if needed bytes are available in the current buffer
255            if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) {
256                $this->reset($position, $length);
257                $offset = $this->offset;
258            } else {
259                $offset = $position - $this->position;
260            }
261        } else {
262            $offset = $this->offset;
263        }
264
265        if (($offset + $length) > $this->bufferLength &&
266            ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength)
267        ) {
268            return false;
269        }
270
271        $bytes = \substr($this->buffer, $offset, $length);
272        $this->offset = $offset + $length;
273
274        return $bytes;
275    }
276
277    /**
278     * Read a line from the current position.
279     *
280     * @param int $length
281     * @return string|bool
282     */
283    public function readLine($length = 1024)
284    {
285        if ($this->ensureContent() === false) {
286            return false;
287        }
288
289        $line = '';
290        while ($this->ensureContent()) {
291            $char = $this->readByte();
292
293            if ($char === "\n") {
294                break;
295            }
296
297            if ($char === "\r") {
298                if ($this->getByte() === "\n") {
299                    $this->addOffset(1);
300                }
301                break;
302            }
303
304            $line .= $char;
305
306            if (\strlen($line) >= $length) {
307                break;
308            }
309        }
310
311        return $line;
312    }
313
314    /**
315     * Set the offset position in the current buffer.
316     *
317     * @param int $offset
318     */
319    public function setOffset($offset)
320    {
321        if ($offset > $this->bufferLength || $offset < 0) {
322            throw new \OutOfRangeException(
323                \sprintf('Offset (%s) out of range (length: %s)', $offset, $this->bufferLength)
324            );
325        }
326
327        $this->offset = (int) $offset;
328    }
329
330    /**
331     * Returns the current offset in the current buffer.
332     *
333     * @return int
334     */
335    public function getOffset()
336    {
337        return $this->offset;
338    }
339
340    /**
341     * Add an offset to the current offset.
342     *
343     * @param int $offset
344     */
345    public function addOffset($offset)
346    {
347        $this->setOffset($this->offset + $offset);
348    }
349
350    /**
351     * Make sure that there is at least one character beyond the current offset in the buffer.
352     *
353     * @return bool
354     */
355    public function ensureContent()
356    {
357        while ($this->offset >= $this->bufferLength) {
358            if (!$this->increaseLength()) {
359                return false;
360            }
361        }
362        return true;
363    }
364
365    /**
366     * Returns the stream.
367     *
368     * @return resource
369     */
370    public function getStream()
371    {
372        return $this->stream;
373    }
374
375    /**
376     * Gets the total available length.
377     *
378     * @return int
379     */
380    public function getTotalLength()
381    {
382        if ($this->totalLength === null) {
383            $stat = \fstat($this->stream);
384            $this->totalLength = $stat['size'];
385        }
386
387        return $this->totalLength;
388    }
389
390    /**
391     * Resets the buffer to a position and re-read the buffer with the given length.
392     *
393     * If the $pos parameter is negative the start buffer position will be the $pos'th position from
394     * the end of the file.
395     *
396     * If the $pos parameter is negative and the absolute value is bigger then the totalLength of
397     * the file $pos will set to zero.
398     *
399     * @param int|null $pos Start position of the new buffer
400     * @param int $length Length of the new buffer. Mustn't be negative
401     */
402    public function reset($pos = 0, $length = 200)
403    {
404        if ($pos === null) {
405            $pos = $this->position + $this->offset;
406        } elseif ($pos < 0) {
407            $pos = \max(0, $this->getTotalLength() + $pos);
408        }
409
410        \fseek($this->stream, $pos);
411
412        $this->position = $pos;
413        $this->buffer = $length > 0 ? \fread($this->stream, $length) : '';
414        $this->bufferLength = \strlen($this->buffer);
415        $this->offset = 0;
416
417        // If a stream wrapper is in use it is possible that
418        // length values > 8096 will be ignored, so use the
419        // increaseLength()-method to correct that behavior
420        if ($this->bufferLength < $length && $this->increaseLength($length - $this->bufferLength)) {
421            // increaseLength parameter is $minLength, so cut to have only the required bytes in the buffer
422            $this->buffer = \substr($this->buffer, 0, $length);
423            $this->bufferLength = \strlen($this->buffer);
424        }
425    }
426
427    /**
428     * Ensures bytes in the buffer with a specific length and location in the file.
429     *
430     * @param int $pos
431     * @param int $length
432     * @see reset()
433     */
434    public function ensure($pos, $length)
435    {
436        if ($pos >= $this->position
437            && $pos < ($this->position + $this->bufferLength)
438            && ($this->position + $this->bufferLength) >= ($pos + $length)
439        ) {
440            $this->offset = $pos - $this->position;
441        } else {
442            $this->reset($pos, $length);
443        }
444    }
445
446    /**
447     * Forcefully read more data into the buffer.
448     *
449     * @param int $minLength
450     * @return bool Returns false if the stream reaches the end
451     */
452    public function increaseLength($minLength = 100)
453    {
454        $length = \max($minLength, 100);
455
456        if (\feof($this->stream) || $this->getTotalLength() === $this->position + $this->bufferLength) {
457            return false;
458        }
459
460        $newLength = $this->bufferLength + $length;
461        do {
462            $this->buffer .= \fread($this->stream, $newLength - $this->bufferLength);
463            $this->bufferLength = \strlen($this->buffer);
464        } while (($this->bufferLength !== $newLength) && !\feof($this->stream));
465
466        return true;
467    }
468}
469