Files
CST-agent/vendor/sashidown/src/stream.c
T
WHCandCursor 1f1fcba7ad Initial CST-Agent: portable Linux/Termux/Windows coding agent.
Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 22:42:32 +08:00

365 lines
10 KiB
C

#include "sashidown.h"
#include "line_index.h"
#include "fsm.h"
#include "render.h"
#include <stdlib.h>
#include <string.h>
#define STREAM_INPUT_INIT 4096
#define STREAM_OUTPUT_INIT 8192
#define STREAM_OUTPUT_MAX (64u * 1024u * 1024u)
#define STREAM_LINE_IX_INIT 256
struct sashidown_stream {
char *input_buf;
size_t input_len;
size_t input_cap;
size_t indexed_len; /* bytes already in line_ix */
size_t render_pos;
size_t last_complete_nl; /* [0, last_complete_nl) ends on line boundary */
sashidown_render_state_t render_state;
line_index_t line_ix;
char *output_buf;
size_t output_cap;
sashidown_ctx_t render_ctx;
sashidown_stream_cb on_html;
void *user;
int finished;
int aborted;
};
static int stream_grow_input(sashidown_stream_t *s, size_t need)
{
if (need <= s->input_cap)
return 0;
size_t cap = s->input_cap ? s->input_cap : STREAM_INPUT_INIT;
while (cap < need)
cap *= 2;
char *p = (char *)realloc(s->input_buf, cap);
if (!p)
return -1;
s->input_buf = p;
s->input_cap = cap;
return 0;
}
static int stream_grow_output(sashidown_stream_t *s, size_t need)
{
if (need <= s->output_cap)
return 0;
size_t cap = s->output_cap ? s->output_cap : STREAM_OUTPUT_INIT;
while (cap < need)
cap *= 2;
if (cap > STREAM_OUTPUT_MAX)
cap = STREAM_OUTPUT_MAX;
if (cap <= s->output_cap)
return -1;
char *p = (char *)realloc(s->output_buf, cap);
if (!p)
return -1;
s->output_buf = p;
s->output_cap = cap;
return 0;
}
static int stream_flush_html(sashidown_stream_t *s)
{
sashidown_ctx_t *ctx = &s->render_ctx;
if (ctx->output_len == 0)
return 0;
if (!s->on_html)
return 0;
int r = s->on_html(ctx->output, ctx->output_len, s->user);
if (r != 0) {
s->aborted = 1;
return -1;
}
ctx->output_len = 0;
return 0;
}
/* Sync line index through input_len; update last_complete_nl. */
static void stream_catchup_index(sashidown_stream_t *s)
{
if (s->indexed_len >= s->input_len)
return;
if (s->indexed_len == 0) {
line_index_build(s->input_buf, s->input_len, &s->line_ix);
size_t last = 0;
if (s->line_ix.valid && s->line_ix.n > 1) {
last = (size_t)s->line_ix.off[s->line_ix.n - 1];
} else {
const char *p = s->input_buf;
const char *end = s->input_buf + s->input_len;
const char *hit;
while ((hit = memchr(p, '\n', (size_t)(end - p))) != NULL) {
last = (size_t)(hit - s->input_buf) + 1;
p = hit + 1;
}
}
if (last)
s->last_complete_nl = last;
} else {
size_t nl = line_index_extend(&s->line_ix, s->input_buf,
s->indexed_len, s->input_len);
if (nl)
s->last_complete_nl = nl;
}
s->indexed_len = s->input_len;
}
/* 1 = overflow (state restored), 0 = ok, -1 = error */
static int stream_render_attempt(sashidown_stream_t *s, size_t stop, int is_final)
{
sashidown_ctx_t *ctx = &s->render_ctx;
size_t saved_pos = s->render_pos;
sashidown_render_state_t saved_state = s->render_state;
ctx->output_len = 0;
ctx->overflow = 0;
fsm_write_begin(ctx);
int r = sashidown_render_incremental(ctx, &s->render_state,
s->input_buf, s->input_len,
&s->render_pos, stop, is_final);
fsm_write_end(ctx);
if (ctx->overflow) {
s->render_pos = saved_pos;
s->render_state = saved_state;
return 1;
}
if (r != 0)
return -1;
return stream_flush_html(s);
}
static size_t stream_soft_stop(sashidown_stream_t *s, size_t from, size_t target)
{
if (from >= target)
return target;
/* Keep each attempt roughly within half the output buffer. */
size_t soft = from + s->output_cap / 2;
if (soft >= target)
return target;
const char *base = s->input_buf + from;
size_t lim = soft - from;
const char *p = base;
const char *end = base + lim;
const char *last = NULL;
const char *hit;
while ((hit = memchr(p, '\n', (size_t)(end - p))) != NULL) {
last = hit;
p = hit + 1;
}
if (last)
return (size_t)(last - s->input_buf) + 1;
/* No newline in soft window — take the next one so we still make progress. */
hit = memchr(s->input_buf + from, '\n', target - from);
if (hit)
return (size_t)(hit - s->input_buf) + 1;
return target;
}
static size_t stream_next_line_stop(sashidown_stream_t *s, size_t from, size_t limit)
{
if (from >= limit)
return limit;
const char *hit = memchr(s->input_buf + from, '\n', limit - from);
if (hit)
return (size_t)(hit - s->input_buf) + 1;
return limit;
}
/* Render up to pass_stop (is_final as given). Grow/shrink on overflow.
* Returns 0 on success, -1 on error. */
static int stream_render_window(sashidown_stream_t *s, size_t pass_stop, int is_final)
{
sashidown_ctx_t *ctx = &s->render_ctx;
for (;;) {
int ar = stream_render_attempt(s, pass_stop, is_final);
if (ar == 0)
return 0;
if (ar < 0)
return -1;
if (stream_grow_output(s, s->output_cap * 2) == 0) {
ctx->output = s->output_buf;
ctx->output_cap = s->output_cap;
continue;
}
size_t one = stream_next_line_stop(s, s->render_pos, pass_stop);
if (one <= s->render_pos || one >= pass_stop)
return -1;
pass_stop = one;
is_final = 0;
}
}
static int stream_render_pass(sashidown_stream_t *s, size_t stop, int is_final)
{
if (s->aborted)
return -1;
if (!is_final && stop <= s->render_pos)
return 0;
sashidown_ctx_t *ctx = &s->render_ctx;
ctx->input = s->input_buf;
ctx->input_len = s->input_len;
size_t target = is_final ? s->input_len : stop;
/* Common case: one or a few lines already fit in the output buffer. */
if (target - s->render_pos <= s->output_cap / 4) {
int pass_final = is_final && (target >= s->input_len);
size_t before = s->render_pos;
if (stream_render_window(s, target, pass_final) != 0)
return -1;
if (s->render_pos > before || pass_final)
return (is_final && !pass_final)
? stream_render_window(s, s->input_len, 1)
: 0;
if (!is_final)
return 0; /* deferred (incomplete table, etc.) */
/* fall through to spanned finalization */
}
while (s->render_pos < target) {
size_t before = s->render_pos;
size_t pass_stop = stream_soft_stop(s, s->render_pos, target);
int pass_final = is_final && (pass_stop >= s->input_len);
if (stream_render_window(s, pass_stop, pass_final) != 0)
return -1;
if (s->render_pos > before) {
if (pass_final)
return 0;
continue;
}
/* No progress: soft window may have deferred a table. Retry full target. */
if (pass_stop < target) {
pass_final = is_final && (target >= s->input_len);
if (stream_render_window(s, target, pass_final) != 0)
return -1;
if (s->render_pos > before) {
if (pass_final)
return 0;
continue;
}
}
/* Still stuck — waiting for more input (incomplete table, etc.). */
if (!is_final)
return 0;
return -1;
}
if (!is_final)
return 0;
/* EOF reached without a finalizing pass — close open blocks. */
return stream_render_window(s, s->input_len, 1);
}
sashidown_stream_t *sashidown_stream_create(const sashidown_stream_opts_t *opts)
{
if (!opts || !opts->on_html)
return NULL;
sashidown_stream_t *s = (sashidown_stream_t *)calloc(1, sizeof(*s));
if (!s)
return NULL;
size_t out_cap = opts->output_buf_size ? opts->output_buf_size : STREAM_OUTPUT_INIT;
if (out_cap > STREAM_OUTPUT_MAX)
out_cap = STREAM_OUTPUT_MAX;
s->output_cap = out_cap;
s->output_buf = (char *)malloc(s->output_cap);
if (!s->output_buf) {
free(s);
return NULL;
}
if (line_index_init_dynamic(&s->line_ix, STREAM_LINE_IX_INIT) != 0) {
free(s->output_buf);
free(s);
return NULL;
}
s->on_html = opts->on_html;
s->user = opts->user;
sashidown_render_state_init(&s->render_state);
s->render_ctx.output = s->output_buf;
s->render_ctx.output_cap = s->output_cap;
s->render_ctx.line_ix = &s->line_ix;
return s;
}
int sashidown_stream_feed(sashidown_stream_t *s, const char *chunk, size_t len)
{
if (!s || s->aborted)
return -1;
if (s->finished)
return 0;
if (!chunk && len > 0)
return -1;
if (len == 0)
return 0;
int has_nl = (memchr(chunk, '\n', len) != NULL);
if (stream_grow_input(s, s->input_len + len) != 0)
return -1;
memcpy(s->input_buf + s->input_len, chunk, len);
s->input_len += len;
/* Tiny-chunk fast path: no newline → defer index + render. */
if (!has_nl)
return 0;
stream_catchup_index(s);
if (s->last_complete_nl <= s->render_pos)
return 0;
return stream_render_pass(s, s->last_complete_nl, 0);
}
int sashidown_stream_finish(sashidown_stream_t *s)
{
if (!s || s->aborted)
return -1;
if (s->finished)
return 0;
stream_catchup_index(s);
int r = stream_render_pass(s, s->input_len, 1);
s->finished = 1;
return r;
}
void sashidown_stream_destroy(sashidown_stream_t *s)
{
if (!s)
return;
free(s->input_buf);
free(s->output_buf);
line_index_free(&s->line_ix);
free(s);
}
size_t sashidown_stream_pending(const sashidown_stream_t *s)
{
if (!s || s->input_len <= s->render_pos)
return 0;
return s->input_len - s->render_pos;
}