DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_zlib.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ssh_zlib.cpp
6 * @brief SSH server-to-client context-takeover DEFLATE - implementation.
7 *
8 * A fixed-Huffman (BTYPE=01) greedy-LZ77 compressor, like presentation/deflate, but STREAMING: it
9 * keeps a persistent sliding window across packets (context takeover) and wraps the output as a zlib
10 * stream (RFC 1950 header once) with a Z_SYNC_FLUSH boundary per packet. Because a new packet's
11 * matches may reach back into prior packets' bytes, each call runs LZ77 over [history || input] but
12 * only EMITS tokens for the input; the history is inserted into the hash chains first (search only,
13 * no output). Hash chains are rebuilt each call over the (possibly slid) history, so a window slide
14 * needs no chain relocation. Bits are packed LSB-first; Huffman codes are stored bit-reversed so
15 * writing them LSB-first puts them on the wire MSB-first (RFC 1951 sec 3.1.1).
16 *
17 * The one architectural difference from the WebSocket compressor: it KEEPS the `00 00 ff ff` sync
18 * marker on the wire (SSH sends it; permessage-deflate strips it) and prepends the 2-byte zlib
19 * header on the first packet. (The LZ77+Huffman core is deliberately a sibling of presentation/
20 * deflate rather than a shared instance - the stateful vs stateless split is fundamental; unifying
21 * the two behind one parameterized streaming codec is a follow-up for the core review.)
22 */
23
25
26#if DETWS_ENABLE_SSH_ZLIB
27
28#include <string.h>
29
30namespace
31{
32constexpr int MIN_MATCH = 3; // shortest LZ77 back-reference
33constexpr int MAX_MATCH = 258; // longest (RFC 1951 length code 285)
34constexpr int HASH_MASK = SSH_ZLIB_HASH_SIZE - 1;
35constexpr int WINDOW = DETWS_SSH_ZLIB_WINDOW; // max back-reference distance (power of two)
36constexpr int MAX_CHAIN = 128; // bounded hash-chain walk per position
37constexpr uint16_t NONE = 0xFFFF; // empty hash slot / chain terminator
38
39// Length code base values and extra bits (RFC 1951 sec 3.2.5), codes 257..285.
40const short LEN_BASE[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27,
41 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
42const short LEN_EXTRA[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
43
44// Distance code base values and extra bits, codes 0..29.
45const short DIST_BASE[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129,
46 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
47const short DIST_EXTRA[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
48 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
49
50// Reverse the low @p len bits of @p code (Huffman codes go on the wire MSB-first).
51uint16_t reverse_bits(uint16_t code, int len)
52{
53 uint16_t r = 0;
54 for (int k = 0; k < len; k++)
55 {
56 r = (uint16_t)((r << 1) | (code & 1));
57 code >>= 1;
58 }
59 return r;
60}
61
62// Build the fixed Huffman code/length tables (RFC 1951 sec 3.2.6), storing each code bit-reversed so
63// it can be emitted LSB-first. Identical assignment to inflate/deflate's fixed() so the two agree.
64void build_fixed(SshDeflate *z)
65{
66 int sym = 0;
67 for (; sym < 144; sym++)
68 z->ll_len[sym] = 8;
69 for (; sym < 256; sym++)
70 z->ll_len[sym] = 9;
71 for (; sym < 280; sym++)
72 z->ll_len[sym] = 7;
73 for (; sym < 288; sym++)
74 z->ll_len[sym] = 8;
75 for (sym = 0; sym < 30; sym++)
76 z->d_len[sym] = 5;
77
78 uint16_t bl_count[16];
79 memset(bl_count, 0, sizeof(bl_count));
80 for (sym = 0; sym < 288; sym++)
81 bl_count[z->ll_len[sym]]++;
82 uint16_t next_code[16];
83 next_code[0] = 0;
84 uint16_t code = 0;
85 for (int b = 1; b <= 15; b++)
86 {
87 code = (uint16_t)((code + bl_count[b - 1]) << 1);
88 next_code[b] = code;
89 }
90 for (sym = 0; sym < 288; sym++)
91 {
92 int len = z->ll_len[sym];
93 z->ll_code[sym] = reverse_bits(next_code[len]++, len);
94 }
95 for (sym = 0; sym < 30; sym++)
96 z->d_code[sym] = reverse_bits((uint16_t)sym, 5);
97}
98
99// LSB-first bit writer over the caller's output buffer.
100struct BitWriter
101{
102 uint8_t *out;
103 size_t cap;
104 size_t cnt;
105 uint32_t acc; // bit accumulator (LSB-first)
106 int nbits; // bits currently buffered (< 8 between calls)
107 bool overflow;
108};
109
110void put_bits(BitWriter *w, uint32_t bits, int n)
111{
112 w->acc |= bits << w->nbits;
113 w->nbits += n;
114 while (w->nbits >= 8)
115 {
116 if (w->cnt >= w->cap)
117 {
118 w->overflow = true;
119 return;
120 }
121 w->out[w->cnt++] = (uint8_t)(w->acc & 0xFF);
122 w->acc >>= 8;
123 w->nbits -= 8;
124 }
125}
126
127// Flush any partial byte, padding the high bits with zero (byte alignment).
128void align_byte(BitWriter *w)
129{
130 if (w->nbits > 0)
131 {
132 if (w->cnt >= w->cap)
133 {
134 w->overflow = true;
135 return;
136 }
137 w->out[w->cnt++] = (uint8_t)(w->acc & 0xFF);
138 w->acc = 0;
139 w->nbits = 0;
140 }
141}
142
143// Emit one raw byte (only valid on a byte boundary: the zlib header and sync marker).
144void put_byte(BitWriter *w, uint8_t b)
145{
146 if (w->cnt >= w->cap)
147 {
148 w->overflow = true;
149 return;
150 }
151 w->out[w->cnt++] = b;
152}
153
154// 3-byte rolling hash into a SSH_ZLIB_HASH_SIZE bucket.
155inline int hash3(const uint8_t *p)
156{
157 return (int)(((uint32_t)p[0] << 10 ^ (uint32_t)p[1] << 5 ^ (uint32_t)p[2]) & HASH_MASK);
158}
159
160void emit_literal(BitWriter *w, const SshDeflate *z, uint8_t b)
161{
162 put_bits(w, z->ll_code[b], z->ll_len[b]);
163}
164
165void emit_match(BitWriter *w, const SshDeflate *z, int len, int dist)
166{
167 int li = 0;
168 while (li < 28 && len >= LEN_BASE[li + 1])
169 li++;
170 int lsym = 257 + li;
171 put_bits(w, z->ll_code[lsym], z->ll_len[lsym]);
172 if (LEN_EXTRA[li])
173 put_bits(w, (uint32_t)(len - LEN_BASE[li]), LEN_EXTRA[li]);
174
175 int di = 0;
176 while (di < 29 && dist >= DIST_BASE[di + 1])
177 di++;
178 put_bits(w, z->d_code[di], z->d_len[di]);
179 if (DIST_EXTRA[di])
180 put_bits(w, (uint32_t)(dist - DIST_BASE[di]), DIST_EXTRA[di]);
181}
182
183// Walk the hash chain from cand (newest-first) for the longest match of buf[i..] within WINDOW.
184// Writes the best match length/distance found (both 0 if none).
185void zlib_chain_match(const SshDeflate *z, const uint8_t *buf, size_t i, uint16_t cand, int chain, size_t max_len,
186 int *best_len, int *best_dist)
187{
188 *best_len = 0;
189 *best_dist = 0;
190 while (cand != NONE && chain > 0)
191 {
192 chain--;
193 size_t dist = i - cand;
194 if (dist > (size_t)WINDOW)
195 break; // chain is newest-first; everything past here is farther
196 size_t l = 0;
197 while (l < max_len && buf[cand + l] == buf[i + l])
198 l++;
199 if ((int)l > *best_len)
200 {
201 *best_len = (int)l;
202 *best_dist = (int)dist;
203 if (l >= max_len)
204 break;
205 }
206 cand = z->prev[cand];
207 }
208}
209} // namespace
210
211void ssh_deflate_init(SshDeflate *z, uint8_t *work, uint16_t *head, uint16_t *prev, uint16_t *ll_code, uint8_t *ll_len,
212 uint16_t *d_code, uint8_t *d_len)
213{
214 z->work = work;
215 z->head = head;
216 z->prev = prev;
217 z->ll_code = ll_code;
218 z->ll_len = ll_len;
219 z->d_code = d_code;
220 z->d_len = d_len;
221 z->hist = 0;
222 z->header_sent = false;
223 build_fixed(z);
224}
225
226int ssh_deflate_packet(SshDeflate *z, const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap, size_t *out_len)
227{
228 if (src_len > (size_t)DETWS_SSH_ZLIB_MAX_IN)
229 return -1;
230
231 // Lay [history || input] out contiguously; matches for the input may reach back into history.
232 size_t hist = z->hist;
233 if (hist + src_len > SSH_ZLIB_WORK_SIZE)
234 return -1; // sizing invariant (hist <= WINDOW, src_len <= MAX_IN) should prevent this
235 memcpy(z->work + hist, src, src_len);
236 size_t total = hist + src_len;
237 const uint8_t *buf = z->work;
238
239 // Rebuild the hash over the history (search-only) so input matches can reference it.
240 for (int b = 0; b < SSH_ZLIB_HASH_SIZE; b++)
241 z->head[b] = NONE;
242 for (size_t p = 0; p + MIN_MATCH <= total && p < hist; p++)
243 {
244 int h = hash3(buf + p);
245 z->prev[p] = z->head[h];
246 z->head[h] = (uint16_t)p;
247 }
248
249 BitWriter w;
250 w.out = dst;
251 w.cap = dst_cap;
252 w.cnt = 0;
253 w.acc = 0;
254 w.nbits = 0;
255 w.overflow = false;
256
257 // RFC 1950 zlib header, once at stream start: CMF=0x78 (deflate, 32 KB window), FLG=0x9C
258 // (default level, FCHECK making 0x789C divisible by 31). Byte-aligned, before any deflate bits.
259 if (!z->header_sent)
260 {
261 put_byte(&w, 0x78);
262 put_byte(&w, 0x9C);
263 z->header_sent = true;
264 }
265
266 // One fixed-Huffman block, not final: BFINAL=0 (1 bit), BTYPE=01 (2 bits, value 1).
267 put_bits(&w, 0, 1);
268 put_bits(&w, 1, 2);
269
270 size_t i = hist; // emit tokens only for the new input
271 while (i < total)
272 {
273 int best_len = 0;
274 int best_dist = 0;
275
276 if (i + MIN_MATCH <= total)
277 {
278 int h = hash3(buf + i);
279 uint16_t cand = z->head[h];
280 size_t max_len = total - i;
281 if (max_len > (size_t)MAX_MATCH)
282 max_len = MAX_MATCH;
283 zlib_chain_match(z, buf, i, cand, MAX_CHAIN, max_len, &best_len, &best_dist);
284 }
285
286 size_t advance;
287 if (best_len >= MIN_MATCH)
288 {
289 emit_match(&w, z, best_len, best_dist);
290 advance = (size_t)best_len;
291 }
292 else
293 {
294 emit_literal(&w, z, buf[i]);
295 advance = 1;
296 }
297
298 // Insert each consumed position into the hash chains for later matches. The byte comparison
299 // above validates every candidate, so a stale insert can only cost ratio, never correctness.
300 size_t end = i + advance;
301 while (i < end)
302 {
303 if (i + MIN_MATCH <= total)
304 {
305 int h = hash3(buf + i);
306 z->prev[i] = z->head[h];
307 z->head[h] = (uint16_t)i;
308 }
309 i++;
310 }
311 }
312
313 // End-of-block, then a Z_SYNC_FLUSH: byte-align via an empty stored block and KEEP its
314 // 0x00 0x00 0xff 0xff tail on the wire (SSH sends it; a zlib inflate() flushes at the boundary).
315 put_bits(&w, z->ll_code[256], z->ll_len[256]); // end-of-block symbol
316 put_bits(&w, 0, 1); // BFINAL=0 (empty stored block)
317 put_bits(&w, 0, 2); // BTYPE=00 (stored)
318 align_byte(&w);
319 put_byte(&w, 0x00);
320 put_byte(&w, 0x00);
321 put_byte(&w, 0xff);
322 put_byte(&w, 0xff);
323
324 if (w.overflow)
325 return -1;
326
327 // Slide the window: keep the last WINDOW bytes of [history || input] as history for next time.
328 size_t keep = total;
329 if (keep > (size_t)WINDOW)
330 keep = WINDOW;
331 if (keep < total)
332 memmove(z->work, z->work + total - keep, keep);
333 z->hist = keep;
334
335 *out_len = w.cnt;
336 return 0;
337}
338
339#endif // DETWS_ENABLE_SSH_ZLIB
#define DETWS_SSH_ZLIB_WINDOW
SSH s2c DEFLATE sliding-window size in bytes (max back-reference distance). Power of two,...
#define DETWS_SSH_ZLIB_MAX_IN
Largest uncompressed payload the s2c compressor accepts in one call (bytes). Outbound SSH payloads ar...
SSH server-to-client compression: a context-takeover DEFLATE stream (no heap).