CMSDK 2.0.1
Cross-platform C++ base library and SDK for the Psyclone AIOS platform
Loading...
Searching...
No Matches
StreamDecoders.h
Go to the documentation of this file.
1// StreamDecoders.h
2// T1.1 step 1.2a: streaming transport decode primitives.
3// 1.2a-i: HTTP chunked-transfer decoder (RFC 7230 sec 4.1) fed from byte
4// buffers; no sockets. 1.2a-ii: SSE (text/event-stream) line parser.
5// 1.2a-iii: incremental read-loop adapter (StreamByteSource + pump loops)
6// feeding either decoder; SSLConnection::receiveAvailable() fits via the
7// ConnectionByteSource template, tests inject mock byte sources.
8
9#ifndef _STREAMDECODERS_H_
10#define _STREAMDECODERS_H_
11
12#include <string>
13#include <vector>
14#include <cstddef>
15#include <cstdint>
16#include <functional>
17#include <cstring>
18#include "hash/crc32.h" // vendored IEEE CRC32 (zlib polynomial) - eventstream CRCs
19
20namespace cmsdk {
21
22// Incremental HTTP chunked-transfer decoder.
23// Feed raw bytes with feed(); decoded payload accumulates in body().
24// isComplete() turns true once the terminating 0-chunk (and its final CRLF)
25// has been consumed. hasError() reports malformed input (bad hex size line,
26// missing CRLF after a chunk body).
28public:
29 HTTPChunkedDecoder() : state(ST_SIZE), chunkRemaining(0), complete(false), error(false) {}
30
31 // Feed count bytes; returns false if the decoder is in error state.
32 bool feed(const char* data, size_t count) {
33 for (size_t n = 0; n < count && !error; n++)
34 feedByte(data[n]);
35 return !error;
36 }
37 bool feed(const std::string& data) { return feed(data.data(), data.size()); }
38
39 const std::string& body() const { return decoded; }
40 bool isComplete() const { return complete; }
41 bool hasError() const { return error; }
42
43 void reset() {
44 state = ST_SIZE; sizeLine.clear(); decoded.clear();
45 chunkRemaining = 0; complete = false; error = false;
46 }
47
48private:
49 enum State {
50 ST_SIZE, // reading hex size line (up to CRLF)
51 ST_BODY, // reading chunkRemaining body bytes
52 ST_BODY_CRLF, // expecting CR then LF after chunk body
53 ST_BODY_LF,
54 ST_TRAILER, // after 0-chunk: consume trailer lines until blank line
55 ST_DONE
56 };
57
58 void feedByte(char c) {
59 switch (state) {
60 case ST_SIZE:
61 if (c == '\n') {
62 // strip optional CR and any chunk extension after ';'
63 size_t end = sizeLine.find(';');
64 std::string hex = (end == std::string::npos) ? sizeLine : sizeLine.substr(0, end);
65 while (!hex.empty() && (hex[hex.size()-1] == '\r' || hex[hex.size()-1] == ' '))
66 hex.erase(hex.size()-1);
67 if (!parseHex(hex, chunkRemaining)) { error = true; return; }
68 sizeLine.clear();
69 if (chunkRemaining == 0) { state = ST_TRAILER; sizeLine.clear(); }
70 else state = ST_BODY;
71 }
72 else sizeLine += c;
73 break;
74 case ST_BODY:
75 decoded += c;
76 if (--chunkRemaining == 0) state = ST_BODY_CRLF;
77 break;
78 case ST_BODY_CRLF:
79 if (c == '\r') state = ST_BODY_LF;
80 else if (c == '\n') state = ST_SIZE; // tolerate bare LF
81 else error = true;
82 break;
83 case ST_BODY_LF:
84 if (c == '\n') state = ST_SIZE;
85 else error = true;
86 break;
87 case ST_TRAILER:
88 // consume trailer lines; a blank line ends the message
89 if (c == '\n') {
90 std::string line = sizeLine;
91 if (!line.empty() && line[line.size()-1] == '\r') line.erase(line.size()-1);
92 sizeLine.clear();
93 if (line.empty()) { complete = true; state = ST_DONE; }
94 }
95 else sizeLine += c;
96 break;
97 case ST_DONE:
98 break; // ignore any bytes after completion
99 }
100 }
101
102 static bool parseHex(const std::string& s, size_t& out) {
103 if (s.empty()) return false;
104 size_t v = 0;
105 for (size_t n = 0; n < s.size(); n++) {
106 char c = s[n]; int d;
107 if (c >= '0' && c <= '9') d = c - '0';
108 else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
109 else if (c >= 'A' && c <= 'F') d = c - 'A' + 10;
110 else return false;
111 v = (v << 4) | (size_t)d;
112 }
113 out = v;
114 return true;
115 }
116
117 State state;
118 std::string sizeLine; // also reused for trailer-line accumulation
119 std::string decoded;
120 size_t chunkRemaining;
121 bool complete;
122 bool error;
123};
124
125// Incremental SSE (Server-Sent Events, text/event-stream) line parser.
126// Feed raw bytes with feed(); completed events (blank-line boundary) queue up
127// and are drained with nextEvent(). Only `data:` fields are collected (an
128// optional single leading space after the colon is stripped; multiple data
129// lines in one event are joined with '\n'). Other fields (event:, id:,
130// retry:, comments) are skipped safely. An event whose data is the literal
131// sentinel "[DONE]" sets isDone() instead of being queued; bytes after that
132// are ignored. hasError() is reserved for malformed input (currently only
133// oversized single lines, guarding runaway buffers).
135public:
136 SSEEventParser() : haveData(false), done(false), error(false) {}
137
138 // Feed count bytes; returns false if the parser is in error state.
139 bool feed(const char* data, size_t count) {
140 for (size_t n = 0; n < count && !error && !done; n++)
141 feedByte(data[n]);
142 return !error;
143 }
144 bool feed(const std::string& data) { return feed(data.data(), data.size()); }
145
146 bool hasEvent() const { return !events.empty(); }
147 // Pops the oldest completed event's data payload; returns false if none.
148 bool nextEvent(std::string& out) {
149 if (events.empty()) return false;
150 out = events.front();
151 events.erase(events.begin());
152 return true;
153 }
154 bool isDone() const { return done; }
155 bool hasError() const { return error; }
156
157 void reset() {
158 line.clear(); eventData.clear(); events.clear();
159 haveData = false; done = false; error = false;
160 }
161
162private:
163 static const size_t MAX_LINE = 1024 * 1024; // 1 MB guard per line
164
165 void feedByte(char c) {
166 if (c == '\n') { processLine(); line.clear(); return; }
167 line += c;
168 if (line.size() > MAX_LINE) error = true;
169 }
170
171 void processLine() {
172 // strip optional trailing CR (accept both CRLF and bare LF endings)
173 if (!line.empty() && line[line.size()-1] == '\r') line.erase(line.size()-1);
174 if (line.empty()) { // blank line = event boundary
175 if (haveData) {
176 if (eventData == "[DONE]") done = true;
177 else events.push_back(eventData);
178 }
179 eventData.clear(); haveData = false;
180 return;
181 }
182 if (line.compare(0, 5, "data:") == 0) {
183 size_t start = 5;
184 if (start < line.size() && line[start] == ' ') start++; // optional space
185 if (haveData) eventData += '\n'; // spec: multiple data lines join with LF
186 eventData += line.substr(start);
187 haveData = true;
188 }
189 // else: event:/id:/retry:/comment lines are skipped safely
190 }
191
192 std::string line; // current (possibly partial) line being assembled
193 std::string eventData; // accumulated data for the in-progress event
194 bool haveData;
195 std::vector<std::string> events;
196 bool done;
197 bool error;
198};
199
200// ---------------------------------------------------------------------------
201// 1.2a-iii: incremental read-loop adapter.
202// Abstracted byte source so tests can inject canned/staged buffers and the
203// real transport is a thin wrapper over SSLConnection::receiveAvailable().
204// ---------------------------------------------------------------------------
205
206// Result of one read attempt from a byte source.
208 SRC_DATA, // got >= 1 byte
209 SRC_AGAIN, // no data right now (would-block / timeout); retry later
210 SRC_CLOSED, // orderly end of stream
211 SRC_ERROR // transport failure
212};
213
214// Minimal pull-style byte source interface.
216public:
217 virtual ~StreamByteSource() {}
218 // Read up to maxSize bytes into buf; set got. got is only meaningful
219 // when SRC_DATA is returned (and must then be >= 1).
220 virtual StreamReadResult read(char* buf, size_t maxSize, size_t& got) = 0;
221};
222
223// Thin adapter over a CMSDK network connection (e.g. SSLConnection).
224// receiveAvailable(data, size, maxSize, timeout) returns true with size >= 0
225// when bytes (possibly none yet) were pulled from the connection buffer, and
226// false when nothing arrived within the timeout or the buffer path failed;
227// isConnected() disambiguates would-block from end-of-stream.
228template<class Connection>
230public:
231 ConnectionByteSource(Connection& c, unsigned int timeoutMS)
232 : con(c), timeout(timeoutMS) {}
233 StreamReadResult read(char* buf, size_t maxSize, size_t& got) {
234 unsigned int size = 0;
235 bool ok = con.receiveAvailable(buf, size, (unsigned int)maxSize, timeout);
236 if (ok && size > 0) { got = size; return SRC_DATA; }
237 return con.isConnected() ? SRC_AGAIN : SRC_CLOSED;
238 }
239private:
240 Connection& con;
241 unsigned int timeout;
242};
243
244// Outcome of a pump loop.
246 PUMP_COMPLETE, // decoder reported natural completion ([DONE] / 0-chunk)
247 PUMP_CLOSED, // source closed before completion (may be normal for SSE)
248 PUMP_ERROR, // decoder rejected input or source failed
249 PUMP_LIMIT // safety cap on iterations hit (bug guard; avoids hangs)
250};
251
252// Pump bytes from src into a chunked decoder until the terminating 0-chunk,
253// stream end or error. Newly decoded body bytes are surfaced incrementally
254// through onData (may be empty). maxIterations bounds the loop.
257 const std::function<void(const char*, size_t)>& onData,
258 size_t maxIterations = 100000, size_t readSize = 4096) {
259 std::vector<char> buf(readSize);
260 size_t delivered = dec.body().size();
261 for (size_t iter = 0; iter < maxIterations; iter++) {
262 size_t got = 0;
263 StreamReadResult r = src.read(&buf[0], readSize, got);
264 if (r == SRC_ERROR) return PUMP_ERROR;
265 if (r == SRC_CLOSED) return dec.isComplete() ? PUMP_COMPLETE : PUMP_CLOSED;
266 if (r == SRC_DATA) {
267 if (!dec.feed(&buf[0], got)) return PUMP_ERROR;
268 const std::string& body = dec.body();
269 if (onData && body.size() > delivered)
270 onData(body.data() + delivered, body.size() - delivered);
271 delivered = body.size();
272 if (dec.isComplete()) return PUMP_COMPLETE;
273 }
274 // SRC_AGAIN: just retry (source owns any waiting/timeout policy)
275 }
276 return PUMP_LIMIT;
277}
278
279// Pump bytes from src into an SSE parser until [DONE], stream end or error.
280// Each completed event's data payload is surfaced through onEvent.
282 SSEEventParser& parser,
283 const std::function<void(const std::string&)>& onEvent,
284 size_t maxIterations = 100000, size_t readSize = 4096) {
285 std::vector<char> buf(readSize);
286 for (size_t iter = 0; iter < maxIterations; iter++) {
287 size_t got = 0;
288 StreamReadResult r = src.read(&buf[0], readSize, got);
289 if (r == SRC_ERROR) return PUMP_ERROR;
290 if (r == SRC_DATA && !parser.feed(&buf[0], got)) {
291 // drain any events completed before the malformed input
292 std::string ev;
293 while (parser.nextEvent(ev)) { if (onEvent) onEvent(ev); }
294 return PUMP_ERROR;
295 }
296 std::string ev;
297 while (parser.nextEvent(ev)) { if (onEvent) onEvent(ev); }
298 if (parser.isDone()) return PUMP_COMPLETE;
299 if (r == SRC_CLOSED) return PUMP_CLOSED;
300 }
301 return PUMP_LIMIT;
302}
303
304// ---------------------------------------------------------------------------
305// 1.2b-i: AWS eventstream (Bedrock invoke-with-response-stream) binary frame
306// parser. Incremental: feed raw bytes; completed frames queue up and are
307// drained with nextFrame(). Wire format per frame:
308// [4 total-length BE][4 headers-length BE][4 prelude CRC] (12-byte prelude)
309// headers: repeated { 1 key-len, key, 1 value-type, value }
310// payload bytes, then [4 message CRC]
311// 1.2b-ii: both CRC fields are validated: prelude CRC covers the first 8
312// bytes; message CRC covers everything before the trailing 4 bytes. Both are
313// standard IEEE CRC32 (zlib polynomial), big-endian on the wire. Extracts the ':event-type' header (string, type 7) and the
314// payload for each frame. Error flag + reset(), matching sibling decoders.
316public:
317 struct Frame {
318 std::string eventType; // ':event-type' header value ('' if absent)
319 std::string payload; // raw payload bytes (JSON chunk for Bedrock)
320 };
321
322 EventStreamFrameParser() : error(false) {}
323
324 // Feed count bytes; returns false if the parser is in error state.
325 bool feed(const char* data, size_t count) {
326 if (error) return false;
327 if (buf.size() + count > MAX_BUFFER) { error = true; return false; }
328 buf.append(data, count);
329 size_t guard = 0;
330 while (!error && tryParseFrame()) {
331 if (++guard > MAX_FRAMES_PER_FEED) { error = true; break; }
332 }
333 return !error;
334 }
335 bool feed(const std::string& data) { return feed(data.data(), data.size()); }
336
337 bool hasFrame() const { return !frames.empty(); }
338 // Pops the oldest completed frame; returns false if none.
339 bool nextFrame(Frame& out) {
340 if (frames.empty()) return false;
341 out = frames.front();
342 frames.erase(frames.begin());
343 return true;
344 }
345 bool hasError() const { return error; }
346
347 void reset() { buf.clear(); frames.clear(); error = false; }
348
349private:
350 static const size_t MAX_BUFFER = 16 * 1024 * 1024; // 16 MB runaway guard
351 static const size_t MAX_FRAMES_PER_FEED = 100000; // hang guard
352 static const size_t PRELUDE_SIZE = 12; // 2 lengths + prelude CRC
353 static const size_t MIN_FRAME = 16; // prelude + message CRC
354
355 static uint32_t be32(const unsigned char* p) {
356 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
357 ((uint32_t)p[2] << 8) | (uint32_t)p[3];
358 }
359 static uint16_t be16(const unsigned char* p) {
360 return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]);
361 }
362 // True if the IEEE CRC32 of [data,len) matches the 4 big-endian bytes at
363 // wireCrc. hash::CRC32::getHash() already emits big-endian bytes.
364 static bool crcMatches(const unsigned char* data, size_t len,
365 const unsigned char* wireCrc) {
366 hash::CRC32 crc;
367 crc.add(data, len);
368 unsigned char computed[4];
369 crc.getHash(computed);
370 return memcmp(computed, wireCrc, 4) == 0;
371 }
372
373 // Attempts to parse one complete frame from the front of buf.
374 // Returns true if a frame was consumed (parsed or skipped), false if more
375 // bytes are needed. Sets error on malformed framing.
376 bool tryParseFrame() {
377 if (buf.size() < PRELUDE_SIZE) return false;
378 const unsigned char* p = (const unsigned char*)buf.data();
379 uint32_t totalLen = be32(p);
380 uint32_t headersLen = be32(p + 4);
381 // Prelude CRC (p+8) covers the two length fields. A mismatch means the
382 // lengths themselves are untrustworthy: error out, do not consume.
383 if (!crcMatches(p, 8, p + 8)) { error = true; return false; }
384 if (totalLen < MIN_FRAME || totalLen > MAX_BUFFER ||
385 (size_t)headersLen > (size_t)totalLen - MIN_FRAME)
386 { error = true; return false; }
387 if (buf.size() < totalLen) return false; // incomplete frame; wait
388
389 // Message CRC: trailing 4 bytes, over all preceding bytes of the frame.
390 if (!crcMatches(p, (size_t)totalLen - 4, p + totalLen - 4))
391 { error = true; return false; }
392
393 Frame f;
394 if (!parseHeaders(p + PRELUDE_SIZE, headersLen, f.eventType))
395 { error = true; return false; }
396 size_t payloadLen = (size_t)totalLen - MIN_FRAME - headersLen;
397 f.payload.assign(buf.data() + PRELUDE_SIZE + headersLen, payloadLen);
398 frames.push_back(f);
399 buf.erase(0, totalLen);
400 return true;
401 }
402
403 // Walks the headers block, extracting the ':event-type' string header.
404 // Handles all AWS eventstream value types well enough not to desync.
405 // Returns false on malformed/truncated header data.
406 bool parseHeaders(const unsigned char* h, size_t len, std::string& eventType) {
407 size_t pos = 0, guard = 0;
408 while (pos < len) {
409 if (++guard > 1000) return false; // hang guard
410 size_t keyLen = h[pos++];
411 if (pos + keyLen + 1 > len) return false;
412 std::string key((const char*)h + pos, keyLen);
413 pos += keyLen;
414 unsigned char type = h[pos++];
415 size_t valLen;
416 switch (type) {
417 case 0: case 1: valLen = 0; break; // bool true/false
418 case 2: valLen = 1; break; // byte
419 case 3: valLen = 2; break; // short
420 case 4: valLen = 4; break; // int
421 case 5: case 8: valLen = 8; break; // long / timestamp
422 case 6: case 7: // byte-buffer / string
423 if (pos + 2 > len) return false;
424 valLen = be16(h + pos); pos += 2;
425 break;
426 case 9: valLen = 16; break; // uuid
427 default: return false; // unknown type: desync risk
428 }
429 if (pos + valLen > len) return false;
430 if (type == 7 && key == ":event-type")
431 eventType.assign((const char*)h + pos, valLen);
432 pos += valLen;
433 }
434 return pos == len;
435 }
436
437 std::string buf; // unconsumed raw bytes
438 std::vector<Frame> frames; // completed frames awaiting nextFrame()
439 bool error;
440};
441
442// Pump bytes from src into an eventstream frame parser until stream end or
443// error (eventstream has no in-band [DONE] sentinel at this layer; the
444// caller decides completion from frame content, e.g. a message_stop event).
445// Each completed frame is surfaced through onFrame. Bounded like siblings.
448 const std::function<void(const EventStreamFrameParser::Frame&)>& onFrame,
449 size_t maxIterations = 100000, size_t readSize = 4096) {
450 std::vector<char> buf(readSize);
451 for (size_t iter = 0; iter < maxIterations; iter++) {
452 size_t got = 0;
453 StreamReadResult r = src.read(&buf[0], readSize, got);
454 if (r == SRC_ERROR) return PUMP_ERROR;
455 if (r == SRC_DATA && !parser.feed(&buf[0], got)) {
456 // drain any frames completed before the malformed input
458 while (parser.nextFrame(f)) { if (onFrame) onFrame(f); }
459 return PUMP_ERROR;
460 }
462 while (parser.nextFrame(f)) { if (onFrame) onFrame(f); }
463 if (r == SRC_CLOSED) return PUMP_COMPLETE; // orderly close = end of stream
464 }
465 return PUMP_LIMIT;
466}
467
468} // namespace cmsdk
469
470#endif // _STREAMDECODERS_H_
ConnectionByteSource(Connection &c, unsigned int timeoutMS)
StreamReadResult read(char *buf, size_t maxSize, size_t &got)
bool feed(const char *data, size_t count)
bool feed(const std::string &data)
bool feed(const char *data, size_t count)
const std::string & body() const
bool feed(const std::string &data)
bool nextEvent(std::string &out)
bool feed(const char *data, size_t count)
bool feed(const std::string &data)
virtual StreamReadResult read(char *buf, size_t maxSize, size_t &got)=0
std::string getHash()
return latest hash as 8 hex characters
void add(const void *data, size_t numBytes)
add arbitrary number of bytes
Third-party (vendored): CRC32 checksum from Stephan Brumme's portable hashing library (create....
StreamPumpResult pumpChunkedStream(StreamByteSource &src, HTTPChunkedDecoder &dec, const std::function< void(const char *, size_t)> &onData, size_t maxIterations=100000, size_t readSize=4096)
StreamPumpResult pumpSSEStream(StreamByteSource &src, SSEEventParser &parser, const std::function< void(const std::string &)> &onEvent, size_t maxIterations=100000, size_t readSize=4096)
StreamPumpResult pumpEventStream(StreamByteSource &src, EventStreamFrameParser &parser, const std::function< void(const EventStreamFrameParser::Frame &)> &onFrame, size_t maxIterations=100000, size_t readSize=4096)