Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
ContentEncoding
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
2 / 2
11
100.00% covered (success)
100.00%
1 / 1
 isSupported
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 getAliases
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(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 <github@mentordosnerds.com>
13 * @license   https://opensource.org/licenses/MIT MIT License
14 */
15
16namespace 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 */
31enum 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}