forked from WHC/Hermit
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
647 lines
19 KiB
C
647 lines
19 KiB
C
#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;
|
|
}
|