Skip to content

Commit 3c85039

Browse files
committed
Add ContentEncoding enum
1 parent e4e818d commit 3c85039

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

src/Header/ContentEncoding.php

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of php-fast-forward/http-message.
7+
*
8+
* This source file is subject to the license bundled
9+
* with this source code in the file LICENSE.
10+
*
11+
* @link https://github.com/php-fast-forward/http-message
12+
* @copyright Copyright (c) 2025 Felipe Sayão Lobato Abreu <[email protected]>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Http\Message\Header;
17+
18+
/**
19+
* Enum ContentEncoding.
20+
*
21+
* Represents common and experimental HTTP Content-Encoding header values.
22+
* Content-Encoding defines the compression mechanism applied to the HTTP
23+
* message body. Implementations using this enum MUST follow the semantics
24+
* defined in RFC 7231, RFC 9110, and the relevant algorithm RFCs.
25+
*
26+
* Each encoding describes a specific compression algorithm or an identity
27+
* transformation. Servers and intermediaries using this enum SHOULD ensure
28+
* that content negotiation is performed safely and consistently according
29+
* to client capabilities, honoring q-values and alias mappings.
30+
*/
31+
enum ContentEncoding: string
32+
{
33+
/**
34+
* A format using the Lempel-Ziv coding (LZ77) with a 32-bit CRC.
35+
*
36+
* The HTTP/1.1 standard states that servers supporting this encoding
37+
* SHOULD also recognize `"x-gzip"` as an alias for compatibility.
38+
* Implementations consuming this enum MUST treat both forms as
39+
* equivalent during content negotiation.
40+
*/
41+
case Gzip = 'gzip';
42+
43+
/**
44+
* A format using the Lempel-Ziv-Welch (LZW) algorithm.
45+
*
46+
* Historically derived from the UNIX `compress` program. This encoding
47+
* is largely obsolete in modern HTTP contexts and SHOULD NOT be used
48+
* except for legacy interoperation.
49+
*/
50+
case Compress = 'compress';
51+
52+
/**
53+
* A format using the zlib framing structure (RFC 1950) with the
54+
* DEFLATE compression algorithm (RFC 1951).
55+
*
56+
* This encoding MUST NOT be confused with “raw deflate” streams.
57+
*/
58+
case Deflate = 'deflate';
59+
60+
/**
61+
* A format using the Brotli compression algorithm.
62+
*
63+
* Defined in RFC 7932, Brotli provides modern general-purpose
64+
* compression and SHOULD be preferred over older schemes such as gzip
65+
* when client support is present.
66+
*/
67+
case Brotli = 'br';
68+
69+
/**
70+
* A format using the Zstandard compression algorithm.
71+
*
72+
* Defined in RFC 8878, Zstandard (“zstd”) offers high compression
73+
* ratios and fast decompression. Implementations MAY use dictionary
74+
* compression where supported by the protocol extension.
75+
*/
76+
case Zstd = 'zstd';
77+
78+
/**
79+
* Indicates the identity function (no compression).
80+
*
81+
* The identity encoding MUST be considered acceptable if the client
82+
* omits an Accept-Encoding header. It MUST NOT apply any compression
83+
* transformation to the content.
84+
*/
85+
case Identity = 'identity';
86+
87+
/**
88+
* Experimental: A format using the Dictionary-Compressed Brotli algorithm.
89+
*
90+
* See the Compression Dictionary Transport specification. This encoding
91+
* is experimental and MAY NOT be supported by all clients.
92+
*/
93+
case Dcb = 'dcb';
94+
95+
/**
96+
* Experimental: A format using the Dictionary-Compressed Zstandard algorithm.
97+
*
98+
* See the Compression Dictionary Transport specification. This encoding
99+
* is experimental and MAY NOT be supported by all clients.
100+
*/
101+
case Dcz = 'dcz';
102+
103+
/**
104+
* Determines whether a given encoding is acceptable according to an
105+
* `Accept-Encoding` header value.
106+
*
107+
* This method MUST correctly apply HTTP content negotiation rules:
108+
* - Parse q-values, which MUST determine the client's preference level.
109+
* - Interpret “q=0” as explicit rejection.
110+
* - Support wildcards (“*”) as fallback.
111+
* - Recognize “x-gzip” as an alias for the gzip encoding.
112+
*
113+
* If an encoding is not explicitly listed and no wildcard is present,
114+
* the encoding SHOULD be considered acceptable unless the header
115+
* exclusively lists explicit rejections.
116+
*
117+
* @param self $encoding the encoding to evaluate
118+
* @param string $acceptEncodingHeader the raw `Accept-Encoding` header value
119+
*
120+
* @return bool true if the encoding is acceptable according to negotiation rules
121+
*/
122+
public static function isSupported(self $encoding, string $acceptEncodingHeader): bool
123+
{
124+
$preferences = [];
125+
$pattern = '/(?<name>[a-z*-]+)(?:;\s*q=(?<q>[0-9.]+))?/i';
126+
127+
if (\preg_match_all($pattern, $acceptEncodingHeader, $matches, PREG_SET_ORDER)) {
128+
foreach ($matches as $match) {
129+
$name = \mb_trim($match['name']);
130+
$q = isset($match['q']) && '' !== $match['q'] ? (float) $match['q'] : 1.0;
131+
$preferences[\mb_strtolower($name)] = $q;
132+
}
133+
}
134+
135+
$encodingName = \mb_strtolower($encoding->value);
136+
$aliases = self::getAliases($encoding);
137+
138+
$checkNames = [$encodingName, ...$aliases];
139+
140+
foreach ($checkNames as $name) {
141+
if (isset($preferences[$name])) {
142+
return $preferences[$name] > 0.0;
143+
}
144+
}
145+
146+
if (isset($preferences['*'])) {
147+
return $preferences['*'] > 0.0;
148+
}
149+
150+
return true;
151+
}
152+
153+
/**
154+
* Returns known alias names for a given encoding.
155+
*
156+
* Implementations MUST treat aliases as equivalent when performing
157+
* content negotiation. Currently only gzip uses an alias (“x-gzip”),
158+
* but future extensions MAY introduce additional aliases.
159+
*
160+
* @param self $encoding the encoding whose aliases will be returned
161+
*
162+
* @return string[] a list of lowercase alias identifiers
163+
*/
164+
private static function getAliases(self $encoding): array
165+
{
166+
return match ($encoding) {
167+
self::Gzip => ['x-gzip'],
168+
default => [],
169+
};
170+
}
171+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of php-fast-forward/http-message.
7+
*
8+
* This source file is subject to the license bundled
9+
* with this source code in the file LICENSE.
10+
*
11+
* @link https://github.com/php-fast-forward/http-message
12+
* @copyright Copyright (c) 2025 Felipe Sayão Lobato Abreu <[email protected]>
13+
* @license https://opensource.org/licenses/MIT MIT License
14+
*/
15+
16+
namespace FastForward\Http\Message\Tests\Header;
17+
18+
use FastForward\Http\Message\Header\ContentEncoding;
19+
use PHPUnit\Framework\Attributes\CoversClass;
20+
use PHPUnit\Framework\Attributes\DataProvider;
21+
use PHPUnit\Framework\TestCase;
22+
23+
/**
24+
* @internal
25+
*/
26+
#[CoversClass(ContentEncoding::class)]
27+
final class ContentEncodingTest extends TestCase
28+
{
29+
#[DataProvider('providerCases')]
30+
public function testCasesHaveCorrectValues(ContentEncoding $case, string $expected): void
31+
{
32+
self::assertSame($expected, $case->value);
33+
}
34+
35+
#[DataProvider('providerIsSupported')]
36+
public function testIsSupported(ContentEncoding $encoding, string $header, bool $expected): void
37+
{
38+
self::assertSame($expected, ContentEncoding::isSupported($encoding, $header));
39+
}
40+
41+
public static function providerCases(): array
42+
{
43+
return [
44+
[ContentEncoding::Gzip, 'gzip'],
45+
[ContentEncoding::Compress, 'compress'],
46+
[ContentEncoding::Deflate, 'deflate'],
47+
[ContentEncoding::Brotli, 'br'],
48+
[ContentEncoding::Zstd, 'zstd'],
49+
[ContentEncoding::Identity, 'identity'],
50+
[ContentEncoding::Dcb, 'dcb'],
51+
[ContentEncoding::Dcz, 'dcz'],
52+
];
53+
}
54+
55+
public static function providerIsSupported(): array
56+
{
57+
return [
58+
'explicitly accepted' => [ContentEncoding::Gzip, 'gzip, deflate, br', true],
59+
'another explicitly accepted' => [ContentEncoding::Brotli, 'gzip, deflate, br', true],
60+
'explicitly rejected' => [ContentEncoding::Gzip, 'gzip;q=0, deflate, br', false],
61+
'accepted by x-gzip alias' => [ContentEncoding::Gzip, 'x-gzip, deflate, br', true],
62+
'rejected by x-gzip alias' => [ContentEncoding::Gzip, 'x-gzip;q=0, deflate, br', false],
63+
'not mentioned but wildcard accepts' => [ContentEncoding::Brotli, 'gzip, *;q=0.5', true],
64+
'not mentioned and wildcard rejects' => [ContentEncoding::Brotli, 'gzip, *;q=0', false],
65+
'not mentioned and no wildcard (implicit accept)' => [ContentEncoding::Deflate, 'gzip, br', true],
66+
'empty header (implicit accept)' => [ContentEncoding::Gzip, '', true],
67+
'wildcard only' => [ContentEncoding::Zstd, '*', true],
68+
'identity is always accepted unless q=0' => [ContentEncoding::Identity, 'gzip, br', true],
69+
'identity explicitly rejected' => [ContentEncoding::Identity, 'identity;q=0', false],
70+
];
71+
}
72+
}

0 commit comments

Comments
 (0)