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