• 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

45.05
/src/Psl/Encoding/QuotedPrintable/EncodingReadHandle.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\Encoding\QuotedPrintable;
6

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

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

17
use const PHP_EOL;
18

19
/**
20
 * A read handle that encodes raw text from an inner readable handle using quoted-printable encoding.
21
 *
22
 * Reads line-by-line from the inner handle and encodes each line via {@see encode_line()}.
23
 */
24
final class EncodingReadHandle implements IO\BufferedReadHandleInterface
25
{
26
    use IO\ReadHandleConvenienceMethodsTrait;
27

28
    private IO\Reader $reader;
29
    private string $buffer = '';
30
    private bool $eof = false;
31
    private bool $firstLine = true;
32

33
    public function __construct(IO\ReadHandleInterface $handle)
34
    {
35
        $this->reader = new IO\Reader($handle);
10✔
36
    }
37

38
    public function reachedEndOfDataSource(): bool
39
    {
40
        return $this->eof && $this->buffer === '';
8✔
41
    }
42

43
    public function tryRead(null|int $max_bytes = null): string
44
    {
45
        if ($this->buffer === '' && !$this->eof) {
8✔
NEW
46
            $this->fillBuffer();
×
47
        }
48

49
        if ($this->buffer === '') {
8✔
50
            return '';
1✔
51
        }
52

53
        if (null === $max_bytes || $max_bytes >= strlen($this->buffer)) {
7✔
54
            $result = $this->buffer;
7✔
55
            $this->buffer = '';
7✔
56
            return $result;
7✔
57
        }
58

NEW
59
        $result = substr($this->buffer, 0, $max_bytes);
×
NEW
60
        $this->buffer = substr($this->buffer, $max_bytes);
×
NEW
61
        return $result;
×
62
    }
63

64
    public function read(
65
        null|int $max_bytes = null,
66
        CancellationTokenInterface $cancellation = new NullCancellationToken(),
67
    ): string {
68
        if ($this->eof && $this->buffer === '') {
8✔
NEW
69
            return '';
×
70
        }
71

72
        if ($this->buffer === '') {
8✔
73
            $this->fillBuffer($cancellation);
7✔
74
        }
75

76
        return $this->tryRead($max_bytes);
8✔
77
    }
78

79
    public function readByte(CancellationTokenInterface $cancellation = new NullCancellationToken()): string
80
    {
81
        if ($this->buffer === '' && !$this->eof) {
2✔
82
            $this->fillBuffer($cancellation);
2✔
83
        }
84

85
        if ($this->buffer === '') {
2✔
86
            throw new IO\Exception\RuntimeException('Reached EOF without any more data.');
1✔
87
        }
88

89
        $ret = $this->buffer[0];
1✔
90
        if ($ret === $this->buffer) {
1✔
91
            $this->buffer = '';
1✔
92
            return $ret;
1✔
93
        }
94

95
        $this->buffer = substr($this->buffer, 1);
1✔
96
        return $ret;
1✔
97
    }
98

99
    public function readLine(CancellationTokenInterface $cancellation = new NullCancellationToken()): null|string
100
    {
NEW
101
        $line = $this->readUntil(PHP_EOL, $cancellation);
×
NEW
102
        if ($line !== null) {
×
NEW
103
            return $line;
×
104
        }
105

106
        // No EOL found; return whatever remains, or null if empty
NEW
107
        if ($this->buffer === '' && !$this->eof) {
×
NEW
108
            $this->fillBuffer($cancellation);
×
109
        }
110

NEW
111
        if ($this->buffer === '') {
×
NEW
112
            return null;
×
113
        }
114

NEW
115
        $result = $this->buffer;
×
NEW
116
        $this->buffer = '';
×
NEW
117
        return $result;
×
118
    }
119

120
    public function readUntil(
121
        string $suffix,
122
        CancellationTokenInterface $cancellation = new NullCancellationToken(),
123
    ): null|string {
124
        $suffix_len = strlen($suffix);
1✔
125
        $idx = strpos($this->buffer, $suffix);
1✔
126
        if ($idx !== false) {
1✔
NEW
127
            $result = substr($this->buffer, 0, $idx);
×
NEW
128
            $this->buffer = substr($this->buffer, $idx + $suffix_len);
×
NEW
129
            return $result;
×
130
        }
131

132
        while (!$this->eof) {
1✔
133
            $offset = strlen($this->buffer) - $suffix_len + 1;
1✔
134
            $offset = $offset > 0 ? $offset : 0;
1✔
135

136
            $this->fillBuffer($cancellation);
1✔
137

138
            $idx = strpos($this->buffer, $suffix, $offset);
1✔
139
            if ($idx !== false) {
1✔
140
                $result = substr($this->buffer, 0, $idx);
1✔
141
                $this->buffer = substr($this->buffer, $idx + $suffix_len);
1✔
142
                return $result;
1✔
143
            }
144
        }
145

NEW
146
        return null;
×
147
    }
148

149
    public function readUntilBounded(
150
        string $suffix,
151
        int $max_bytes,
152
        CancellationTokenInterface $cancellation = new NullCancellationToken(),
153
    ): null|string {
NEW
154
        $suffix_len = strlen($suffix);
×
NEW
155
        $idx = strpos($this->buffer, $suffix);
×
NEW
156
        if ($idx !== false) {
×
NEW
157
            if ($idx > $max_bytes) {
×
NEW
158
                throw new IO\Exception\OverflowException(Psl\Str\format(
×
NEW
159
                    'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
NEW
160
                    $max_bytes,
×
NEW
161
                    $suffix,
×
NEW
162
                ));
×
163
            }
164

NEW
165
            $result = substr($this->buffer, 0, $idx);
×
NEW
166
            $this->buffer = substr($this->buffer, $idx + $suffix_len);
×
NEW
167
            return $result;
×
168
        }
169

NEW
170
        if (strlen($this->buffer) > $max_bytes) {
×
NEW
171
            throw new IO\Exception\OverflowException(Psl\Str\format(
×
NEW
172
                'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
NEW
173
                $max_bytes,
×
NEW
174
                $suffix,
×
NEW
175
            ));
×
176
        }
177

NEW
178
        while (!$this->eof) {
×
NEW
179
            $offset = strlen($this->buffer) - $suffix_len + 1;
×
NEW
180
            $offset = $offset > 0 ? $offset : 0;
×
181

NEW
182
            $this->fillBuffer($cancellation);
×
183

NEW
184
            $idx = strpos($this->buffer, $suffix, $offset);
×
NEW
185
            if ($idx !== false) {
×
NEW
186
                if ($idx > $max_bytes) {
×
NEW
187
                    throw new IO\Exception\OverflowException(Psl\Str\format(
×
NEW
188
                        'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
NEW
189
                        $max_bytes,
×
NEW
190
                        $suffix,
×
NEW
191
                    ));
×
192
                }
193

NEW
194
                $result = substr($this->buffer, 0, $idx);
×
NEW
195
                $this->buffer = substr($this->buffer, $idx + $suffix_len);
×
NEW
196
                return $result;
×
197
            }
198

NEW
199
            if (strlen($this->buffer) > $max_bytes) {
×
NEW
200
                throw new IO\Exception\OverflowException(Psl\Str\format(
×
NEW
201
                    'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
NEW
202
                    $max_bytes,
×
NEW
203
                    $suffix,
×
NEW
204
                ));
×
205
            }
206
        }
207

NEW
208
        return null;
×
209
    }
210

211
    private function fillBuffer(CancellationTokenInterface $cancellation = new NullCancellationToken()): void
212
    {
213
        if ($this->eof) {
10✔
NEW
214
            return;
×
215
        }
216

217
        $line = $this->reader->readUntil("\n", $cancellation);
10✔
218
        if ($line !== null) {
10✔
219
            $line = str_ends_with($line, "\r") ? substr($line, 0, -1) : $line;
2✔
220

221
            if (!$this->firstLine) {
2✔
NEW
222
                $this->buffer .= "\r\n";
×
223
            }
224

225
            $this->buffer .= encode_line($line);
2✔
226
            $this->firstLine = false;
2✔
227

228
            return;
2✔
229
        }
230

231
        $remaining = $this->reader->read(null, $cancellation);
10✔
232
        if ($remaining !== '') {
10✔
233
            $remaining = str_ends_with($remaining, "\r") ? substr($remaining, 0, -1) : $remaining;
8✔
234

235
            if (!$this->firstLine) {
8✔
236
                $this->buffer .= "\r\n";
2✔
237
            }
238

239
            $this->buffer .= encode_line($remaining);
8✔
240
        }
241

242
        $this->eof = true;
10✔
243
    }
244
}
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