Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
74.07% |
20 / 27 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
| ChunkedBodyEncoder | |
74.07% |
20 / 27 |
|
0.00% |
0 / 2 |
15.95 | |
0.00% |
0 / 1 |
| encode | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
8.83 | |||
| encodeRemainderFromStream | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
5.68 | |||
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * SPDX-License-Identifier: EUPL-1.2 |
| 5 | * |
| 6 | * This file is part of icap-flow. |
| 7 | * |
| 8 | * Licensed under the EUPL, Version 1.2 only (the "Licence"); |
| 9 | * you may not use this work except in compliance with the Licence. |
| 10 | * You may obtain a copy of the Licence at: |
| 11 | * |
| 12 | * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 |
| 13 | * |
| 14 | * Unless required by applicable law or agreed to in writing, software |
| 15 | * distributed under the Licence is distributed on an "AS IS" basis, |
| 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 17 | */ |
| 18 | |
| 19 | declare(strict_types=1); |
| 20 | |
| 21 | namespace Ndrstmr\Icap; |
| 22 | |
| 23 | /** |
| 24 | * HTTP/1.1 chunked-transfer encoder used by both the {@see |
| 25 | * RequestFormatter} (for the encapsulated body inside an ICAP request) |
| 26 | * and by the strict RFC 3507 §4.5 preview-continue path (where the |
| 27 | * client sends the rest of the body on the same socket after the |
| 28 | * server's `100 Continue` response). |
| 29 | * |
| 30 | * Strings are emitted as a single chunk; resources are read in |
| 31 | * {@see self::CHUNK_SIZE} blocks so a multi-gigabyte body never needs |
| 32 | * to reside in memory. |
| 33 | */ |
| 34 | final class ChunkedBodyEncoder |
| 35 | { |
| 36 | public const int CHUNK_SIZE = 8192; |
| 37 | |
| 38 | /** |
| 39 | * @param string|resource $body |
| 40 | * @return iterable<string> |
| 41 | */ |
| 42 | public function encode(mixed $body, bool $previewIsComplete = false): iterable |
| 43 | { |
| 44 | $terminator = $previewIsComplete ? "0; ieof\r\n\r\n" : "0\r\n\r\n"; |
| 45 | |
| 46 | if (is_string($body)) { |
| 47 | if ($body !== '') { |
| 48 | yield dechex(strlen($body)) . "\r\n" . $body . "\r\n"; |
| 49 | } |
| 50 | yield $terminator; |
| 51 | return; |
| 52 | } |
| 53 | |
| 54 | if (!is_resource($body)) { |
| 55 | throw new \InvalidArgumentException( |
| 56 | 'Encapsulated HTTP body must be a string or a readable stream resource.', |
| 57 | ); |
| 58 | } |
| 59 | |
| 60 | rewind($body); |
| 61 | while (!feof($body)) { |
| 62 | $chunk = fread($body, self::CHUNK_SIZE); |
| 63 | if ($chunk === false || $chunk === '') { |
| 64 | break; |
| 65 | } |
| 66 | yield dechex(strlen($chunk)) . "\r\n" . $chunk . "\r\n"; |
| 67 | } |
| 68 | yield $terminator; |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Encode the remainder of an already-partially-read stream as |
| 73 | * HTTP/1.1 chunked-transfer. Reads from the **current** stream |
| 74 | * position (after the preview bytes) without rewinding. |
| 75 | * |
| 76 | * Used by the strict RFC 3507 §4.5 continuation path: the client |
| 77 | * has already sent the preview chunk, received `100 Continue`, and |
| 78 | * now streams the rest of the body on the same socket. |
| 79 | * |
| 80 | * @param resource $stream readable stream positioned past the preview |
| 81 | * @return iterable<string> |
| 82 | */ |
| 83 | public function encodeRemainderFromStream(mixed $stream): iterable |
| 84 | { |
| 85 | if (!is_resource($stream)) { |
| 86 | throw new \InvalidArgumentException( |
| 87 | 'Expected a readable stream resource.', |
| 88 | ); |
| 89 | } |
| 90 | |
| 91 | while (!feof($stream)) { |
| 92 | $chunk = fread($stream, self::CHUNK_SIZE); |
| 93 | if ($chunk === false || $chunk === '') { |
| 94 | break; |
| 95 | } |
| 96 | yield dechex(strlen($chunk)) . "\r\n" . $chunk . "\r\n"; |
| 97 | } |
| 98 | yield "0\r\n\r\n"; |
| 99 | } |
| 100 | } |