• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sirn-se / websocket-php / 14221063815

02 Apr 2025 01:37PM UTC coverage: 99.675% (-0.3%) from 100.0%
14221063815

push

github

sirn-se
Compressor: Per-MEssage Deflate

165 of 169 new or added lines in 7 files covered. (97.63%)

1228 of 1232 relevant lines covered (99.68%)

23.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.03
/src/Middleware/CompressionExtension/DeflateCompressor.php
1
<?php
2

3
/**
4
 * Copyright (C) 2014-2025 Textalk and contributors.
5
 *
6
 * This file is part of Websocket PHP and is free software under the ISC License.
7
 * License text: https://raw.githubusercontent.com/sirn-se/websocket-php/master/COPYING.md
8
 */
9

10
namespace WebSocket\Middleware\CompressionExtension;
11

12
use DeflateContext;
13
use InflateContext;
14
use RangeException;
15
use RuntimeException;
16
use stdClass;
17
use Stringable;
18
use WebSocket\Message\{
19
    Binary,
20
    Message,
21
    Text,
22
};
23
use WebSocket\Trait\StringableTrait;
24

25
/**
26
 * WebSocket\Middleware\PerMessageCompression\DeflateCompressor class.
27
 * Handles compression using permessage-deflate.
28
 * @see https://datatracker.ietf.org/doc/html/rfc7692
29
 * @phpstan-type Config stdClass&object{
30
 *   compressor: mixed,
31
 *   isServer: bool,
32
 *   deflator: DeflateContext|null,
33
 *   inflator: InflateContext|null,
34
 *   isServer: bool,
35
 *   serverNoContextTakeover: bool,
36
 *   clientNoContextTakeover: bool,
37
 *   serverMaxWindowBits: int<self::MIN_WINDOW_SIZE, self::MAX_WINDOW_SIZE>,
38
 *   clientMaxWindowBits: int<self::MIN_WINDOW_SIZE, self::MAX_WINDOW_SIZE>,
39
 * }
40
 */
41
class DeflateCompressor implements CompressorInterface, Stringable
42
{
43
    use StringableTrait;
44

45
    private const MIN_WINDOW_SIZE = 9;
46
    private const MAX_WINDOW_SIZE = 15;
47

48
    private bool $serverNoContextTakeover;
49
    private bool $clientNoContextTakeover;
50
    /** @var int<self::MIN_WINDOW_SIZE, self::MAX_WINDOW_SIZE> $serverMaxWindowBits */
51
    private int $serverMaxWindowBits;
52
    /** @var int<self::MIN_WINDOW_SIZE, self::MAX_WINDOW_SIZE> $clientMaxWindowBits */
53
    private int $clientMaxWindowBits;
54

55
    public function __construct(
56
        bool $serverNoContextTakeover = false,
57
        bool $clientNoContextTakeover = false,
58
        int $serverMaxWindowBits = self::MAX_WINDOW_SIZE,
59
        int $clientMaxWindowBits = self::MAX_WINDOW_SIZE,
60
    ) {
61
        if (!extension_loaded('zlib')) {
7✔
NEW
62
            throw new RuntimeException("DeflateCompressor require zlib extension.");
×
63
        }
64
        if ($serverMaxWindowBits < self::MIN_WINDOW_SIZE || $serverMaxWindowBits > self::MAX_WINDOW_SIZE) {
7✔
NEW
65
            throw new RangeException("DeflateCompressor serverMaxWindowBits must be in range 9-15.");
×
66
        }
67
        if ($clientMaxWindowBits < self::MIN_WINDOW_SIZE || $clientMaxWindowBits > self::MAX_WINDOW_SIZE) {
7✔
NEW
68
            throw new RangeException("DeflateCompressor clientMaxWindowBits must be in range 9-15.");
×
69
        }
70
        $this->serverNoContextTakeover = $serverNoContextTakeover;
7✔
71
        $this->clientNoContextTakeover = $clientNoContextTakeover;
7✔
72
        $this->serverMaxWindowBits = $serverMaxWindowBits;
7✔
73
        $this->clientMaxWindowBits = $clientMaxWindowBits;
7✔
74
    }
75

76
    public function getRequestHeaderValue(): string
77
    {
78
        $header = "permessage-deflate";
4✔
79
        if ($this->serverNoContextTakeover) {
4✔
80
            $header .= "; server_no_context_takeover";
1✔
81
        }
82
        if ($this->clientNoContextTakeover) {
4✔
83
            $header .= "; client_no_context_takeover";
1✔
84
        }
85
        if ($this->serverMaxWindowBits != self::MAX_WINDOW_SIZE) {
4✔
86
            $header .= "; server_max_window_bits={$this->serverMaxWindowBits}";
2✔
87
        }
88
        if ($this->clientMaxWindowBits != self::MAX_WINDOW_SIZE) {
4✔
89
            $header .= "; client_max_window_bits={$this->clientMaxWindowBits}";
2✔
90
        }
91
        return $header;
4✔
92
    }
93

94
    /**
95
     * @param Config $configuration
96
     */
97
    public function getResponseHeaderValue(object $configuration): string
98
    {
99
        // @todo: throw HandshakeException or bad config
100
        $header = "permessage-deflate";
3✔
101
        if ($configuration->serverNoContextTakeover) {
3✔
102
            $header .= "; server_no_context_takeover";
2✔
103
        }
104
        if ($configuration->clientNoContextTakeover) {
3✔
105
            $header .= "; client_no_context_takeover";
2✔
106
        }
107
        $serverMaxWindowBits = min($configuration->serverMaxWindowBits, $this->serverMaxWindowBits);
3✔
108
        if ($serverMaxWindowBits != self::MAX_WINDOW_SIZE) {
3✔
109
            $header .= "; server_max_window_bits={$serverMaxWindowBits}";
2✔
110
        }
111
        $clientMaxWindowBits = min($configuration->clientMaxWindowBits, $this->clientMaxWindowBits);
3✔
112
        if ($clientMaxWindowBits != self::MAX_WINDOW_SIZE) {
3✔
113
            $header .= "; client_max_window_bits={$clientMaxWindowBits}";
2✔
114
        }
115
        return $header;
3✔
116
    }
117

118
    /**
119
     * @param Config $configuration
120
     */
121
    public function isEligable(object $configuration): bool
122
    {
123
        return
6✔
124
            $configuration->compressor == 'permessage-deflate'
6✔
125
            && $configuration->serverMaxWindowBits <= $this->serverMaxWindowBits
6✔
126
            && $configuration->clientMaxWindowBits <= $this->clientMaxWindowBits
6✔
127
            ;
6✔
128
    }
129

130
    /**
131
     * @return Config
132
     */
133
    public function getConfiguration(string $element, bool $isServer): object
134
    {
135
        $configuration = (object)[
6✔
136
            'compressor' => null,
6✔
137
            'isServer' => $isServer,
6✔
138
            'serverNoContextTakeover' => $this->serverNoContextTakeover,
6✔
139
            'clientNoContextTakeover' => $this->clientNoContextTakeover,
6✔
140
            'serverMaxWindowBits' => $this->serverMaxWindowBits,
6✔
141
            'clientMaxWindowBits' => $this->clientMaxWindowBits,
6✔
142
            'deflator' => null,
6✔
143
            'inflator' => null,
6✔
144
        ];
6✔
145
        foreach (explode(';', $element) as $parameter) {
6✔
146
            $parts = explode('=', $parameter);
6✔
147
            $key = trim($parts[0]);
6✔
148
            // @todo: Error handling when parsing
149
            switch ($key) {
150
                case 'permessage-deflate':
6✔
151
                    $configuration->compressor = $key;
6✔
152
                    break;
6✔
153
                case 'server_no_context_takeover':
2✔
154
                    $configuration->serverNoContextTakeover = true;
1✔
155
                    break;
1✔
156
                case 'client_no_context_takeover':
2✔
157
                    $configuration->clientNoContextTakeover = true;
1✔
158
                    break;
1✔
159
                case 'server_max_window_bits':
2✔
160
                    $bits = intval($parts[1] ?? self::MAX_WINDOW_SIZE);
2✔
161
                    $configuration->serverMaxWindowBits = min($bits, $this->serverMaxWindowBits);
2✔
162
                    break;
2✔
163
                case 'client_max_window_bits':
2✔
164
                    $bits = intval($parts[1] ?? self::MAX_WINDOW_SIZE);
2✔
165
                    $configuration->clientMaxWindowBits = min($bits, $this->clientMaxWindowBits);
2✔
166
                    break;
2✔
167
            }
168
        }
169
        return $configuration;
6✔
170
    }
171

172
    /**
173
     * @template T of Binary|Text
174
     * @param T $message
175
     * @param Config $configuration
176
     * @return T
177
     */
178
    public function compress(Binary|Text $message, object $configuration): Binary|Text
179
    {
180
        $windowBits = $configuration->isServer
5✔
181
            ? $configuration->serverMaxWindowBits
3✔
182
            : $configuration->clientMaxWindowBits;
2✔
183
        $noContextTakeover = $configuration->isServer
5✔
184
            ? $configuration->serverNoContextTakeover
3✔
185
            : $configuration->clientNoContextTakeover;
2✔
186

187
        if (is_null($configuration->deflator) || $noContextTakeover) {
5✔
188
            $configuration->deflator = deflate_init(ZLIB_ENCODING_RAW, [
5✔
189
                'level' => -1,
5✔
190
                'window' => $windowBits,
5✔
191
                'strategy' => ZLIB_DEFAULT_STRATEGY
5✔
192
            ]) ?: null;
5✔
193
        }
194
        /** @var string $deflated */
195
        $deflated = deflate_add($configuration->deflator, $message->getPayload(), ZLIB_SYNC_FLUSH);
5✔
196
        $deflated = substr($deflated, 0, -4); // Remove 4 last chars
5✔
197
        $message->setCompress(true);
5✔
198
        $message->setPayload($deflated);
5✔
199
        return $message;
5✔
200
    }
201

202
    /**
203
     * @param Config $configuration
204
     */
205
    public function decompress(Binary|Text $message, object $configuration): Binary|Text
206
    {
207
        $windowBits = $configuration->isServer
5✔
208
            ? $configuration->clientMaxWindowBits
3✔
209
            : $configuration->serverMaxWindowBits;
2✔
210
        $noContextTakeover = $configuration->isServer
5✔
211
            ? $configuration->clientNoContextTakeover
3✔
212
            : $configuration->serverNoContextTakeover;
2✔
213

214
        if (is_null($configuration->inflator) || $noContextTakeover) {
5✔
215
            $configuration->inflator = inflate_init(ZLIB_ENCODING_RAW, [
5✔
216
                'level' => -1,
5✔
217
                'window' => $windowBits,
5✔
218
                'strategy' => ZLIB_DEFAULT_STRATEGY
5✔
219
            ]) ?: null;
5✔
220
        }
221
        /** @var string $inflated */
222
        $inflated = inflate_add($configuration->inflator, $message->getPayload() . "\x00\x00\xff\xff");
5✔
223
        $message->setCompress(false);
5✔
224
        $message->setPayload($inflated);
5✔
225
        return $message;
5✔
226
    }
227
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc