Files
CST-agent/src/clipboard.c
T
WHC e0b0eddf3a v0.8.6: general mode, clipboard text, PDF text extraction
Working mode: --mode coding|general (CST_MODE, settings.json, /mode,
WebUI run settings). general swaps the default framing for a general
assistant one -- workflow, safety notes, tool wording (run_shell, the
subagents) -- and /status shows it; minimal (the paper profile) stays
byte-frozen. The prompt state carries the mode so a switch leaves a
system note in history.

Clipboard: clipboard_read / clipboard_write tools (16 MiB cap read,
default 64 KiB window; writing is a WRITE permission with a preview) on
pbcopy / wl-copy / xclip / pbpaste / wl-paste / xclip and the Win32
clipboard. In the TUI, /attach paste falls back to clipboard text when
there is no image on it.

PDF: a dependency-free text extractor (src/pdf_text.c): inflate,
Flate/ASCIIHex/ASCII85, PNG/TIFF predictors, layout-independent object
scan, object streams, page tree walk with inherited resources, content
lexer (Tj/TJ/'/" and the Td/TD/T*/Tm spacing heuristics), per-font
ToUnicode CMaps and inline images skipped. Encrypted files and pages
with no text layer are refused with the reason.

Attachments: /attach (a path or files dropped on the terminal) now take
images, PDFs and text. A PDF is stored as its text layer with the mime
still saying application/pdf and its page count; anything else has to be
UTF-8 text. Documents travel inline as text parts, so CST_NO_IMAGES and
text-only models no longer drop them, while images keep their old path.

Tests: test_pdf_text (plain, bare and zlib-framed Flate, inline image,
CID + ToUnicode, object stream, error paths), documents in test_attach,
inlined documents in test_llm_body, mode in test_config.
2026-10-11 11:09:39 +08:00

652 lines
17 KiB
C

#include "cst_clipboard.h"
#include "cst_attach.h"
#include "os_compat.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
static void set_err(char *err, size_t err_sz, const char *msg) {
if (err && err_sz)
snprintf(err, err_sz, "%s", msg);
}
static void put_le32(unsigned char *p, unsigned long v) {
p[0] = (unsigned char)(v & 0xff);
p[1] = (unsigned char)((v >> 8) & 0xff);
p[2] = (unsigned char)((v >> 16) & 0xff);
p[3] = (unsigned char)((v >> 24) & 0xff);
}
static void put_be32(unsigned char *p, unsigned long v) {
p[0] = (unsigned char)((v >> 24) & 0xff);
p[1] = (unsigned char)((v >> 16) & 0xff);
p[2] = (unsigned char)((v >> 8) & 0xff);
p[3] = (unsigned char)(v & 0xff);
}
static unsigned long get_le32(const unsigned char *p) {
return (unsigned long)p[0] | (unsigned long)p[1] << 8 |
(unsigned long)p[2] << 16 | (unsigned long)p[3] << 24;
}
static unsigned long crc32_update(unsigned long crc, const unsigned char *p,
size_t n) {
static unsigned long table[256];
static int ready;
if (!ready) {
for (unsigned long i = 0; i < 256; i++) {
unsigned long c = i;
for (int k = 0; k < 8; k++)
c = c & 1 ? 0xedb88320UL ^ (c >> 1) : c >> 1;
table[i] = c;
}
ready = 1;
}
crc ^= 0xffffffffUL;
for (size_t i = 0; i < n; i++)
crc = table[(crc ^ p[i]) & 0xff] ^ (crc >> 8);
return crc ^ 0xffffffffUL;
}
static unsigned char *png_chunk(unsigned char *o, const char *type,
const unsigned char *data, size_t n) {
put_be32(o, (unsigned long)n);
memcpy(o + 4, type, 4);
if (n)
memcpy(o + 8, data, n);
put_be32(o + 8 + n, crc32_update(0, o + 4, n + 4));
return o + 12 + n;
}
/* RGB rows (filter byte 0 each) in stored deflate blocks: no compressor in
* the tree, and clipboard screenshots stay well under the attach cap. */
static char *png_from_raw(const unsigned char *raw, size_t raw_n, long w,
long h, size_t *n) {
size_t blocks = raw_n / 65535 + 1;
size_t z_n = 2 + raw_n + blocks * 5 + 4;
size_t total = 8 + (12 + 13) + (12 + z_n) + 12;
unsigned char *out = malloc(total);
unsigned char *z = malloc(z_n);
if (!out || !z) {
free(out);
free(z);
return NULL;
}
unsigned char *q = z;
*q++ = 0x78;
*q++ = 0x01;
unsigned long a = 1, b = 0;
size_t left = raw_n, pos = 0;
do {
size_t len = left > 65535 ? 65535 : left;
*q++ = left == len ? 1 : 0;
q[0] = (unsigned char)(len & 0xff);
q[1] = (unsigned char)(len >> 8);
q[2] = (unsigned char)(~len & 0xff);
q[3] = (unsigned char)((~len >> 8) & 0xff);
q += 4;
memcpy(q, raw + pos, len);
for (size_t i = 0; i < len; i++) {
a = (a + raw[pos + i]) % 65521;
b = (b + a) % 65521;
}
q += len;
pos += len;
left -= len;
} while (left);
put_be32(q, b << 16 | a);
q += 4;
unsigned char ihdr[13];
put_be32(ihdr, (unsigned long)w);
put_be32(ihdr + 4, (unsigned long)h);
ihdr[8] = 8; /* bit depth */
ihdr[9] = 2; /* RGB */
ihdr[10] = ihdr[11] = ihdr[12] = 0;
unsigned char *o = out;
memcpy(o, "\x89PNG\r\n\x1a\n", 8);
o = png_chunk(o + 8, "IHDR", ihdr, sizeof(ihdr));
o = png_chunk(o, "IDAT", z, (size_t)(q - z));
o = png_chunk(o, "IEND", NULL, 0);
free(z);
*n = (size_t)(o - out);
return (char *)out;
}
/* CF_DIB is a BMP without its 14-byte file header; the pixel offset depends
* on the header size, BI_BITFIELDS masks and the color table. */
char *cst_dib_to_image(const void *dib_v, size_t sz, size_t *n) {
const unsigned char *dib = dib_v;
if (!dib || sz < 40)
return NULL;
unsigned long hsize = get_le32(dib);
long w = (long)(int)get_le32(dib + 4);
long hh = (long)(int)get_le32(dib + 8);
unsigned bits = (unsigned)(dib[14] | dib[15] << 8);
unsigned long comp = get_le32(dib + 16);
unsigned long used = get_le32(dib + 32);
if (hsize < 40 || hsize > sz)
return NULL;
size_t off = hsize;
if (hsize == 40 && comp == 3 /* BI_BITFIELDS */)
off += 12;
size_t colors = used;
if (!colors && bits <= 8)
colors = (size_t)1 << bits;
if (colors > 256)
return NULL;
off += colors * 4;
if (off > sz)
return NULL;
long h = hh < 0 ? -hh : hh;
if ((bits == 24 || bits == 32) && (comp == 0 || comp == 3) && w > 0 &&
h > 0 && w <= 32768 && h <= 32768) {
size_t stride = ((size_t)w * bits / 8 + 3) & ~(size_t)3;
size_t raw_n = (size_t)h * (1 + (size_t)w * 3);
if (stride * (size_t)h <= sz - off && raw_n < CST_ATTACH_MAX_BYTES) {
unsigned char *raw = malloc(raw_n);
if (!raw)
return NULL;
size_t bpp = bits / 8;
for (long y = 0; y < h; y++) {
long src_y = hh > 0 ? h - 1 - y : y; /* positive height = bottom-up */
const unsigned char *s = dib + off + (size_t)src_y * stride;
unsigned char *d = raw + (size_t)y * (1 + (size_t)w * 3);
*d++ = 0;
for (long x = 0; x < w; x++, s += bpp) {
*d++ = s[2];
*d++ = s[1];
*d++ = s[0];
}
}
char *png = png_from_raw(raw, raw_n, w, h, n);
free(raw);
return png;
}
}
char *out = malloc(14 + sz);
if (!out)
return NULL;
unsigned char *hd = (unsigned char *)out;
hd[0] = 'B';
hd[1] = 'M';
put_le32(hd + 2, (unsigned long)(14 + sz));
put_le32(hd + 6, 0);
put_le32(hd + 10, (unsigned long)(14 + off));
memcpy(out + 14, dib, sz);
*n = 14 + sz;
return out;
}
#ifdef _WIN32
int cst_clipboard_image(char **data, size_t *n, char *err, size_t err_sz) {
*data = NULL;
*n = 0;
UINT png = RegisterClipboardFormatA("PNG");
if (!OpenClipboard(NULL)) {
set_err(err, err_sz, "could not open the clipboard");
return -1;
}
HANDLE h = NULL;
int is_png = 0;
if (png && IsClipboardFormatAvailable(png)) {
h = GetClipboardData(png);
is_png = 1;
} else if (IsClipboardFormatAvailable(CF_DIB)) {
h = GetClipboardData(CF_DIB);
}
const unsigned char *p = h ? GlobalLock(h) : NULL;
if (!p) {
CloseClipboard();
set_err(err, err_sz, "no image on the clipboard");
return -1;
}
size_t sz = (size_t)GlobalSize(h);
char *out = NULL;
if (sz > CST_ATTACH_MAX_BYTES) {
set_err(err, err_sz, "clipboard image is too large");
} else if (is_png) {
out = malloc(sz ? sz : 1);
if (out) {
memcpy(out, p, sz);
*n = sz;
}
} else {
out = cst_dib_to_image(p, sz, n);
if (!out)
set_err(err, err_sz, "clipboard bitmap is malformed");
}
GlobalUnlock(h);
CloseClipboard();
if (!out)
return -1;
*data = out;
return 0;
}
#else
/* Runs cmd (stderr already redirected by the caller); 127 = not installed. */
static int run_bytes(const char *cmd, char **data, size_t *n) {
os_run_result_t r;
memset(&r, 0, sizeof(r));
if (os_run_shell_c(NULL, cmd, CST_ATTACH_MAX_BYTES + 1, &r) != 0) {
os_run_result_free(&r);
return -1;
}
if (r.exit_code != 0 || r.out_len == 0) {
int code = r.exit_code == 127 ? 127 : -1;
os_run_result_free(&r);
return code;
}
*data = r.out;
*n = r.out_len;
return 0;
}
static int type_ok(const char *s, size_t n) {
if (n < 7 || n > 63 || strncmp(s, "image/", 6) != 0)
return 0;
for (size_t i = 0; i < n; i++)
if (!isalnum((unsigned char)s[i]) && !strchr("/.+-", s[i]))
return 0;
return 1;
}
/* Best image MIME type from a newline-separated list: PNG first. */
static int pick_type(const char *list, size_t len, char *out, size_t out_sz) {
out[0] = 0;
size_t i = 0;
while (i < len) {
size_t j = i;
while (j < len && list[j] != '\n' && list[j] != '\r')
j++;
if (type_ok(list + i, j - i)) {
int is_png = j - i == 9 && strncmp(list + i, "image/png", 9) == 0;
if (is_png || !out[0])
snprintf(out, out_sz, "%.*s", (int)(j - i), list + i);
if (is_png)
break;
}
i = j + 1;
}
return out[0] ? 0 : -1;
}
static int via_lister(const char *list_cmd, const char *get_fmt,
const char *tool, char **data, size_t *n, char *err,
size_t err_sz) {
char *types = NULL;
size_t tn = 0;
int rc = run_bytes(list_cmd, &types, &tn);
if (rc == 127) {
char msg[96];
snprintf(msg, sizeof(msg), "reading the clipboard needs %s", tool);
set_err(err, err_sz, msg);
return -1;
}
char type[64];
int ok = rc == 0 && pick_type(types, tn, type, sizeof(type)) == 0;
free(types);
if (!ok) {
set_err(err, err_sz, "no image on the clipboard");
return -1;
}
char cmd[256];
snprintf(cmd, sizeof(cmd), get_fmt, type);
if (run_bytes(cmd, data, n) != 0) {
set_err(err, err_sz, "could not read the clipboard image");
return -1;
}
return 0;
}
#ifdef __APPLE__
static int mac_image(char **data, size_t *n, char *err, size_t err_sz) {
int rc = run_bytes("pngpaste - 2>/dev/null", data, n);
if (rc == 0)
return 0;
char path[1024];
int fd = os_mkstemp(path, sizeof(path), "cst-clip");
if (fd < 0) {
set_err(err, err_sz, "could not create a temp file");
return -1;
}
close(fd);
char cmd[1600];
snprintf(cmd, sizeof(cmd),
"osascript -e 'set f to open for access (POSIX file \"%s\") with "
"write permission' -e 'set eof f to 0' -e 'write (the clipboard as "
"\xc2\xabclass PNGf\xc2\xbb) to f' -e 'close access f' "
">/dev/null 2>&1",
path);
os_run_result_t r;
memset(&r, 0, sizeof(r));
int ran = os_run_shell_c(NULL, cmd, 4096, &r);
int code = r.exit_code;
os_run_result_free(&r);
char *buf = NULL;
size_t len = 0;
if (ran == 0 && code == 0) {
FILE *f = fopen(path, "rb");
if (f) {
char chunk[65536];
size_t got;
while ((got = fread(chunk, 1, sizeof(chunk), f)) > 0 &&
len <= CST_ATTACH_MAX_BYTES) {
char *nb = realloc(buf, len + got);
if (!nb)
break;
buf = nb;
memcpy(buf + len, chunk, got);
len += got;
}
fclose(f);
}
}
remove(path);
if (!buf || !len) {
free(buf);
set_err(err, err_sz, "no image on the clipboard");
return -1;
}
*data = buf;
*n = len;
return 0;
}
#endif
int cst_clipboard_image(char **data, size_t *n, char *err, size_t err_sz) {
*data = NULL;
*n = 0;
#ifdef __APPLE__
return mac_image(data, n, err, err_sz);
#else
const char *wl = getenv("WAYLAND_DISPLAY");
if (wl && *wl)
return via_lister("wl-paste --list-types 2>/dev/null",
"wl-paste --no-newline --type '%s' 2>/dev/null",
"wl-clipboard (wl-paste)", data, n, err, err_sz);
const char *x = getenv("DISPLAY");
if (x && *x)
return via_lister("xclip -selection clipboard -t TARGETS -o 2>/dev/null",
"xclip -selection clipboard -t '%s' -o 2>/dev/null",
"xclip", data, n, err, err_sz);
set_err(err, err_sz, "no clipboard here (no Wayland or X11 display)");
return -1;
#endif
}
#endif
/* --- text clipboard ------------------------------------------------------ */
#ifdef _WIN32
int cst_clipboard_text(char **data, size_t *n, char *err, size_t err_sz) {
*data = NULL;
*n = 0;
if (!IsClipboardFormatAvailable(CF_UNICODETEXT) || !OpenClipboard(NULL)) {
set_err(err, err_sz, "no text on the clipboard");
return -1;
}
HANDLE h = GetClipboardData(CF_UNICODETEXT);
const wchar_t *w = h ? GlobalLock(h) : NULL;
if (!w) {
if (h)
GlobalUnlock(h);
CloseClipboard();
set_err(err, err_sz, "no text on the clipboard");
return -1;
}
char *utf8 = os_wide_to_utf8(w);
size_t len = utf8 ? strlen(utf8) : 0;
GlobalUnlock(h);
CloseClipboard();
if (!utf8 || !len) {
free(utf8);
set_err(err, err_sz, "no text on the clipboard");
return -1;
}
if (len > CST_CLIPBOARD_TEXT_MAX) {
free(utf8);
set_err(err, err_sz, "clipboard text is too large");
return -1;
}
*data = utf8;
*n = len;
return 0;
}
int cst_clipboard_set_text(const char *text, size_t n, char *err,
size_t err_sz) {
if (!text || os_clipboard_set(text, n) != 0) {
set_err(err, err_sz, "could not put text on the clipboard");
return -1;
}
return 0;
}
#else
/* stdout of a clipboard reader, capped and NUL-terminated. */
static int clip_read(const char *cmd, const char *tool, char **data, size_t *n,
char *err, size_t err_sz) {
os_run_result_t r;
memset(&r, 0, sizeof(r));
if (os_run_shell_c(NULL, cmd, CST_CLIPBOARD_TEXT_MAX + 1, &r) != 0) {
os_run_result_free(&r);
set_err(err, err_sz, "could not read the clipboard");
return -1;
}
if (r.exit_code != 0 || r.out_len == 0) {
int missing = r.exit_code == 127;
os_run_result_free(&r);
if (missing) {
char msg[96];
snprintf(msg, sizeof(msg), "reading the clipboard needs %s", tool);
set_err(err, err_sz, msg);
} else {
set_err(err, err_sz, "no text on the clipboard");
}
return -1;
}
if (r.out_len > CST_CLIPBOARD_TEXT_MAX) {
os_run_result_free(&r);
set_err(err, err_sz, "clipboard text is too large");
return -1;
}
char *copy = malloc(r.out_len + 1);
if (!copy) {
os_run_result_free(&r);
set_err(err, err_sz, "out of memory");
return -1;
}
memcpy(copy, r.out, r.out_len);
copy[r.out_len] = 0;
os_run_result_free(&r);
*data = copy;
*n = r.out_len;
return 0;
}
int cst_clipboard_text(char **data, size_t *n, char *err, size_t err_sz) {
*data = NULL;
*n = 0;
#ifdef __APPLE__
return clip_read("pbpaste 2>/dev/null", "pbpaste", data, n, err, err_sz);
#else
const char *wl = getenv("WAYLAND_DISPLAY");
if (wl && *wl)
return clip_read("wl-paste --type text/plain --no-newline 2>/dev/null",
"wl-clipboard (wl-paste)", data, n, err, err_sz);
const char *x = getenv("DISPLAY");
if (x && *x)
return clip_read("xclip -selection clipboard -o 2>/dev/null", "xclip",
data, n, err, err_sz);
set_err(err, err_sz, "no clipboard here (no Wayland or X11 display)");
return -1;
#endif
}
int cst_clipboard_set_text(const char *text, size_t n, char *err,
size_t err_sz) {
if (!text)
return -1;
if (n > CST_CLIPBOARD_TEXT_MAX) {
set_err(err, err_sz, "text is too large for the clipboard");
return -1;
}
const char *fmt = NULL, *tool = NULL;
#ifdef __APPLE__
fmt = "pbcopy < '%s'";
tool = "pbcopy";
#else
if (getenv("WAYLAND_DISPLAY")) {
fmt = "wl-copy < '%s'";
tool = "wl-clipboard (wl-copy)";
} else if (getenv("DISPLAY")) {
fmt = "xclip -selection clipboard -i < '%s'";
tool = "xclip";
}
#endif
if (!fmt) {
set_err(err, err_sz, "no clipboard here (no Wayland or X11 display)");
return -1;
}
char tmp[OS_PATH_MAX];
int fd = os_mkstemp(tmp, sizeof(tmp), "cst-clip");
if (fd < 0) {
set_err(err, err_sz, "could not create a temporary file");
return -1;
}
FILE *fp = fdopen(fd, "wb");
int ok = fp && fwrite(text, 1, n, fp) == n && fclose(fp) == 0;
if (!ok) {
if (fp)
fclose(fp);
remove(tmp);
set_err(err, err_sz, "could not write the text");
return -1;
}
char cmd[OS_PATH_MAX + 64];
snprintf(cmd, sizeof(cmd), fmt, tmp);
os_run_result_t r;
memset(&r, 0, sizeof(r));
int rc = os_run_shell_c(NULL, cmd, 4096, &r);
int code = r.exit_code;
os_run_result_free(&r);
remove(tmp);
if (rc != 0 || code != 0) {
char msg[96];
snprintf(msg, sizeof(msg), "writing the clipboard needs %s", tool);
set_err(err, err_sz, msg);
return -1;
}
return 0;
}
#endif
static int hexval(int c) {
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1;
}
/* file://[host]/path -> /path, percent-decoded; 0 on success. */
static int from_file_uri(char *s) {
const char *p = s + 7;
const char *slash = strchr(p, '/');
if (!slash)
return -1;
if (slash != p && !(slash - p == 9 && strncmp(p, "localhost", 9) == 0))
return -1;
char *o = s;
for (const char *q = slash; *q; q++) {
if (*q == '%' && hexval(q[1]) >= 0 && hexval(q[2]) >= 0) {
*o++ = (char)(hexval(q[1]) * 16 + hexval(q[2]));
q += 2;
} else {
*o++ = *q;
}
}
*o = 0;
#ifdef _WIN32
if (s[0] == '/' && isalpha((unsigned char)s[1]) && s[2] == ':')
memmove(s, s + 1, strlen(s));
#endif
return s[0] ? 0 : -1;
}
static int is_space(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
int cst_paste_paths(const char *text, size_t len,
char out[][CST_PASTE_PATH_MAX], int max) {
int n = 0;
size_t i = 0;
while (i < len) {
while (i < len && is_space(text[i]))
i++;
if (i >= len)
break;
if (n >= max)
return -1;
char *tok = out[n];
size_t k = 0;
char q = 0;
while (i < len) {
char c = text[i];
if (!c)
return -1;
if (q) {
if (c == q) {
q = 0;
i++;
continue;
}
} else {
if (c == '\'' || c == '"') {
q = c;
i++;
continue;
}
if (is_space(c))
break;
#ifndef _WIN32
if (c == '\\' && i + 1 < len)
c = text[++i];
#endif
}
if (k + 1 >= CST_PASTE_PATH_MAX)
return -1;
tok[k++] = c;
i++;
}
if (q || !k)
return -1;
tok[k] = 0;
if (strncmp(tok, "file://", 7) == 0 && from_file_uri(tok) != 0)
return -1;
n++;
}
return n > 0 ? n : -1;
}