ReverseRead   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 100
dl 0
loc 276
rs 9.36
c 0
b 0
f 0
wmc 38

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getTail() 0 32 6
A getLastLine() 0 3 1
B getPreviousLine() 0 44 10
A getFileMtime() 0 3 1
A readChunk() 0 19 3
A __destruct() 0 3 1
A __construct() 0 25 5
A getLineAtPost() 0 13 5
A getFileSize() 0 3 1
A cutHead() 0 12 4
A getFirstLine() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\logpeek\File;
6
7
use Exception;
8
9
use function array_merge;
10
use function count;
11
use function explode;
12
use function fclose;
13
use function fgetc;
14
use function file_exists;
15
use function fopen;
16
use function fread;
17
use function fseek;
18
use function is_readable;
19
use function rtrim;
20
use function strlen;
21
use function strpos;
22
use function substr;
23
use function substr_count;
24
25
/**
26
 * Functionatility for line by line reverse reading of a file. It is done by blockwise
27
 * fetching the file from the end and putting the lines into an array.
28
 */
29
class ReverseRead
30
{
31
    /**
32
     * 8192 is max number of octets limited by fread.
33
     * @var int
34
     */
35
    private int $blockSize;
36
37
    /** @var int */
38
    private int $blockStart;
39
40
    /** @var resource */
41
    private $fileHandle;
42
43
    /**
44
     * fileSize may be changed after initial file size check
45
     * @var int
46
     */
47
    private int $fileSize;
48
49
    /** @var int */
50
    private int $fileMtime;
51
52
    /** @var array Array containing file lines */
53
    private array $content;
54
55
    /** @var string Leftover before first complete line */
56
    private string $remainder;
57
58
    /** @var int  Count read lines from the end */
59
    private int $readPointer;
60
61
62
    /**
63
     * File is checked and file handle to file is opend. But no data is read
64
     * from the file.
65
     *
66
     * @param string $fileUrl Path and filename to file to be read
67
     * @param int $blockSize File read block size in byte
68
     */
69
    public function __construct(string $fileUrl, int $blockSize = 8192)
70
    {
71
        if (!file_exists($fileUrl)) {
72
            throw new Exception("File does not exist: '$fileUrl'");
73
        }
74
        if (!is_readable($fileUrl)) {
75
            throw new Exception("Cannot read file '$fileUrl'");
76
        }
77
78
        $this->blockSize = $blockSize;
79
        $this->content = [];
80
        $this->remainder = '';
81
        $this->readPointer = 0;
82
83
        $fileInfo = stat($fileUrl);
84
        $this->fileSize = $this->blockStart = $fileInfo['size'];
85
        $this->fileMtime = $fileInfo['mtime'];
86
87
        if ($this->fileSize > 0) {
88
            $this->fileHandle = fopen($fileUrl, 'rb');
89
            if (!$this->fileHandle) {
90
                throw new Exception("Cannot open file '$fileUrl'");
91
            }
92
        } else {
93
            throw new Exception("File is zero length: '$fileUrl'");
94
        }
95
    }
96
97
98
    /**
99
     */
100
    public function __destruct()
101
    {
102
        fclose($this->fileHandle);
103
    }
104
105
106
    /**
107
     * Fetch chunk of data from file.
108
     * Each time this function is called, will it fetch a chunk
109
     * of data from the file. It starts from the end of the file
110
     * and work towards the beginning of the file.
111
     *
112
     * @return string|false buffer with datablock.
113
     * Will return bool FALSE when there is no more data to get.
114
     */
115
    private function readChunk()
116
    {
117
        $splits = $this->blockSize;
118
119
        $this->blockStart -= $splits;
120
        if ($this->blockStart < 0) {
121
            $splits += $this->blockStart;
122
            $this->blockStart = 0;
123
        }
124
125
        // Return false if nothing more to read
126
        if ($splits === 0) {
127
            return false;
128
        }
129
130
        fseek($this->fileHandle, $this->blockStart, SEEK_SET);
131
        $buff = fread($this->fileHandle, $splits);
132
133
        return $buff;
134
    }
135
136
137
    /**
138
     * Get one line of data from the file, starting from the end of the file.
139
     *
140
     * @return string|false One line of data from the file.
141
     * Bool FALSE when there is no more data to get.
142
     */
143
    public function getPreviousLine()
144
    {
145
        if (count($this->content) === 0 || $this->readPointer < 1) {
146
            do {
147
                $buff = $this->readChunk();
148
                if ($buff === false) {
149
                    // Empty buffer, no more to read.
150
                    if (strlen($this->remainder) > 0) {
151
                        $buff = $this->remainder;
152
                        $this->remainder = '';
153
                        // Exit from while-loop
154
                        break;
155
                    }
156
157
                    // Remainder also empty.
158
                    return false;
159
                }
160
                $eolPos = strpos($buff, "\n");
161
162
                if ($eolPos === false) {
163
                    // No eol found. Make buffer head of remainder and empty buffer.
164
                    $this->remainder = $buff . $this->remainder;
165
                    $buff = '';
166
                } elseif ($eolPos !== 0) {
167
                    // eol found.
168
                    $buff .= $this->remainder;
169
                    $this->remainder = substr($buff, 0, $eolPos);
170
                    $buff = substr($buff, $eolPos + 1);
171
                } else {
172
                    // eol must be 0.
173
                    $buff .= $this->remainder;
174
                    $buff = substr($buff, 1);
175
                    $this->remainder = '';
176
                }
177
            } while (($buff !== false) && ($eolPos === false));
178
179
            $this->content = explode("\n", $buff);
180
            $this->readPointer = count($this->content);
181
        }
182
183
        if (count($this->content) > 0) {
184
            return $this->content[--$this->readPointer];
185
        } else {
186
            return false;
187
        }
188
    }
189
190
191
    /**
192
     * @param string &$haystack
193
     * @param string $needle
194
     * @param int $exit
195
     * @return string|false
196
     */
197
    private function cutHead(string &$haystack, string $needle, int $exit)
198
    {
199
        $pos = 0;
200
        $cnt = 0;
201
        // Holder på inntill antall ønskede linjer eller vi ikke finner flere linjer
202
        /** @psalm-suppress PossiblyFalseArgument */
203
        while ($cnt < $exit && ($pos = strpos($haystack, $needle, $pos)) !== false) {
204
            $pos++;
205
            $cnt++;
206
        }
207
        /** @psalm-var int|false $pos */
208
        return ($pos === false) ? false : substr($haystack, $pos, strlen($haystack));
0 ignored issues
show
introduced by
The condition $pos === false is always false.
Loading history...
209
    }
210
211
212
    /**
213
     * FIXME: This function has some error, do not use before auditing and testing
214
     * @param int $lines
215
     * @return array
216
     */
217
    public function getTail(int $lines = 10): array
218
    {
219
        $this->blockStart = $this->fileSize;
220
        $buff1 = [];
221
        $lastLines = [];
222
223
        while ($this->blockStart) {
224
            $tmp_buff = $this->readChunk();
225
            if ($tmp_buff === false) {
226
                break;
227
            }
228
            $buff = $tmp_buff;
229
230
            $lines -= substr_count($buff, "\n");
231
232
            if ($lines <= 0) {
233
                $tmp_buff = $this->cutHead($buff, "\n", abs($lines) + 1);
0 ignored issues
show
Bug introduced by
abs($lines) + 1 of type double is incompatible with the type integer expected by parameter $exit of SimpleSAML\Module\logpee...\ReverseRead::cutHead(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
                $tmp_buff = $this->cutHead($buff, "\n", /** @scrutinizer ignore-type */ abs($lines) + 1);
Loading history...
234
                if ($tmp_buff === false) {
235
                    break;
236
                }
237
                $buff1[] = $tmp_buff;
238
                break;
239
            }
240
241
            $buff1[] = $buff;
242
        }
243
244
        for ($i = count($buff1); $i >= 0; $i--) {
245
            $lastLines = array_merge($lastLines, explode("\n", $buff1[$i]));
246
        }
247
248
        return $lastLines;
249
    }
250
251
252
    /**
253
     * @param int $pos
254
     * @return string|false
255
     */
256
    private function getLineAtPost(int $pos)
257
    {
258
        if ($pos < 0 || $pos > $this->fileSize) {
259
            return false;
260
        }
261
262
        $seeker = $pos;
263
        fseek($this->fileHandle, $seeker, SEEK_SET);
264
        while ($seeker > 0 && fgetc($this->fileHandle) !== "\n") {
265
            fseek($this->fileHandle, --$seeker, SEEK_SET);
266
        }
267
268
        return rtrim(fgets($this->fileHandle));
269
    }
270
271
272
    /**
273
     * @return string|false
274
     */
275
    public function getFirstLine()
276
    {
277
        return $this->getLineAtPost(0);
278
    }
279
280
281
    /**
282
     * @return string|false
283
     */
284
    public function getLastLine()
285
    {
286
        return $this->getLineAtPost($this->fileSize - 2);
287
    }
288
289
290
    /**
291
     * @return int
292
     */
293
    public function getFileSize(): int
294
    {
295
        return $this->fileSize;
296
    }
297
298
299
    /**
300
     * @return int
301
     */
302
    public function getFileMtime(): int
303
    {
304
        return $this->fileMtime;
305
    }
306
}
307