#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include "cJSON.h" #include "cst_attach.h" #include "cst_pdf.h" #include "cst_util.h" #include "os_compat.h" #include #include #include /* --- base64 -------------------------------------------------------------- */ static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; char *cst_base64_encode(const void *data, size_t n) { const unsigned char *p = data; size_t olen = ((n + 2) / 3) * 4; char *out = malloc(olen + 1); if (!out) return NULL; size_t o = 0; for (size_t i = 0; i < n; i += 3) { unsigned v = (unsigned)p[i] << 16; if (i + 1 < n) v |= (unsigned)p[i + 1] << 8; if (i + 2 < n) v |= (unsigned)p[i + 2]; out[o++] = B64[(v >> 18) & 63]; out[o++] = B64[(v >> 12) & 63]; out[o++] = i + 1 < n ? B64[(v >> 6) & 63] : '='; out[o++] = i + 2 < n ? B64[v & 63] : '='; } out[o] = 0; return out; } static int b64_val(unsigned char c) { if (c >= 'A' && c <= 'Z') return c - 'A'; if (c >= 'a' && c <= 'z') return c - 'a' + 26; if (c >= '0' && c <= '9') return c - '0' + 52; if (c == '+' || c == '-') return 62; if (c == '/' || c == '_') return 63; return -1; } char *cst_base64_decode(const char *s, size_t n, size_t *out_len) { if (out_len) *out_len = 0; if (!s) return NULL; char *out = malloc(n / 4 * 3 + 4); if (!out) return NULL; size_t o = 0; unsigned acc = 0; int bits = 0; for (size_t i = 0; i < n; i++) { unsigned char c = (unsigned char)s[i]; if (c == '=') break; if (c == ' ' || c == '\t' || c == '\r' || c == '\n') continue; int v = b64_val(c); if (v < 0) { free(out); return NULL; } acc = (acc << 6) | (unsigned)v; bits += 6; if (bits >= 8) { bits -= 8; out[o++] = (char)((acc >> bits) & 0xFF); } } if (out_len) *out_len = o; return out; } /* --- format sniffing ----------------------------------------------------- */ static int be32(const unsigned char *p) { return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; } static int le32(const unsigned char *p) { return (int)((unsigned)p[0] | ((unsigned)p[1] << 8) | ((unsigned)p[2] << 16) | ((unsigned)p[3] << 24)); } static int le16(const unsigned char *p) { return (int)((unsigned)p[0] | ((unsigned)p[1] << 8)); } static const char *mime_of_ext(const char *mime) { if (strcmp(mime, "application/pdf") == 0 || strcmp(mime, "text/plain") == 0) return "txt"; /* a document is stored as the text it was reduced to */ if (strcmp(mime, "image/png") == 0) return "png"; if (strcmp(mime, "image/jpeg") == 0) return "jpg"; if (strcmp(mime, "image/gif") == 0) return "gif"; if (strcmp(mime, "image/webp") == 0) return "webp"; if (strcmp(mime, "image/bmp") == 0) return "bmp"; return "bin"; } /* A JPEG SOF marker carries the frame size; skip it when scanning. */ static int jpeg_sof(int marker) { switch (marker) { case 0xC0: case 0xC1: case 0xC2: case 0xC3: case 0xC5: case 0xC6: case 0xC7: case 0xC9: case 0xCA: case 0xCB: case 0xCD: case 0xCE: case 0xCF: return 1; default: return 0; } } int cst_attach_sniff(const void *data, size_t n, char *mime, size_t mime_sz) { const unsigned char *p = data; const char *m = NULL; if (n >= 8 && memcmp(p, "\x89PNG\r\n\x1a\n", 8) == 0) m = "image/png"; else if (n >= 3 && p[0] == 0xFF && p[1] == 0xD8 && p[2] == 0xFF) m = "image/jpeg"; else if (n >= 6 && memcmp(p, "GIF87a", 6) == 0) m = "image/gif"; else if (n >= 6 && memcmp(p, "GIF89a", 6) == 0) m = "image/gif"; else if (n >= 12 && memcmp(p, "RIFF", 4) == 0 && memcmp(p + 8, "WEBP", 4) == 0) m = "image/webp"; else if (n >= 2 && p[0] == 'B' && p[1] == 'M') m = "image/bmp"; if (!m) return -1; snprintf(mime, mime_sz, "%s", m); return 0; } /* PNG / GIF / BMP are direct; JPEG needs a marker walk; WebP has three * container flavours. */ int cst_attach_dimensions(const void *data, size_t n, int *w, int *h) { const unsigned char *p = data; *w = *h = 0; char mime[32]; if (cst_attach_sniff(data, n, mime, sizeof(mime)) != 0) return -1; if (strcmp(mime, "image/png") == 0) { /* 8 signature + 4 length + "IHDR" + w + h */ if (n < 24 || memcmp(p + 12, "IHDR", 4) != 0) return -1; *w = be32(p + 16); *h = be32(p + 20); return 0; } if (strcmp(mime, "image/gif") == 0) { if (n < 10) return -1; *w = le16(p + 6); *h = le16(p + 8); return 0; } if (strcmp(mime, "image/bmp") == 0) { if (n < 26) return -1; int wi = le32(p + 18); int hi = le32(p + 22); if (wi < 0) wi = -wi; /* bottom-up rows are stored with a negative height */ if (hi < 0) hi = -hi; *w = wi; *h = hi; return 0; } if (strcmp(mime, "image/webp") == 0) { if (n < 30) return -1; const unsigned char *c = p + 12; if (memcmp(c, "VP8 ", 4) == 0) { /* 4 tag + 3 start code, then 16-bit width / height (14 bits each) */ if (n < 30 || !(c[7] == 0x9D && c[8] == 0x01 && c[9] == 0x2A)) return -1; *w = le16(c + 10) & 0x3FFF; *h = le16(c + 12) & 0x3FFF; return 0; } if (memcmp(c, "VP8L", 4) == 0) { if (n < 25 || c[8] != 0x2F) return -1; unsigned b = (unsigned)c[9] | ((unsigned)c[10] << 8) | ((unsigned)c[11] << 16) | ((unsigned)c[12] << 24); *w = (int)(b & 0x3FFF) + 1; *h = (int)((b >> 14) & 0x3FFF) + 1; return 0; } if (memcmp(c, "VP8X", 4) == 0) { if (n < 30) return -1; *w = (int)((unsigned)c[12] | ((unsigned)c[13] << 8) | ((unsigned)c[14] << 16)) + 1; *h = (int)((unsigned)c[15] | ((unsigned)c[16] << 8) | ((unsigned)c[17] << 16)) + 1; return 0; } return -1; } if (strcmp(mime, "image/jpeg") == 0) { size_t i = 2; while (i + 3 < n) { if (p[i] != 0xFF) return -1; /* lost sync */ unsigned marker = p[i + 1]; i += 2; if (marker == 0xD8 || (marker >= 0xD0 && marker <= 0xD7)) continue; /* standalone markers: no payload */ if (marker == 0xD9 || marker == 0x01) continue; if (i + 1 >= n) return -1; size_t seg = (size_t)((p[i] << 8) | p[i + 1]); if (seg < 2 || i + seg > n) return -1; if (jpeg_sof((int)marker) && seg >= 7) { *h = (p[i + 3] << 8) | p[i + 4]; *w = (p[i + 5] << 8) | p[i + 6]; return 0; } i += seg; } } return -1; } /* --- storage ------------------------------------------------------------- */ /* /attachments/ — beside the JSONL so moving the * workspace keeps the two together. */ int cst_attach_dir(const char *session_path, char *out, size_t out_sz) { if (!session_path || !session_path[0]) return -1; char dir[OS_PATH_MAX]; snprintf(dir, sizeof(dir), "%s", session_path); char *slash = strrchr(dir, '/'); #ifdef _WIN32 char *bslash = strrchr(dir, '\\'); if (bslash && (!slash || bslash > slash)) slash = bslash; #endif char leaf[256]; const char *base = slash ? slash + 1 : dir; size_t bl = strlen(base); if (bl >= sizeof(leaf)) bl = sizeof(leaf) - 1; memcpy(leaf, base, bl); leaf[bl] = 0; if (slash) *(slash + 1) = 0; else { dir[0] = '.'; dir[1] = 0; } char *dot = strstr(leaf, ".jsonl"); if (dot) *dot = 0; char rel[320]; snprintf(rel, sizeof(rel), "attachments/%s", leaf[0] ? leaf : "session"); char full[OS_PATH_MAX]; if (os_path_join(full, sizeof(full), dir, rel) != 0) return -1; if (os_mkdir_p(full, 0700) != 0) return -1; snprintf(out, out_sz, "%s", full); return 0; } /* Stored names are "." made by store(); anything else in a JSONL * (hand-edited or crafted) could point outside the attachment directory. */ int cst_attach_rel_ok(const char *rel) { if (!rel || !rel[0] || rel[0] == '.') return 0; for (const char *p = rel; *p; p++) { char c = *p; if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.')) return 0; } return strstr(rel, "..") == NULL; } int cst_attach_full_path(const char *session_path, const cst_attachment_t *a, char *out, size_t out_sz) { if (!a || !cst_attach_rel_ok(a->rel)) return -1; char dir[OS_PATH_MAX]; if (cst_attach_dir(session_path, dir, sizeof(dir)) != 0) return -1; return os_path_join(out, out_sz, dir, a->rel); } static char *read_whole(const char *path, size_t *out_len, size_t max) { FILE *f = fopen(path, "rb"); if (!f) return NULL; cst_buf_t b = {0}; char chunk[16384]; size_t n; while ((n = fread(chunk, 1, sizeof(chunk), f)) > 0) { if (b.len + n > max || cst_buf_append(&b, chunk, n) != 0) { cst_buf_free(&b); fclose(f); return NULL; } } fclose(f); *out_len = b.len; return cst_buf_steal(&b); } /* FNV over the bytes, the name and the clock: short, stable, unique enough * inside one session. The prefix says what the name is for: "img" for what a * model is shown, "art" for what a person is handed. */ static void make_id(const char *prefix, const char *name, const char *data, size_t n, char *out, size_t out_sz) { uint64_t h = cst_fnv1a64(CST_FNV_INIT, data, n > 4096 ? 4096 : n); h = cst_fnv1a64(h, name, strlen(name)); h = cst_fnv1a64(h, &n, sizeof(n)); double now = os_now_ms(); h = cst_fnv1a64(h, &now, sizeof(now)); h = cst_fnv1a64(h, &(int){os_getpid()}, sizeof(int)); snprintf(out, out_sz, "%s-%08x", prefix && prefix[0] ? prefix : "img", (unsigned)(h & 0xFFFFFFFFu)); } /* n bytes, NUL-terminated: a text payload is handed around as a C string. */ static char *memdup_z(const char *data, size_t n) { char *copy = malloc(n + 1); if (!copy) return NULL; memcpy(copy, data, n); copy[n] = 0; return copy; } /* What a picked file is, plus the payload to store for it: an image stays as * it is, a PDF is reduced to its text layer (the mime still says PDF, the * stored file holds text), anything else must be UTF-8 text. The caller frees * the result; *out_len is its length. */ static char *prepare(const char *data, size_t n, char *mime, size_t mime_sz, int *pages, size_t *out_len, char *err, size_t err_sz) { *pages = 0; *out_len = n; if (cst_attach_sniff(data, n, mime, mime_sz) == 0) { if (n > CST_ATTACH_MAX_BYTES) { snprintf(err, err_sz, "image is %.1f MB; the limit is %d MB", (double)n / (1024 * 1024), (int)(CST_ATTACH_MAX_BYTES >> 20)); return NULL; } char *copy = memdup_z(data, n); if (!copy) snprintf(err, err_sz, "out of memory"); return copy; } if (n >= 4 && memcmp(data, "%PDF", 4) == 0) { int pg = 0; char perr[256]; char *text = cst_pdf_text(data, n, &pg, perr, sizeof(perr)); if (!text) { snprintf(err, err_sz, "cannot read the PDF: %.200s", perr); return NULL; } snprintf(mime, mime_sz, "application/pdf"); *pages = pg; *out_len = strlen(text); return text; } if (n > CST_ATTACH_TEXT_MAX) { snprintf(err, err_sz, "text is %.1f MB; the limit is %d MB", (double)n / (1024 * 1024), (int)(CST_ATTACH_TEXT_MAX >> 20)); return NULL; } if (memchr(data, 0, n) || !cst_utf8_valid(data, n)) { snprintf(err, err_sz, "not an image (PNG, JPEG, GIF, WebP or BMP), a PDF or UTF-8 " "text; the content decides, not the extension"); return NULL; } snprintf(mime, mime_sz, "text/plain"); char *copy = memdup_z(data, n); if (!copy) snprintf(err, err_sz, "out of memory"); return copy; } /* Write one payload whose kind is already known. id_prefix names it ("img" / * "art"), for_user marks it as the person's rather than the model's, and * ext_override is the stored file's extension when the caller knows better * than the mime does. */ static cst_attachment_t *store(const char *session_path, const char *name, const char *data, size_t n, const char *mime, int pages, const char *origin, int for_user, const char *id_prefix, const char *ext_override, char *err, size_t err_sz) { if (!name || !name[0]) name = "image"; char dir[OS_PATH_MAX]; if (cst_attach_dir(session_path, dir, sizeof(dir)) != 0) { snprintf(err, err_sz, "cannot create the attachment directory"); return NULL; } cst_attachment_t *a = calloc(1, sizeof(*a)); if (!a) { snprintf(err, err_sz, "out of memory"); return NULL; } make_id(id_prefix, name, data, n, a->id, sizeof(a->id)); snprintf(a->rel, sizeof(a->rel), "%s.%s", a->id, ext_override && ext_override[0] ? ext_override : mime_of_ext(mime)); snprintf(a->name, sizeof(a->name), "%s", name && name[0] ? name : "image"); snprintf(a->mime, sizeof(a->mime), "%s", mime); snprintf(a->origin, sizeof(a->origin), "%s", origin ? origin : "user"); snprintf(a->detail, sizeof(a->detail), "auto"); a->for_user = for_user ? 1 : 0; a->bytes = (long long)n; a->pages = pages; if (cst_attach_is_image(a)) cst_attach_dimensions(data, n, &a->width, &a->height); char full[OS_PATH_MAX]; if (cst_attach_full_path(session_path, a, full, sizeof(full)) != 0) { snprintf(err, err_sz, "attachment path is too long"); free(a); return NULL; } FILE *f = fopen(full, "wb"); if (!f) { snprintf(err, err_sz, "cannot write %.200s", full); free(a); return NULL; } size_t w = fwrite(data, 1, n, f); if (fclose(f) != 0 || w != n) { snprintf(err, err_sz, "cannot write %.200s", full); free(a); return NULL; } return a; } cst_attachment_t *cst_attach_import_bytes(const char *session_path, const char *name, const char *data, size_t n, char *err, size_t err_sz) { char dummy[8]; if (!err || !err_sz) { err = dummy; err_sz = sizeof(dummy); } err[0] = 0; if (!session_path || !session_path[0]) { snprintf(err, err_sz, "no session to store the file in"); return NULL; } if (!data || !n) { snprintf(err, err_sz, "the file is empty"); return NULL; } char mime[32]; if (cst_attach_sniff(data, n, mime, sizeof(mime)) != 0) { snprintf(err, err_sz, "not a supported image (PNG, JPEG, GIF, WebP or BMP; " "checked by content, not by extension)"); return NULL; } if (n > CST_ATTACH_MAX_BYTES) { snprintf(err, err_sz, "image is %.1f MB; the limit is %d MB", (double)n / (1024 * 1024), (int)(CST_ATTACH_MAX_BYTES >> 20)); return NULL; } return store(session_path, name, data, n, mime, 0, "user", 0, "img", NULL, err, err_sz); } cst_attachment_t *cst_attach_import_file(const char *session_path, const char *src_path, char *err, size_t err_sz) { char dummy[8]; if (!err || !err_sz) { err = dummy; err_sz = sizeof(dummy); } err[0] = 0; size_t n = 0; char *data = read_whole(src_path, &n, CST_ATTACH_MAX_BYTES); if (!data) { snprintf(err, err_sz, "cannot read %.300s (or it is larger than %d MB)", src_path, (int)(CST_ATTACH_MAX_BYTES >> 20)); return NULL; } const char *leaf = src_path; for (const char *p = src_path; *p; p++) if (*p == '/' || *p == '\\') leaf = p + 1; char mime[32]; int pages = 0; size_t pn = 0; char *payload = prepare(data, n, mime, sizeof(mime), &pages, &pn, err, err_sz); free(data); if (!payload) return NULL; cst_attachment_t *a = store(session_path, leaf, payload, pn, mime, pages, "user", 0, "img", NULL, err, err_sz); free(payload); return a; } /* The mime of a delivered file: its extension decides (the content is not * sniffed -- a .zip is not a picture), and anything unknown is a plain blob * the OS still opens by name. */ static const char *mime_from_name(const char *name) { static const struct { const char *ext, *mime; } T[] = { {"md", "text/markdown"}, {"markdown", "text/markdown"}, {"txt", "text/plain"}, {"log", "text/plain"}, {"json", "application/json"}, {"csv", "text/csv"}, {"html", "text/html"}, {"htm", "text/html"}, {"xml", "application/xml"}, {"yaml", "application/yaml"}, {"yml", "application/yaml"}, {"toml", "application/toml"}, {"pdf", "application/pdf"}, {"zip", "application/zip"}, {"gz", "application/gzip"}, {"tgz", "application/gzip"}, {"tar", "application/x-tar"}, {"sh", "text/x-shellscript"}, {"py", "text/x-python"}, {"c", "text/x-c"}, {"h", "text/x-c"}, {"cpp", "text/x-c++"}, {"java", "text/x-java"}, {"kt", "text/x-kotlin"}, {"js", "text/javascript"}, {"ts", "text/typescript"}, {"go", "text/x-go"}, {"rs", "text/x-rust"}, {"rb", "text/x-ruby"}, {"qml", "text/x-qml"}, {"css", "text/css"}, {"sql", "text/x-sql"}, {"diff", "text/x-diff"}, {"patch", "text/x-diff"}, {"png", "image/png"}, {"jpg", "image/jpeg"}, {"jpeg", "image/jpeg"}, {"gif", "image/gif"}, {"webp", "image/webp"}, {"bmp", "image/bmp"}, {"svg", "image/svg+xml"}, {"ico", "image/x-icon"}, {"mp3", "audio/mpeg"}, {"m4a", "audio/mp4"}, {"ogg", "audio/ogg"}, {"wav", "audio/wav"}, {"mp4", "video/mp4"}, {"webm", "video/webm"}, {"apk", "application/vnd.android.package-archive"}, {"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, {"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, {"pptx", "application/vnd.openxmlformats-officedocument.presentationml." "presentation"}, }; const char *dot = name ? strrchr(name, '.') : NULL; if (!dot || !dot[1]) return "application/octet-stream"; const char *e = dot + 1; size_t el = strlen(e); for (size_t i = 0; i < sizeof(T) / sizeof(T[0]); i++) { if (strlen(T[i].ext) != el) continue; size_t k = 0; for (; k < el; k++) { char c = e[k]; if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); if (c != T[i].ext[k]) break; } if (k == el) return T[i].mime; } return "application/octet-stream"; } /* The stored file's extension: the name's own when it is plain ([a-z0-9], at * most 8 characters -- a stored name has to stay portable), else whatever the * mime maps to. */ static void ext_for_name(const char *name, const char *mime, char *out, size_t out_sz) { size_t k = 0; const char *dot = name ? strrchr(name, '.') : NULL; if (dot && dot[1] && out_sz > 1) { for (const char *p = dot + 1; *p && k < 8 && k + 1 < out_sz; p++) { char c = *p; if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))) break; out[k++] = c; } } out[k] = 0; if (!k) snprintf(out, out_sz, "%s", mime_of_ext(mime)); } cst_attachment_t *cst_attach_import_artifact(const char *session_path, const char *src_path, const char *title, char *err, size_t err_sz) { char dummy[8]; if (!err || !err_sz) { err = dummy; err_sz = sizeof(dummy); } err[0] = 0; if (!session_path || !session_path[0]) { snprintf(err, err_sz, "no session to store the file in"); return NULL; } if (!src_path || !src_path[0]) { snprintf(err, err_sz, "no file to deliver"); return NULL; } size_t n = 0; char *data = read_whole(src_path, &n, CST_ARTIFACT_MAX_BYTES); if (!data) { snprintf(err, err_sz, "cannot read %.300s (or it is larger than %d MB)", src_path, (int)(CST_ARTIFACT_MAX_BYTES >> 20)); return NULL; } const char *leaf = src_path; for (const char *p = src_path; *p; p++) if (*p == '/' || *p == '\\') leaf = p + 1; const char *name = (title && title[0]) ? title : leaf; char mime[64], ext[16]; snprintf(mime, sizeof(mime), "%s", mime_from_name(name)); ext_for_name(name, mime, ext, sizeof(ext)); cst_attachment_t *a = store(session_path, name, data, n, mime, 0, "tool", 1, "art", ext, err, err_sz); free(data); return a; } /* Images are opaque and travel as image parts; everything else is text the * store holds and the request inlines. */ int cst_attach_is_image(const cst_attachment_t *a) { return a && strncmp(a->mime, "image/", 6) == 0; } int cst_attach_is_text(const cst_attachment_t *a) { /* A delivered file is the person's: it is never inlined into a request. */ return a && !a->for_user && !cst_attach_is_image(a); } char *cst_attach_read(const char *session_path, const cst_attachment_t *a, size_t *out_len, char *err, size_t err_sz) { char dummy[8]; if (!err || !err_sz) { err = dummy; err_sz = sizeof(dummy); } err[0] = 0; char full[OS_PATH_MAX]; if (cst_attach_full_path(session_path, a, full, sizeof(full)) != 0) { snprintf(err, err_sz, "attachment has no stored file"); return NULL; } char *data = read_whole(full, out_len, CST_ATTACH_MAX_BYTES); if (!data) { snprintf(err, err_sz, "%.200s is missing; it was deleted from the session store", a->name); return NULL; } if (!cst_attach_is_image(a)) /* a document is text, nothing to verify */ return data; char mime[32]; if (cst_attach_sniff(data, *out_len, mime, sizeof(mime)) != 0) { snprintf(err, err_sz, "%.200s in the session store is no longer an image", a->name); free(data); return NULL; } return data; } char *cst_attach_data_url(const char *session_path, const cst_attachment_t *a) { if (!cst_attach_is_image(a)) /* a document travels as text, not as a URL */ return NULL; char err[256]; size_t n = 0; char *data = cst_attach_read(session_path, a, &n, err, sizeof(err)); if (!data) return NULL; char mime[32]; cst_attach_sniff(data, n, mime, sizeof(mime)); /* the bytes decide */ char *b64 = cst_base64_encode(data, n); free(data); if (!b64) return NULL; cst_buf_t b = {0}; cst_buf_printf(&b, "data:%s;base64,", mime); cst_buf_puts(&b, b64); free(b64); return cst_buf_steal(&b); } void cst_attach_describe(const cst_attachment_t *a, char *out, size_t out_sz) { if (!a) { snprintf(out, out_sz, "(none)"); return; } const char *kind = a->mime; char dim[32] = ""; if (cst_attach_is_image(a)) { if (strncmp(a->mime, "image/", 6) == 0) kind = a->mime + 6; if (a->width > 0 && a->height > 0) snprintf(dim, sizeof(dim), "%dx%d ", a->width, a->height); } else if (strcmp(a->mime, "application/pdf") == 0) { kind = "pdf"; if (a->pages > 0) snprintf(dim, sizeof(dim), "%d page%s ", a->pages, a->pages == 1 ? "" : "s"); } else if (strcmp(a->mime, "text/plain") == 0 || !a->mime[0]) { kind = "text"; } out[0] = 0; if (a->bytes >= 1024 * 1024) snprintf(out, out_sz, "%s%s %.1f MB", dim, kind, (double)a->bytes / (1024 * 1024)); else if (a->bytes >= 1024) snprintf(out, out_sz, "%s%s %.1f KB", dim, kind, (double)a->bytes / 1024); else snprintf(out, out_sz, "%s%s %lld B", dim, kind, a->bytes); } /* --- lifetime ------------------------------------------------------------ */ void cst_attach_free(cst_attachment_t *a) { free(a); } /* Entries always live in one array (a 1-element array for a single import), * so freeing the block releases them all. */ void cst_attachments_free(cst_attachment_t *list, int n) { (void)n; free(list); } cst_attachment_t *cst_attachments_dup(const cst_attachment_t *list, int n) { if (!list || n <= 0) return NULL; cst_attachment_t *out = calloc((size_t)n, sizeof(*out)); if (!out) return NULL; for (int i = 0; i < n; i++) out[i] = list[i]; return out; } int cst_attachments_push(cst_attachment_t **list, int *n, int *cap, const cst_attachment_t *src) { if (!list || !n || !cap || !src) return -1; if (*n == *cap) { int nc = *cap ? *cap * 2 : 4; cst_attachment_t *nv = realloc(*list, (size_t)nc * sizeof(**list)); if (!nv) return -1; *list = nv; *cap = nc; } (*list)[(*n)++] = *src; return 0; } /* --- JSONL --------------------------------------------------------------- */ char *cst_attachments_to_json(const cst_attachment_t *list, int n) { if (!list || n <= 0) return NULL; cJSON *arr = cJSON_CreateArray(); if (!arr) return NULL; for (int i = 0; i < n; i++) { const cst_attachment_t *a = &list[i]; cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "id", a->id); cJSON_AddStringToObject(o, "name", a->name); cJSON_AddStringToObject(o, "mime", a->mime); cJSON_AddStringToObject(o, "rel", a->rel); cJSON_AddNumberToObject(o, "bytes", (double)a->bytes); if (a->width > 0 && a->height > 0) { cJSON_AddNumberToObject(o, "width", a->width); cJSON_AddNumberToObject(o, "height", a->height); } if (a->pages > 0) cJSON_AddNumberToObject(o, "pages", a->pages); if (a->detail[0] && strcmp(a->detail, "auto") != 0) cJSON_AddStringToObject(o, "detail", a->detail); if (a->origin[0]) cJSON_AddStringToObject(o, "origin", a->origin); if (a->for_user) cJSON_AddNumberToObject(o, "for_user", 1); cJSON_AddItemToArray(arr, o); } char *s = cJSON_PrintUnformatted(arr); cJSON_Delete(arr); return s; } cst_attachment_t *cst_attachments_from_json(const void *array_json, int *n_out) { if (n_out) *n_out = 0; const cJSON *arr = array_json; if (!cJSON_IsArray(arr)) return NULL; int n = cJSON_GetArraySize(arr); if (n <= 0) return NULL; cst_attachment_t *list = calloc((size_t)n, sizeof(*list)); if (!list) return NULL; int k = 0; const cJSON *it = NULL; cJSON_ArrayForEach(it, arr) { if (!cJSON_IsObject(it)) continue; const cJSON *jid = cJSON_GetObjectItemCaseSensitive(it, "id"); const cJSON *jrel = cJSON_GetObjectItemCaseSensitive(it, "rel"); if (!cJSON_IsString(jid) || !cJSON_IsString(jrel) || !cst_attach_rel_ok(jrel->valuestring)) continue; cst_attachment_t *a = &list[k]; snprintf(a->id, sizeof(a->id), "%s", jid->valuestring); snprintf(a->rel, sizeof(a->rel), "%s", jrel->valuestring); const cJSON *v = cJSON_GetObjectItemCaseSensitive(it, "name"); snprintf(a->name, sizeof(a->name), "%s", cJSON_IsString(v) ? v->valuestring : a->id); v = cJSON_GetObjectItemCaseSensitive(it, "mime"); snprintf(a->mime, sizeof(a->mime), "%s", cJSON_IsString(v) ? v->valuestring : "image/*"); v = cJSON_GetObjectItemCaseSensitive(it, "bytes"); a->bytes = cJSON_IsNumber(v) ? (long long)v->valuedouble : 0; v = cJSON_GetObjectItemCaseSensitive(it, "width"); a->width = cJSON_IsNumber(v) ? v->valueint : 0; v = cJSON_GetObjectItemCaseSensitive(it, "height"); a->height = cJSON_IsNumber(v) ? v->valueint : 0; v = cJSON_GetObjectItemCaseSensitive(it, "pages"); a->pages = cJSON_IsNumber(v) ? v->valueint : 0; v = cJSON_GetObjectItemCaseSensitive(it, "detail"); snprintf(a->detail, sizeof(a->detail), "%s", cJSON_IsString(v) ? v->valuestring : "auto"); v = cJSON_GetObjectItemCaseSensitive(it, "origin"); snprintf(a->origin, sizeof(a->origin), "%s", cJSON_IsString(v) ? v->valuestring : ""); v = cJSON_GetObjectItemCaseSensitive(it, "for_user"); a->for_user = cJSON_IsNumber(v) ? v->valueint : 0; k++; } if (k == 0) { free(list); return NULL; } if (n_out) *n_out = k; return list; }