ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_inflate.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_inflate.cpp
6 * @brief SSH client-to-server resumable, context-takeover INFLATE - implementation.
7 *
8 * A canonical-Huffman DEFLATE decoder (RFC 1951), like the one-shot presentation/inflate but built to
9 * resume across packets: it reads the logical stream (carried tail bytes ++ this packet) bit by bit,
10 * decodes only *complete* blocks, and on running out of input rolls the in-progress block back to its
11 * boundary - so no mid-block state persists. Output goes to the caller buffer and into a persistent
12 * 32 KB circular window that satisfies back-references reaching into earlier packets.
13 */
14
16
17#if PC_ENABLE_SSH_ZLIB
18
19#include <string.h>
20
21namespace
22{
23#define PC_MAXBITS 15
24#define PC_MAXLCODES 288
25#define PC_MAXDCODES 32
26
27// Block decode outcomes.
28#define PC_BLK_OK 0 // a whole block decoded
29#define PC_BLK_NEED 1 // ran out of input mid-block (roll back to the boundary, wait for more)
30#define PC_BLK_ERR 2 // malformed stream or output overflow
31
32// Length / distance base + extra-bit tables (RFC 1951 sec 3.2.5), codes 257..285 and 0..29.
33const short LEN_BASE[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27,
34 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
35const 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};
36const short DIST_BASE[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129,
37 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577};
38const short DIST_EXTRA[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
39 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
40
41// Huffman table scratch (stack-resident per call; the decoder rebuilds it per block since no block
42// state persists across packets).
43struct Tables
44{
45 short lcount[PC_MAXBITS + 1];
46 short lsym[PC_MAXLCODES];
47 short dcount[PC_MAXBITS + 1];
48 short dsym[PC_MAXDCODES];
49 short lengths[PC_MAXLCODES + PC_MAXDCODES];
50};
51
52struct Huffman
53{
54 short *count;
55 short *symbol;
56};
57
58// Bit reader over the two-segment logical stream (carried tail ++ this packet), LSB first.
59struct BitIn
60{
61 const uint8_t *seg0;
62 size_t n0;
63 const uint8_t *seg1;
64 size_t n1;
65 size_t bitpos; // current bit offset into the logical stream
66 size_t nbits; // total bits available
67 bool underflow;
68};
69
70inline uint8_t logical_byte(const uint8_t *seg0, size_t n0, const uint8_t *seg1, size_t i)
71{
72 return i < n0 ? seg0[i] : seg1[i - n0];
73}
74
75// Pull @p need bits (LSB first). On end-of-input sets underflow and returns what it has.
76int getbits(BitIn *b, int need)
77{
78 int v = 0;
79 for (int i = 0; i < need; i++)
80 {
81 if (b->bitpos >= b->nbits)
82 {
83 b->underflow = true;
84 return v;
85 }
86 size_t byte = b->bitpos >> 3;
87 int bit = (int)(b->bitpos & 7u);
88 v |= ((logical_byte(b->seg0, b->n0, b->seg1, byte) >> bit) & 1) << i;
89 b->bitpos++;
90 }
91 return v;
92}
93
94// Decode one canonical-Huffman symbol; -1 on an invalid code, underflow flagged on end-of-input.
95int hdecode(BitIn *b, const Huffman *h)
96{
97 int code = 0;
98 int first = 0;
99 int index = 0;
100 for (int len = 1; len <= PC_MAXBITS; len++)
101 {
102 code |= getbits(b, 1);
103 if (b->underflow)
104 {
105 return -1;
106 }
107 int count = h->count[len];
108 if (code - count < first)
109 {
110 return h->symbol[index + (code - first)];
111 }
112 index += count;
113 first += count;
114 first <<= 1;
115 code <<= 1;
116 }
117 return -1;
118}
119
120// Build a canonical-Huffman table from code lengths. 0 = complete, >0 = incomplete, <0 = over-subscribed.
121int construct(Huffman *h, const short *lengths, int n)
122{
123 for (int len = 0; len <= PC_MAXBITS; len++)
124 {
125 h->count[len] = 0;
126 }
127 for (int sym = 0; sym < n; sym++)
128 {
129 h->count[lengths[sym]]++;
130 }
131 if (h->count[0] == n)
132 {
133 return 0;
134 }
135 int left = 1;
136 for (int len = 1; len <= PC_MAXBITS; len++)
137 {
138 left <<= 1;
139 left -= h->count[len];
140 if (left < 0)
141 {
142 return left;
143 }
144 }
145 short offs[PC_MAXBITS + 1];
146 offs[1] = 0;
147 for (int len = 1; len < PC_MAXBITS; len++)
148 {
149 offs[len + 1] = offs[len] + h->count[len];
150 }
151 for (int sym = 0; sym < n; sym++)
152 {
153 if (lengths[sym] != 0)
154 {
155 h->symbol[offs[lengths[sym]]++] = (short)sym;
156 }
157 }
158 return left;
159}
160
161// Output one byte to the caller buffer and the circular window; flags overflow when dst is full.
162struct OutCtx
163{
164 uint8_t *dst;
165 size_t cap;
166 size_t cnt;
167 SshInflate *z;
168 bool overflow;
169};
170
171void put_byte(OutCtx *o, uint8_t byte)
172{
173 if (o->cnt >= o->cap)
174 {
175 o->overflow = true;
176 return;
177 }
178 o->dst[o->cnt++] = byte;
179 o->z->window[o->z->wpos] = byte;
180 o->z->wpos = (o->z->wpos + 1u) & (SSH_INFLATE_WINDOW - 1u);
181 if (o->z->whist < SSH_INFLATE_WINDOW)
182 {
183 o->z->whist++;
184 }
185}
186
187// Literal/length + distance decode into the output/window. PC_BLK_OK on end-of-block.
188int do_codes(BitIn *b, OutCtx *o, const Huffman *lc, const Huffman *dc)
189{
190 for (;;)
191 {
192 int sym = hdecode(b, lc);
193 if (b->underflow)
194 {
195 return PC_BLK_NEED;
196 }
197 if (sym < 0)
198 {
199 return PC_BLK_ERR;
200 }
201 if (sym == 256)
202 {
203 return PC_BLK_OK; // end of block
204 }
205 if (sym < 256)
206 {
207 put_byte(o, (uint8_t)sym);
208 if (o->overflow)
209 {
210 return PC_BLK_ERR;
211 }
212 continue;
213 }
214 sym -= 257;
215 if (sym >= 29)
216 {
217 return PC_BLK_ERR;
218 }
219 int len = LEN_BASE[sym] + getbits(b, LEN_EXTRA[sym]);
220 if (b->underflow)
221 {
222 return PC_BLK_NEED;
223 }
224 int dsym = hdecode(b, dc);
225 if (b->underflow)
226 {
227 return PC_BLK_NEED;
228 }
229 if (dsym < 0 || dsym >= 30)
230 {
231 return PC_BLK_ERR;
232 }
233 size_t dist = (size_t)(DIST_BASE[dsym] + getbits(b, DIST_EXTRA[dsym]));
234 if (b->underflow)
235 {
236 return PC_BLK_NEED;
237 }
238 if (dist == 0 || dist > o->z->whist)
239 {
240 return PC_BLK_ERR; // back-reference before the start of the window history
241 }
242 for (int k = 0; k < len; k++)
243 {
244 uint32_t idx = (o->z->wpos - (uint32_t)dist) & (SSH_INFLATE_WINDOW - 1u);
245 put_byte(o, o->z->window[idx]);
246 if (o->overflow)
247 {
248 return PC_BLK_ERR;
249 }
250 }
251 }
252}
253
254// Uncompressed (stored) block: align to the next byte, read LEN/NLEN, copy LEN literal bytes.
255int do_stored(BitIn *b, OutCtx *o)
256{
257 if (b->bitpos & 7u)
258 {
259 b->bitpos = (b->bitpos + 7u) & ~(size_t)7u; // discard bits to the byte boundary
260 }
261 if (b->bitpos + 32u > b->nbits)
262 {
263 return PC_BLK_NEED;
264 }
265 int len = getbits(b, 16);
266 int nlen = getbits(b, 16);
267 if ((len ^ nlen) != 0xFFFF)
268 {
269 return PC_BLK_ERR;
270 }
271 if (b->bitpos + (size_t)len * 8u > b->nbits)
272 {
273 return PC_BLK_NEED;
274 }
275 for (int k = 0; k < len; k++)
276 {
277 put_byte(o, (uint8_t)getbits(b, 8));
278 if (o->overflow)
279 {
280 return PC_BLK_ERR;
281 }
282 }
283 return PC_BLK_OK;
284}
285
286// Fixed-Huffman block (RFC 1951 sec 3.2.6).
287int do_fixed(BitIn *b, OutCtx *o, Tables *t)
288{
289 Huffman lc = {t->lcount, t->lsym};
290 Huffman dc = {t->dcount, t->dsym};
291 int sym = 0;
292 for (; sym < 144; sym++)
293 {
294 t->lengths[sym] = 8;
295 }
296 for (; sym < 256; sym++)
297 {
298 t->lengths[sym] = 9;
299 }
300 for (; sym < 280; sym++)
301 {
302 t->lengths[sym] = 7;
303 }
304 for (; sym < 288; sym++)
305 {
306 t->lengths[sym] = 8;
307 }
308 construct(&lc, t->lengths, 288);
309 for (sym = 0; sym < 30; sym++)
310 {
311 t->lengths[sym] = 5;
312 }
313 construct(&dc, t->lengths, 30);
314 return do_codes(b, o, &lc, &dc);
315}
316
317// Dynamic-Huffman block (RFC 1951 sec 3.2.7). Because a NEED rolls the whole block back and re-decodes
318// it next packet, the tables are rebuilt from scratch here every attempt - no partial state is kept.
319int do_dynamic(BitIn *b, OutCtx *o, Tables *t)
320{
321 static const short ORDER[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
322 Huffman lc = {t->lcount, t->lsym};
323 Huffman dc = {t->dcount, t->dsym};
324
325 int nlen = getbits(b, 5) + 257;
326 int ndist = getbits(b, 5) + 1;
327 int ncode = getbits(b, 4) + 4;
328 if (b->underflow)
329 {
330 return PC_BLK_NEED;
331 }
332 if (nlen > PC_MAXLCODES || ndist > PC_MAXDCODES)
333 {
334 return PC_BLK_ERR;
335 }
336
337 int index;
338 for (index = 0; index < ncode; index++)
339 {
340 t->lengths[ORDER[index]] = (short)getbits(b, 3);
341 }
342 if (b->underflow)
343 {
344 return PC_BLK_NEED;
345 }
346 for (; index < 19; index++)
347 {
348 t->lengths[ORDER[index]] = 0;
349 }
350 if (construct(&lc, t->lengths, 19) != 0)
351 {
352 return PC_BLK_ERR; // the code-length code must be complete
353 }
354
355 index = 0;
356 while (index < nlen + ndist)
357 {
358 int symbol = hdecode(b, &lc);
359 if (b->underflow)
360 {
361 return PC_BLK_NEED;
362 }
363 if (symbol < 0)
364 {
365 return PC_BLK_ERR;
366 }
367 if (symbol < 16)
368 {
369 t->lengths[index++] = (short)symbol;
370 continue;
371 }
372 int repeat_len = 0;
373 int repeat;
374 if (symbol == 16)
375 {
376 if (index == 0)
377 {
378 return PC_BLK_ERR;
379 }
380 repeat_len = t->lengths[index - 1];
381 repeat = 3 + getbits(b, 2);
382 }
383 else if (symbol == 17)
384 {
385 repeat = 3 + getbits(b, 3);
386 }
387 else
388 {
389 repeat = 11 + getbits(b, 7);
390 }
391 if (b->underflow)
392 {
393 return PC_BLK_NEED;
394 }
395 if (index + repeat > nlen + ndist)
396 {
397 return PC_BLK_ERR;
398 }
399 while (repeat--)
400 {
401 t->lengths[index++] = (short)repeat_len;
402 }
403 }
404 if (t->lengths[256] == 0)
405 {
406 return PC_BLK_ERR; // no end-of-block code
407 }
408
409 int err = construct(&lc, t->lengths, nlen);
410 if (err && (err < 0 || nlen != lc.count[0] + lc.count[1]))
411 {
412 return PC_BLK_ERR;
413 }
414 err = construct(&dc, t->lengths + nlen, ndist);
415 if (err && (err < 0 || ndist != dc.count[0] + dc.count[1]))
416 {
417 return PC_BLK_ERR;
418 }
419 return do_codes(b, o, &lc, &dc);
420}
421
422// Decode one whole block (header + body). PC_BLK_NEED restores nothing itself; the caller rolls back.
423int do_block(BitIn *b, OutCtx *o, Tables *t)
424{
425 getbits(b, 1); // BFINAL: SSH streams flush rather than finish, so it is not acted on
426 int type = getbits(b, 2);
427 if (b->underflow)
428 {
429 return PC_BLK_NEED;
430 }
431 if (type == 0)
432 {
433 return do_stored(b, o);
434 }
435 if (type == 1)
436 {
437 return do_fixed(b, o, t);
438 }
439 if (type == 2)
440 {
441 return do_dynamic(b, o, t);
442 }
443 return PC_BLK_ERR; // type 3 is reserved
444}
445} // namespace
446
447void ssh_inflate_init(SshInflate *z, uint8_t *window)
448{
449 z->window = window;
450 z->wpos = 0;
451 z->whist = 0;
452 z->carry_len = 0;
453 z->bit_off = 0;
454 z->header_seen = false;
455}
456
457int ssh_inflate_packet(SshInflate *z, const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap, size_t *out_len)
458{
459 if (!z || (src_len && !src) || !out_len)
460 {
461 return -1;
462 }
463
464 BitIn b;
465 b.seg0 = z->carry;
466 b.n0 = z->carry_len;
467 b.seg1 = src;
468 b.n1 = src_len;
469 b.nbits = ((size_t)z->carry_len + src_len) * 8u;
470 b.bitpos = z->bit_off;
471 b.underflow = false;
472
473 OutCtx o;
474 o.dst = dst;
475 o.cap = dst_cap;
476 o.cnt = 0;
477 o.z = z;
478 o.overflow = false;
479
480 // RFC 1950 zlib header, once per stream: CM=8 (deflate) + the mod-31 check. CINFO (the peer's
481 // window size) is not enforced - we always carry the full 32 KB window, which covers any CINFO.
482 if (!z->header_seen)
483 {
484 if (b.nbits - b.bitpos < 16u)
485 {
486 // Fewer than the 2 header bytes arrived; carry them whole and wait for more.
487 size_t rem = (size_t)z->carry_len + src_len;
488 if (rem > SSH_INFLATE_CARRY)
489 {
490 return -1;
491 }
492 uint8_t tmp[SSH_INFLATE_CARRY];
493 for (size_t i = 0; i < rem; i++)
494 {
495 tmp[i] = logical_byte(z->carry, z->carry_len, src, i);
496 }
497 memcpy(z->carry, tmp, rem);
498 z->carry_len = (uint8_t)rem;
499 z->bit_off = 0;
500 *out_len = 0;
501 return 0;
502 }
503 int cmf = getbits(&b, 8);
504 int flg = getbits(&b, 8);
505 if ((cmf & 0x0F) != 8)
506 {
507 return -1; // compression method must be DEFLATE
508 }
509 if ((((unsigned)cmf << 8) | (unsigned)flg) % 31u != 0u)
510 {
511 return -1; // header checksum
512 }
513 if (flg & 0x20)
514 {
515 return -1; // a preset dictionary (FDICT) is not used by SSH
516 }
517 z->header_seen = true;
518 }
519
520 Tables tbl;
521 size_t boundary = b.bitpos; // bit position of the last decoded block boundary
522
523 for (;;)
524 {
525 if (b.bitpos >= b.nbits)
526 {
527 break; // input exhausted exactly at a boundary
528 }
529 size_t cp_bit = b.bitpos;
530 size_t cp_cnt = o.cnt;
531 uint32_t cp_wpos = z->wpos;
532 uint32_t cp_whist = z->whist;
533
534 int st = do_block(&b, &o, &tbl);
535 if (st == PC_BLK_OK)
536 {
537 boundary = b.bitpos;
538 continue;
539 }
540 if (st == PC_BLK_NEED)
541 {
542 // Roll the incomplete block back to its boundary and stop; it re-decodes next packet.
543 b.bitpos = cp_bit;
544 o.cnt = cp_cnt;
545 z->wpos = cp_wpos;
546 z->whist = cp_whist;
547 boundary = cp_bit;
548 break;
549 }
550 return -1; // PC_BLK_ERR
551 }
552
553 // Carry the bytes from the last boundary onward (the incomplete flush block).
554 size_t bstart_byte = boundary >> 3;
555 size_t total_bytes = (size_t)z->carry_len + src_len;
556 size_t rem = total_bytes - bstart_byte;
557 if (rem > SSH_INFLATE_CARRY)
558 {
559 return -1; // the peer did not flush at a block boundary within the tail bound
560 }
561 uint8_t tmp[SSH_INFLATE_CARRY];
562 for (size_t i = 0; i < rem; i++)
563 {
564 tmp[i] = logical_byte(z->carry, z->carry_len, src, bstart_byte + i);
565 }
566 memcpy(z->carry, tmp, rem);
567 z->carry_len = (uint8_t)rem;
568 z->bit_off = (uint8_t)(boundary & 7u);
569
570 *out_len = o.cnt;
571 return 0;
572}
573
574#endif // PC_ENABLE_SSH_ZLIB
SSH client-to-server decompression: a resumable, context-takeover INFLATE (no heap).