• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

azjezz / psl / 23100454484

15 Mar 2026 01:16AM UTC coverage: 95.628% (-2.8%) from 98.421%
23100454484

Pull #629

github

azjezz
feat(encoding): introduce streaming IO handles for Base64, QuotedPrintable, and Hex

Signed-off-by: azjezz <azjezz@protonmail.com>
Pull Request #629: feat(encoding): introduce streaming IO handles for Base64, QuotedPrintable, and Hex

479 of 797 new or added lines in 13 files covered. (60.1%)

2 existing lines in 1 file now uncovered.

10455 of 10933 relevant lines covered (95.63%)

32.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.73
/src/Psl/Encoding/Hex/EncodingReadHandle.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\Encoding\Hex;
6

7
use Psl;
8
use Psl\Async\CancellationTokenInterface;
9
use Psl\Async\NullCancellationToken;
10
use Psl\IO;
11

12
use function strlen;
13
use function strpos;
14
use function substr;
15

16
use const PHP_EOL;
17

18
/**
19
 * A read handle that hex-encodes raw binary data from an inner readable handle.
20
 *
21
 * Reads chunks from the inner handle, encodes each via {@see encode()},
22
 * and buffers the encoded output for consumption.
23
 */
24
final class EncodingReadHandle implements IO\BufferedReadHandleInterface
25
{
26
    use IO\ReadHandleConvenienceMethodsTrait;
27

28
    private string $buffer = '';
29
    private bool $eof = false;
30

31
    public function __construct(
32
        private readonly IO\ReadHandleInterface $handle,
33
    ) {}
6✔
34

35
    /**
36
     * {@inheritDoc}
37
     */
38
    public function reachedEndOfDataSource(): bool
39
    {
40
        return $this->eof && $this->buffer === '';
3✔
41
    }
42

43
    /**
44
     * {@inheritDoc}
45
     */
46
    public function tryRead(null|int $max_bytes = null): string
47
    {
NEW
48
        if ($this->buffer === '' && !$this->eof) {
×
NEW
49
            $this->fillBuffer();
×
50
        }
51

NEW
52
        if ($this->buffer === '') {
×
NEW
53
            return '';
×
54
        }
55

NEW
56
        if (null === $max_bytes || $max_bytes >= strlen($this->buffer)) {
×
NEW
57
            $result = $this->buffer;
×
NEW
58
            $this->buffer = '';
×
NEW
59
            return $result;
×
60
        }
61

NEW
62
        $result = substr($this->buffer, 0, $max_bytes);
×
NEW
63
        $this->buffer = substr($this->buffer, $max_bytes);
×
NEW
64
        return $result;
×
65
    }
66

67
    /**
68
     * {@inheritDoc}
69
     */
70
    public function read(
71
        null|int $max_bytes = null,
72
        CancellationTokenInterface $cancellation = new NullCancellationToken(),
73
    ): string {
74
        if ($this->eof && $this->buffer === '') {
3✔
NEW
75
            return '';
×
76
        }
77

78
        if ($this->buffer === '') {
3✔
79
            $this->fillBuffer($cancellation);
3✔
80
        }
81

82
        if ($this->buffer === '') {
3✔
83
            return '';
3✔
84
        }
85

86
        if (null === $max_bytes || $max_bytes >= strlen($this->buffer)) {
2✔
87
            $result = $this->buffer;
2✔
88
            $this->buffer = '';
2✔
89
            return $result;
2✔
90
        }
91

NEW
92
        $result = substr($this->buffer, 0, $max_bytes);
×
NEW
93
        $this->buffer = substr($this->buffer, $max_bytes);
×
NEW
94
        return $result;
×
95
    }
96

97
    public function readByte(CancellationTokenInterface $cancellation = new NullCancellationToken()): string
98
    {
99
        if ($this->buffer === '' && !$this->eof) {
1✔
100
            $this->fillBuffer($cancellation);
1✔
101
        }
102

103
        if ($this->buffer === '') {
1✔
NEW
104
            throw new IO\Exception\RuntimeException('Reached EOF without any more data.');
×
105
        }
106

107
        $ret = $this->buffer[0];
1✔
108
        if ($ret === $this->buffer) {
1✔
109
            $this->buffer = '';
1✔
110
            return $ret;
1✔
111
        }
112

113
        $this->buffer = substr($this->buffer, 1);
1✔
114
        return $ret;
1✔
115
    }
116

117
    public function readLine(CancellationTokenInterface $cancellation = new NullCancellationToken()): null|string
118
    {
119
        $line = $this->readUntil(PHP_EOL, $cancellation);
1✔
120
        if ($line !== null) {
1✔
NEW
121
            return $line;
×
122
        }
123

124
        // No EOL found; return whatever remains, or null if empty
125
        if ($this->buffer === '' && !$this->eof) {
1✔
NEW
126
            $this->fillBuffer($cancellation);
×
127
        }
128

129
        if ($this->buffer === '') {
1✔
NEW
130
            return null;
×
131
        }
132

133
        $result = $this->buffer;
1✔
134
        $this->buffer = '';
1✔
135
        return $result;
1✔
136
    }
137

138
    public function readUntil(
139
        string $suffix,
140
        CancellationTokenInterface $cancellation = new NullCancellationToken(),
141
    ): null|string {
142
        $suffix_len = strlen($suffix);
2✔
143
        $idx = strpos($this->buffer, $suffix);
2✔
144
        if ($idx !== false) {
2✔
NEW
145
            $result = substr($this->buffer, 0, $idx);
×
NEW
146
            $this->buffer = substr($this->buffer, $idx + $suffix_len);
×
NEW
147
            return $result;
×
148
        }
149

150
        while (!$this->eof) {
2✔
151
            $offset = strlen($this->buffer) - $suffix_len + 1;
2✔
152
            $offset = $offset > 0 ? $offset : 0;
2✔
153

154
            $this->fillBuffer($cancellation);
2✔
155

156
            $idx = strpos($this->buffer, $suffix, $offset);
2✔
157
            if ($idx !== false) {
2✔
158
                $result = substr($this->buffer, 0, $idx);
1✔
159
                $this->buffer = substr($this->buffer, $idx + $suffix_len);
1✔
160
                return $result;
1✔
161
            }
162
        }
163

164
        return null;
1✔
165
    }
166

167
    public function readUntilBounded(
168
        string $suffix,
169
        int $max_bytes,
170
        CancellationTokenInterface $cancellation = new NullCancellationToken(),
171
    ): null|string {
NEW
172
        $suffix_len = strlen($suffix);
×
NEW
173
        $idx = strpos($this->buffer, $suffix);
×
NEW
174
        if ($idx !== false) {
×
NEW
175
            if ($idx > $max_bytes) {
×
NEW
176
                throw new IO\Exception\OverflowException(Psl\Str\format(
×
NEW
177
                    'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
NEW
178
                    $max_bytes,
×
NEW
179
                    $suffix,
×
NEW
180
                ));
×
181
            }
182

NEW
183
            $result = substr($this->buffer, 0, $idx);
×
NEW
184
            $this->buffer = substr($this->buffer, $idx + $suffix_len);
×
NEW
185
            return $result;
×
186
        }
187

NEW
188
        if (strlen($this->buffer) > $max_bytes) {
×
NEW
189
            throw new IO\Exception\OverflowException(Psl\Str\format(
×
NEW
190
                'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
NEW
191
                $max_bytes,
×
NEW
192
                $suffix,
×
NEW
193
            ));
×
194
        }
195

NEW
196
        while (!$this->eof) {
×
NEW
197
            $offset = strlen($this->buffer) - $suffix_len + 1;
×
NEW
198
            $offset = $offset > 0 ? $offset : 0;
×
199

NEW
200
            $this->fillBuffer($cancellation);
×
201

NEW
202
            $idx = strpos($this->buffer, $suffix, $offset);
×
NEW
203
            if ($idx !== false) {
×
NEW
204
                if ($idx > $max_bytes) {
×
NEW
205
                    throw new IO\Exception\OverflowException(Psl\Str\format(
×
NEW
206
                        'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
NEW
207
                        $max_bytes,
×
NEW
208
                        $suffix,
×
NEW
209
                    ));
×
210
                }
211

NEW
212
                $result = substr($this->buffer, 0, $idx);
×
NEW
213
                $this->buffer = substr($this->buffer, $idx + $suffix_len);
×
NEW
214
                return $result;
×
215
            }
216

NEW
217
            if (strlen($this->buffer) > $max_bytes) {
×
NEW
218
                throw new IO\Exception\OverflowException(Psl\Str\format(
×
NEW
219
                    'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
NEW
220
                    $max_bytes,
×
NEW
221
                    $suffix,
×
NEW
222
                ));
×
223
            }
224
        }
225

NEW
226
        return null;
×
227
    }
228

229
    private function fillBuffer(CancellationTokenInterface $cancellation = new NullCancellationToken()): void
230
    {
231
        if ($this->eof) {
6✔
NEW
232
            return;
×
233
        }
234

235
        $chunk = $this->handle->read(4096, $cancellation);
6✔
236
        if ($chunk === '' && $this->handle->reachedEndOfDataSource()) {
6✔
237
            $this->eof = true;
4✔
238
            return;
4✔
239
        }
240

241
        if ($chunk !== '') {
5✔
242
            $this->buffer .= encode($chunk);
5✔
243
        }
244
    }
245
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc