Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.83% covered (success)
96.83%
61 / 63
81.82% covered (warning)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileSource
96.83% covered (success)
96.83%
61 / 63
81.82% covered (warning)
81.82%
9 / 11
29
0.00% covered (danger)
0.00%
0 / 1
 __construct
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 __destruct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 read
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
5.01
 readByte
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 peek
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 seek
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tell
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 size
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isEof
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fillBuffer
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 invalidateBuffer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Reader\Tokenizer;
6
7use Phpdftk\Filesystem\LocalFilesystem;
8
9final class FileSource implements Source
10{
11    private const BUFFER_SIZE = 8192;
12
13    /** @var resource */
14    private $handle;
15    private readonly int $fileSize;
16
17    private string $buffer = '';
18    private int $bufferStart = 0;
19    private int $bufferLength = 0;
20    private int $position = 0;
21
22    public function __construct(string $path)
23    {
24        LocalFilesystem::assertReadableFile($path);
25        $handle = fopen($path, 'rb');
26        if ($handle === false) {
27            throw new \RuntimeException("Failed to open file: $path");
28        }
29        $this->handle = $handle;
30        $this->fileSize = (int) filesize($path);
31    }
32
33    public function __destruct()
34    {
35        if (is_resource($this->handle)) {
36            fclose($this->handle);
37        }
38    }
39
40    public function read(int $length): string
41    {
42        if ($length <= 0) {
43            return '';
44        }
45
46        $bufferEnd = $this->bufferStart + $this->bufferLength;
47        $offsetInBuffer = $this->position - $this->bufferStart;
48
49        // Fast path: entire read is within the current buffer
50        if ($this->position >= $this->bufferStart && ($this->position + $length) <= $bufferEnd) {
51            $result = substr($this->buffer, $offsetInBuffer, $length);
52            $this->position += $length;
53            return $result;
54        }
55
56        // Slow path: read directly from file
57        fseek($this->handle, $this->position);
58        $result = fread($this->handle, $length);
59        if ($result === false) {
60            return '';
61        }
62        $this->position += strlen($result);
63        $this->invalidateBuffer();
64        return $result;
65    }
66
67    public function readByte(): ?string
68    {
69        if ($this->position >= $this->fileSize) {
70            return null;
71        }
72
73        $offsetInBuffer = $this->position - $this->bufferStart;
74        if ($offsetInBuffer >= 0 && $offsetInBuffer < $this->bufferLength) {
75            $byte = $this->buffer[$offsetInBuffer];
76            $this->position++;
77            return $byte;
78        }
79
80        $this->fillBuffer($this->position);
81        if ($this->bufferLength === 0) {
82            return null;
83        }
84        $byte = $this->buffer[0];
85        $this->position++;
86        return $byte;
87    }
88
89    public function peek(int $length = 1): string
90    {
91        if ($this->position >= $this->fileSize) {
92            return '';
93        }
94
95        $offsetInBuffer = $this->position - $this->bufferStart;
96        if ($offsetInBuffer >= 0 && ($offsetInBuffer + $length) <= $this->bufferLength) {
97            return $length === 1
98                ? $this->buffer[$offsetInBuffer]
99                : substr($this->buffer, $offsetInBuffer, $length);
100        }
101
102        $this->fillBuffer($this->position);
103        if ($this->bufferLength === 0) {
104            return '';
105        }
106        return $length === 1
107            ? $this->buffer[0]
108            : substr($this->buffer, 0, min($length, $this->bufferLength));
109    }
110
111    public function seek(int $offset): void
112    {
113        $this->position = $offset;
114    }
115
116    public function tell(): int
117    {
118        return $this->position;
119    }
120
121    public function size(): int
122    {
123        return $this->fileSize;
124    }
125
126    public function isEof(): bool
127    {
128        return $this->position >= $this->fileSize;
129    }
130
131    private function fillBuffer(int $offset): void
132    {
133        $this->bufferStart = $offset;
134        fseek($this->handle, $offset);
135        $data = fread($this->handle, self::BUFFER_SIZE);
136        if ($data === false || $data === '') {
137            $this->buffer = '';
138            $this->bufferLength = 0;
139            return;
140        }
141        $this->buffer = $data;
142        $this->bufferLength = strlen($data);
143    }
144
145    private function invalidateBuffer(): void
146    {
147        $this->bufferLength = 0;
148    }
149}