Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.07% covered (warning)
74.07%
20 / 27
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChunkedBodyEncoder
74.07% covered (warning)
74.07%
20 / 27
0.00% covered (danger)
0.00%
0 / 2
15.95
0.00% covered (danger)
0.00%
0 / 1
 encode
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
8.83
 encodeRemainderFromStream
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
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
19declare(strict_types=1);
20
21namespace 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 */
34final 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}