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

azjezz / psl / 23100780634

15 Mar 2026 01:37AM UTC coverage: 95.57% (-0.06%) from 95.628%
23100780634

push

github

web-flow
bc(io): `BufferedReadHandleInterface::readLine()` now always splits on `\n` instead of `PHP_EOL` (#630)

13 of 21 new or added lines in 7 files covered. (61.9%)

4 existing lines in 4 files now uncovered.

10462 of 10947 relevant lines covered (95.57%)

32.64 hits per line

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

67.48
/src/Psl/Encoding/QuotedPrintable/DecodingReadHandle.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 quoted_printable_decode;
13
use function str_ends_with;
14
use function strlen;
15
use function strpos;
16
use function substr;
17

18
/**
19
 * A read handle that decodes quoted-printable data from an inner readable handle.
20
 *
21
 * Reads lines from the inner handle, joins soft-break continuations (lines ending with `=`),
22
 * and decodes complete logical lines via {@see quoted_printable_decode()}.
23
 */
24
final class DecodingReadHandle implements IO\BufferedReadHandleInterface
25
{
26
    use IO\ReadHandleConvenienceMethodsTrait;
27

28
    private IO\Reader $reader;
29
    private string $buffer = '';
30
    private string $accumulated = '';
31
    private bool $eof = false;
32
    private bool $hardBreakPending = false;
33

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

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

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

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

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

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

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

73
        if ($this->buffer === '') {
6✔
74
            $this->fillBuffer($cancellation);
5✔
75
        }
76

77
        return $this->tryRead($max_bytes);
6✔
78
    }
79

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

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

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

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

100
    public function readLine(CancellationTokenInterface $cancellation = new NullCancellationToken()): null|string
101
    {
NEW
102
        $line = $this->readUntil("\n", $cancellation);
×
103
        if ($line !== null) {
×
NEW
104
            if ($line !== '' && $line[-1] === "\r") {
×
NEW
105
                return substr($line, 0, -1);
×
106
            }
107

UNCOV
108
            return $line;
×
109
        }
110

111
        // No EOL found; return whatever remains, or null if empty
112
        if ($this->buffer === '' && !$this->eof) {
×
113
            $this->fillBuffer($cancellation);
×
114
        }
115

116
        if ($this->buffer === '') {
×
117
            return null;
×
118
        }
119

120
        $result = $this->buffer;
×
121
        $this->buffer = '';
×
122
        return $result;
×
123
    }
124

125
    public function readUntil(
126
        string $suffix,
127
        CancellationTokenInterface $cancellation = new NullCancellationToken(),
128
    ): null|string {
129
        $suffix_len = strlen($suffix);
4✔
130
        $idx = strpos($this->buffer, $suffix);
4✔
131
        if ($idx !== false) {
4✔
132
            $result = substr($this->buffer, 0, $idx);
×
133
            $this->buffer = substr($this->buffer, $idx + $suffix_len);
×
134
            return $result;
×
135
        }
136

137
        while (!$this->eof) {
4✔
138
            $offset = strlen($this->buffer) - $suffix_len + 1;
4✔
139
            $offset = $offset > 0 ? $offset : 0;
4✔
140

141
            $this->fillBuffer($cancellation);
4✔
142

143
            $idx = strpos($this->buffer, $suffix, $offset);
4✔
144
            if ($idx !== false) {
4✔
145
                $result = substr($this->buffer, 0, $idx);
3✔
146
                $this->buffer = substr($this->buffer, $idx + $suffix_len);
3✔
147
                return $result;
3✔
148
            }
149
        }
150

151
        return null;
2✔
152
    }
153

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

170
            $result = substr($this->buffer, 0, $idx);
×
171
            $this->buffer = substr($this->buffer, $idx + $suffix_len);
×
172
            return $result;
×
173
        }
174

175
        if (strlen($this->buffer) > $max_bytes) {
3✔
176
            throw new IO\Exception\OverflowException(Psl\Str\format(
×
177
                'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
×
178
                $max_bytes,
×
179
                $suffix,
×
180
            ));
×
181
        }
182

183
        while (!$this->eof) {
3✔
184
            $offset = strlen($this->buffer) - $suffix_len + 1;
3✔
185
            $offset = $offset > 0 ? $offset : 0;
3✔
186

187
            $this->fillBuffer($cancellation);
3✔
188

189
            $idx = strpos($this->buffer, $suffix, $offset);
3✔
190
            if ($idx !== false) {
3✔
191
                if ($idx > $max_bytes) {
2✔
192
                    throw new IO\Exception\OverflowException(Psl\Str\format(
1✔
193
                        'Exceeded maximum byte limit (%d) before encountering the suffix ("%s").',
1✔
194
                        $max_bytes,
1✔
195
                        $suffix,
1✔
196
                    ));
1✔
197
                }
198

199
                $result = substr($this->buffer, 0, $idx);
1✔
200
                $this->buffer = substr($this->buffer, $idx + $suffix_len);
1✔
201
                return $result;
1✔
202
            }
203

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

213
        return null;
1✔
214
    }
215

216
    private function fillBuffer(CancellationTokenInterface $cancellation = new NullCancellationToken()): void
217
    {
218
        if ($this->eof) {
16✔
219
            return;
×
220
        }
221

222
        while (true) {
16✔
223
            $line = $this->reader->readUntil("\n", $cancellation);
16✔
224
            if ($line === null) {
16✔
225
                $remaining = $this->reader->read(null, $cancellation);
16✔
226
                if ($remaining !== '') {
16✔
227
                    $this->accumulated .= str_ends_with($remaining, "\r") ? substr($remaining, 0, -1) : $remaining;
14✔
228
                }
229

230
                if ($this->accumulated !== '') {
16✔
231
                    if ($this->hardBreakPending) {
14✔
232
                        $this->buffer .= "\r\n";
4✔
233
                    }
234

235
                    $this->buffer .= quoted_printable_decode($this->accumulated);
14✔
236
                    $this->accumulated = '';
14✔
237
                }
238

239
                $this->eof = true;
16✔
240

241
                return;
16✔
242
            }
243

244
            $line = str_ends_with($line, "\r") ? substr($line, 0, -1) : $line;
6✔
245

246
            if (str_ends_with($line, '=')) {
6✔
247
                /** @var non-negative-int $len */
248
                $len = strlen($line) - 1;
2✔
249
                $this->accumulated .= substr($line, 0, $len);
2✔
250

251
                continue;
2✔
252
            }
253

254
            $this->accumulated .= $line;
4✔
255

256
            if ($this->hardBreakPending) {
4✔
257
                $this->buffer .= "\r\n";
1✔
258
            }
259

260
            $this->buffer .= quoted_printable_decode($this->accumulated);
4✔
261
            $this->accumulated = '';
4✔
262
            $this->hardBreakPending = true;
4✔
263

264
            return;
4✔
265
        }
266
    }
267
}
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