ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 PC_ENABLE_SSH_ZLIB
27
29
30#include <string.h>
31
32namespace
33{
34#define PC_MIN_MATCH 3 // shortest LZ77 back-reference
35#define PC_MAX_MATCH 258 // longest (RFC 1951 length code 285)
36#define PC_HASH_MASK (SSH_ZLIB_HASH_SIZE - 1)
37#define PC_WINDOW PC_SSH_ZLIB_WINDOW // max back-reference distance (power of two)
38#define PC_MAX_CHAIN 128 // bounded hash-chain walk per position
39#define PC_NONE 0xFFFF // empty hash slot / chain terminator
40
41// Length code base values and extra bits (RFC 1951 sec 3.2.5), codes 257..285.
42const short LEN_BASE[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27,
43 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
44const 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};
45
46// Distance code base values and extra bits, codes 0..29.
47const short DIST_BASE[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129,
48 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
49const short DIST_EXTRA[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
50 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
51
52// Reverse the low @p len bits of @p code (Huffman codes go on the wire MSB-first).
53uint16_t reverse_bits(uint16_t code, int len)
54{
55 uint16_t r = 0;
56 for (int k = 0; k < len; k++)
57 {
58 r = (uint16_t)((r << 1) | (code & 1));
59 code >>= 1;
60 }
61 return r;
62}
63
64// Build the fixed Huffman code/length tables (RFC 1951 sec 3.2.6), storing each code bit-reversed so
65// it can be emitted LSB-first. Identical assignment to inflate/deflate's fixed() so the two agree.
66void build_fixed(SshDeflate *z)
67{
68 int sym = 0;
69 for (; sym < 144; sym++)
70 {
71 z->ll_len[sym] = 8;
72 }
73 for (; sym < 256; sym++)
74 {
75 z->ll_len[sym] = 9;
76 }
77 for (; sym < 280; sym++)
78 {
79 z->ll_len[sym] = 7;
80 }
81 for (; sym < 288; sym++)
82 {
83 z->ll_len[sym] = 8;
84 }
85 for (sym = 0; sym < 30; sym++)
86 {
87 z->d_len[sym] = 5;
88 }
89
90 uint16_t bl_count[16];
91 memset(bl_count, 0, sizeof(bl_count));
92 for (sym = 0; sym < 288; sym++)
93 {
94 bl_count[z->ll_len[sym]]++;
95 }
96 uint16_t next_code[16];
97 next_code[0] = 0;
98 uint16_t code = 0;
99 for (int b = 1; b <= 15; b++)
100 {
101 code = (uint16_t)((code + bl_count[b - 1]) << 1);
102 next_code[b] = code;
103 }
104 for (sym = 0; sym < 288; sym++)
105 {
106 int len = z->ll_len[sym];
107 z->ll_code[sym] = reverse_bits(next_code[len]++, len);
108 }
109 for (sym = 0; sym < 30; sym++)
110 {
111 z->d_code[sym] = reverse_bits((uint16_t)sym, 5);
112 }
113}
114
115// Emit one raw byte (only valid on a byte boundary: the zlib header and sync marker).
116void put_byte(pc_bit_writer *w, uint8_t b)
117{
118 if (w->cnt >= w->cap)
119 {
120 w->overflow = true;
121 return;
122 }
123 w->out[w->cnt++] = b;
124}
125
126// 3-byte rolling hash into a SSH_ZLIB_HASH_SIZE bucket.
127inline int hash3(const uint8_t *p)
128{
129 return (int)(((uint32_t)p[0] << 10 ^ (uint32_t)p[1] << 5 ^ (uint32_t)p[2]) & PC_HASH_MASK);
130}
131
132void emit_literal(pc_bit_writer *w, const SshDeflate *z, uint8_t b)
133{
134 pc_bitw_put(w, z->ll_code[b], z->ll_len[b]);
135}
136
137void emit_match(pc_bit_writer *w, const SshDeflate *z, int len, int dist)
138{
139 int li = 0;
140 while (li < 28 && len >= LEN_BASE[li + 1])
141 {
142 li++;
143 }
144 int lsym = 257 + li;
145 pc_bitw_put(w, z->ll_code[lsym], z->ll_len[lsym]);
146 if (LEN_EXTRA[li])
147 {
148 pc_bitw_put(w, (uint32_t)(len - LEN_BASE[li]), LEN_EXTRA[li]);
149 }
150
151 int di = 0;
152 while (di < 29 && dist >= DIST_BASE[di + 1]) // GCOVR_EXCL_BR_LINE di==29 exhaustion is unreachable:
153 {
154 di++; // zlib_chain_match caps dist to PC_WINDOW (8192) < DIST_BASE[26] (8193), so di never passes 25
155 }
156 pc_bitw_put(w, z->d_code[di], z->d_len[di]);
157 if (DIST_EXTRA[di])
158 {
159 pc_bitw_put(w, (uint32_t)(dist - DIST_BASE[di]), DIST_EXTRA[di]);
160 }
161}
162
163// Walk the hash chain from cand (newest-first) for the longest match of buf[i..] within PC_WINDOW.
164// Writes the best match length/distance found (both 0 if none).
165void zlib_chain_match(const SshDeflate *z, const uint8_t *buf, size_t i, uint16_t cand, int chain, size_t max_len,
166 int *best_len, int *best_dist)
167{
168 *best_len = 0;
169 *best_dist = 0;
170 while (cand != PC_NONE && chain > 0)
171 {
172 chain--;
173 size_t dist = i - cand;
174 if (dist > (size_t)PC_WINDOW)
175 {
176 break; // chain is newest-first; everything past here is farther
177 }
178 size_t l = 0;
179 while (l < max_len && buf[cand + l] == buf[i + l])
180 {
181 l++;
182 }
183 if ((int)l > *best_len)
184 {
185 *best_len = (int)l;
186 *best_dist = (int)dist;
187 if (l >= max_len)
188 {
189 break;
190 }
191 }
192 cand = z->prev[cand];
193 }
194}
195} // namespace
196
197void ssh_deflate_init(SshDeflate *z, uint8_t *work, uint16_t *head, uint16_t *prev, uint16_t *ll_code, uint8_t *ll_len,
198 uint16_t *d_code, uint8_t *d_len)
199{
200 z->work = work;
201 z->head = head;
202 z->prev = prev;
203 z->ll_code = ll_code;
204 z->ll_len = ll_len;
205 z->d_code = d_code;
206 z->d_len = d_len;
207 z->hist = 0;
208 z->header_sent = false;
209 build_fixed(z);
210}
211
212int ssh_deflate_packet(SshDeflate *z, const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap, size_t *out_len)
213{
214 if (src_len > (size_t)PC_SSH_ZLIB_MAX_IN)
215 {
216 return -1;
217 }
218
219 // Lay [history || input] out contiguously; matches for the input may reach back into history.
220 size_t hist = z->hist;
221 if (hist + src_len > SSH_ZLIB_WORK_SIZE)
222 {
223 return -1; // sizing invariant (hist <= PC_WINDOW, src_len <= MAX_IN) should prevent this
224 }
225 memcpy(z->work + hist, src, src_len);
226 size_t total = hist + src_len;
227 const uint8_t *buf = z->work;
228
229 // Rebuild the hash over the history (search-only) so input matches can reference it.
230 for (int b = 0; b < SSH_ZLIB_HASH_SIZE; b++)
231 {
232 z->head[b] = PC_NONE;
233 }
234 for (size_t p = 0; p + PC_MIN_MATCH <= total && p < hist; p++)
235 {
236 int h = hash3(buf + p);
237 z->prev[p] = z->head[h];
238 z->head[h] = (uint16_t)p;
239 }
240
242 w.out = dst;
243 w.cap = dst_cap;
244 w.cnt = 0;
245 w.acc = 0;
246 w.nbits = 0;
247 w.overflow = false;
248
249 // RFC 1950 zlib header, once at stream start: CMF=0x78 (deflate, 32 KB window), FLG=0x9C
250 // (default level, FCHECK making 0x789C divisible by 31). Byte-aligned, before any deflate bits.
251 if (!z->header_sent)
252 {
253 put_byte(&w, 0x78);
254 put_byte(&w, 0x9C);
255 z->header_sent = true;
256 }
257
258 // One fixed-Huffman block, not final: BFINAL=0 (1 bit), BTYPE=01 (2 bits, value 1).
259 pc_bitw_put(&w, 0, 1);
260 pc_bitw_put(&w, 1, 2);
261
262 size_t i = hist; // emit tokens only for the new input
263 while (i < total)
264 {
265 int best_len = 0;
266 int best_dist = 0;
267
268 if (i + PC_MIN_MATCH <= total)
269 {
270 int h = hash3(buf + i);
271 uint16_t cand = z->head[h];
272 size_t max_len = total - i;
273 if (max_len > (size_t)PC_MAX_MATCH)
274 {
275 max_len = PC_MAX_MATCH;
276 }
277 zlib_chain_match(z, buf, i, cand, PC_MAX_CHAIN, max_len, &best_len, &best_dist);
278 }
279
280 size_t advance;
281 if (best_len >= PC_MIN_MATCH)
282 {
283 emit_match(&w, z, best_len, best_dist);
284 advance = (size_t)best_len;
285 }
286 else
287 {
288 emit_literal(&w, z, buf[i]);
289 advance = 1;
290 }
291
292 // Insert each consumed position into the hash chains for later matches. The byte comparison
293 // above validates every candidate, so a stale insert can only cost ratio, never correctness.
294 size_t end = i + advance;
295 while (i < end)
296 {
297 if (i + PC_MIN_MATCH <= total)
298 {
299 int h = hash3(buf + i);
300 z->prev[i] = z->head[h];
301 z->head[h] = (uint16_t)i;
302 }
303 i++;
304 }
305 }
306
307 // End-of-block, then a Z_SYNC_FLUSH: byte-align via an empty stored block and KEEP its
308 // 0x00 0x00 0xff 0xff tail on the wire (SSH sends it; a zlib inflate() flushes at the boundary).
309 pc_bitw_put(&w, z->ll_code[256], z->ll_len[256]); // end-of-block symbol
310 pc_bitw_put(&w, 0, 1); // BFINAL=0 (empty stored block)
311 pc_bitw_put(&w, 0, 2); // BTYPE=00 (stored)
312 pc_bitw_align(&w);
313 put_byte(&w, 0x00);
314 put_byte(&w, 0x00);
315 put_byte(&w, 0xff);
316 put_byte(&w, 0xff);
317
318 if (w.overflow)
319 {
320 return -1;
321 }
322
323 // Slide the window: keep the last PC_WINDOW bytes of [history || input] as history for next time.
324 size_t keep = total;
325 if (keep > (size_t)PC_WINDOW)
326 {
327 keep = PC_WINDOW;
328 }
329 if (keep < total)
330 {
331 memmove(z->work, z->work + total - keep, keep);
332 }
333 z->hist = keep;
334
335 *out_len = w.cnt;
336 return 0;
337}
338
339#endif // PC_ENABLE_SSH_ZLIB
LSB-first bit writer over a caller-owned byte buffer - one source of truth.
void pc_bitw_align(pc_bit_writer *w)
Flush any partial byte, padding the high bits with zero (byte alignment).
Definition bitio.h:57
void pc_bitw_put(pc_bit_writer *w, uint32_t bits, int n)
Append the low n bits of bits, LSB-first, spilling any completed bytes to the output.
Definition bitio.h:39
#define PC_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).
LSB-first bit writer over the caller's output buffer; overflow latches once cap is exceeded.
Definition bitio.h:29
int nbits
bits currently buffered (< 8 between calls)
Definition bitio.h:34
size_t cnt
bytes written so far
Definition bitio.h:32
uint32_t acc
bit accumulator (LSB-first)
Definition bitio.h:33
size_t cap
Definition bitio.h:31
bool overflow
Definition bitio.h:35
uint8_t * out
Definition bitio.h:30