aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/Stream/FileStream.php
blob: 308f617dfa289deba678d8b24ca96d1f52712f5b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<?php

declare(strict_types=1);

namespace Nsfisis\Waddiwasi\Stream;

use function assert;
use function chr;
use function fclose;
use function fopen;
use function fread;
use function ord;
use function sprintf;
use function strlen;

final class FileStream implements StreamInterface
{
    /**
     * @var resource
     */
    private readonly mixed $fp;

    /**
     * @var ?int<0, 255>
     */
    private ?int $peekedByte = null;

    public function __construct(
        private readonly string $path,
    ) {
        $fp = fopen($path, 'rb');
        if ($fp === false) {
            throw new IoException("Failed to open file: $path");
        }
        $this->fp = $fp;
    }

    public function close(): void
    {
        fclose($this->fp);
    }

    public function read(int $bytes): string
    {
        if ($this->peekedByte !== null) {
            $first = chr($this->peekedByte);
            $this->peekedByte = null;
            if ($bytes === 1) {
                return $first;
            } else {
                return $first . $this->doRead($bytes - 1);
            }
        } else {
            return $this->doRead($bytes);
        }
    }

    public function readByte(): int
    {
        if ($this->peekedByte !== null) {
            $ret = $this->peekedByte;
            $this->peekedByte = null;
            return $ret;
        }
        return ord($this->doRead(1));
    }

    public function peekByte(): int
    {
        if ($this->peekedByte === null) {
            $this->peekedByte = ord($this->doRead(1));
        }
        return $this->peekedByte;
    }

    public function seek(int $bytes): void
    {
        $this->read($bytes);
    }

    public function tell(): int
    {
        $ret = ftell($this->fp);
        if ($ret === false) {
            throw new IoException("Failed to get current position in file: $this->path");
        }
        assert(0 <= $ret);
        return $ret;
    }

    public function eof(): bool
    {
        // feof() does not work because it returns true only after an
        // unsuccessful fread().
        if ($this->peekedByte !== null) {
            return false;
        }
        $result = fread($this->fp, 1);
        if ($result === false || $result === '') {
            return true;
        }
        $this->peekedByte = ord($result);
        return false;
    }

    /**
     * @param positive-int $bytes
     *
     * @return non-empty-string
     *
     * @phpstan-impure
     */
    private function doRead(int $bytes): string
    {
        $result = fread($this->fp, $bytes);
        if ($result === false) {
            throw new IoException("Failed to read from file: $this->path");
        }
        if (strlen($result) < $bytes) {
            throw new UnexpectedEofException(sprintf("Unexpected EOF while reading from file: %s (%d bytes expected, %d bytes read)", $this->path, $bytes, strlen($result)));
        }
        return $result;
    }
}