v0.7.0: image input, model registry, prefix-cache savings
Images - vision gate: text-only models get a note instead of image parts and no view_image tool; unknown models retry once without images on HTTP 400 - paste: Ctrl+V and /attach paste read the clipboard (wl-paste, xclip, pngpaste/osascript, Win32 PNG or CF_DIB converted to PNG); pasting image file paths attaches them - MCP image content is stored in the session and sent with the tool result - attach: rel path validation, base64 decode, /retry keeps images Model registry - built-in src/models_builtin.json embedded at build time, merged with <config>/models.json and cached GET /models metadata (context, vision, pricing); most specific rule wins per field - /model labels, /status source, --list-models [provider] Cache and cost - stable request prefix (frozen tools, notes as messages), DeepSeek prefix completion, OpenAI/Anthropic cache hints, /cost and hit rate in the status
This commit is contained in:
+646
@@ -0,0 +1,646 @@
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_attach.h"
|
||||
#include "cst_util.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* --- 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, "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 ------------------------------------------------------------- */
|
||||
|
||||
/* <session dir>/attachments/<session stem> — 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 "<id>.<ext>" 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. */
|
||||
static void make_id(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, "img-%08x", (unsigned)(h & 0xFFFFFFFFu));
|
||||
}
|
||||
|
||||
static cst_attachment_t *store(const char *session_path, const char *name,
|
||||
const char *data, size_t n, const char *origin,
|
||||
char *err, size_t err_sz) {
|
||||
if (!name || !name[0])
|
||||
name = "image";
|
||||
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;
|
||||
}
|
||||
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(name, data, n, a->id, sizeof(a->id));
|
||||
snprintf(a->rel, sizeof(a->rel), "%s.%s", a->id, 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->bytes = (long long)n;
|
||||
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 image in");
|
||||
return NULL;
|
||||
}
|
||||
if (!data || !n) {
|
||||
snprintf(err, err_sz, "image is empty");
|
||||
return NULL;
|
||||
}
|
||||
return store(session_path, name, data, n, "user", 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;
|
||||
cst_attachment_t *a = store(session_path, leaf, data, n, "user", err, err_sz);
|
||||
free(data);
|
||||
return 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; the image was deleted from the session "
|
||||
"store",
|
||||
a->name);
|
||||
return NULL;
|
||||
}
|
||||
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) {
|
||||
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 = strncmp(a->mime, "image/", 6) == 0 ? a->mime + 6 : a->mime;
|
||||
char dim[32] = "";
|
||||
if (a->width > 0 && a->height > 0)
|
||||
snprintf(dim, sizeof(dim), "%dx%d ", a->width, a->height);
|
||||
out[0] = 0;
|
||||
if (a->bytes >= 1024 * 1024)
|
||||
snprintf(out, out_sz, "%s%s %.1f MB", dim, kind,
|
||||
(double)a->bytes / (1024 * 1024));
|
||||
else
|
||||
snprintf(out, out_sz, "%s%s %.0f KB", dim, kind,
|
||||
(double)a->bytes / 1024);
|
||||
}
|
||||
|
||||
/* --- 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->detail[0] && strcmp(a->detail, "auto") != 0)
|
||||
cJSON_AddStringToObject(o, "detail", a->detail);
|
||||
if (a->origin[0])
|
||||
cJSON_AddStringToObject(o, "origin", a->origin);
|
||||
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, "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 : "");
|
||||
k++;
|
||||
}
|
||||
if (k == 0) {
|
||||
free(list);
|
||||
return NULL;
|
||||
}
|
||||
if (n_out)
|
||||
*n_out = k;
|
||||
return list;
|
||||
}
|
||||
Reference in New Issue
Block a user