• 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

36.36
/src/Psl/Encoding/Base64/EncodingReadHandle.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\Encoding\Base64;
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 base64-encodes raw binary data from an inner readable handle.
20
 *
21
 * Reads {@see CHUNK_SIZE} (57) byte chunks from the inner handle, encodes each to base64,
22
 * and appends {@see LINE_ENDING} after each encoded chunk.
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
        private readonly Variant $variant = Variant::Standard,
34
        private readonly bool $padding = true,
35
    ) {}
8✔
36

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

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

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

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

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

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

80
        if ($this->buffer === '') {
5✔
81
            $this->fillBuffer($cancellation);
5✔
82
        }
83

84
        if ($this->buffer === '') {
5✔
85
            return '';
5✔
86
        }
87

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

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

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

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

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

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

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

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

NEW
131
        if ($this->buffer === '') {
×
NEW
132
            return null;
×
133
        }
134

NEW
135
        $result = $this->buffer;
×
NEW
136
        $this->buffer = '';
×
NEW
137
        return $result;
×
138
    }
139

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

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

156
            $this->fillBuffer($cancellation);
2✔
157

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

NEW
166
        return null;
×
167
    }
168

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

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

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

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

NEW
202
            $this->fillBuffer($cancellation);
×
203

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

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

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

NEW
228
        return null;
×
229
    }
230

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

237
        $chunk = $this->handle->read(CHUNK_SIZE, $cancellation);
8✔
238
        if ($chunk === '' && $this->handle->reachedEndOfDataSource()) {
8✔
239
            $this->eof = true;
5✔
240
            return;
5✔
241
        }
242

243
        if ($chunk !== '') {
7✔
244
            $this->buffer .= encode($chunk, $this->variant, $this->padding) . LINE_ENDING;
7✔
245
        }
246
    }
247
}
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