The engine can be embedded now instead of only driven from a terminal: the same objects link into a Qt front end (app/) that owns a cst_agent_t inside its own process -- no port, no token, no second process -- and that front end now runs on the desktop and, engine-side, on the phone. Capability metadata (include/cst_tools.h, src/tools/registry.c): - cst_tool_def_t gains perm_name, platforms and available (a probe that returns a reason), so a permission is addressable by name and a tool can say why it cannot run here. - cst_platform_name(), cst_tool_platform_ok(), cst_tool_available(), cst_tool_meta_reload(), cst_tool_perm_name() and CST_PLAT_DESKTOP[]. - cst_tool_register() may take over a built-in name that is not runnable on this platform, so a front end replaces it with its own implementation. - the registry entries carry their class (delete_file -> "delete"), the desktop clipboard tools carry a clipboard probe; find/active/run gate on it. - /api/settings.tools[] exposes perm_name, platforms, available, why and the declared parameters. Engine as a library (Makefile, CMakeLists.txt): - libcstcore.a from the CORE_OBJ set, and a CMake cstcore target shared by the CLI and by app/ (-DCST_BUILD_APP=ON): the engine is compiled once. - Android branch for the vendored sashidown: a per-ABI archive built with the NDK's aarch64-linux-android24-clang wrapper, because CMake's compiler is plain clang and keeps --target/--sysroot in its own variables. Qt front end (app/, new): - AgentCore wraps one cst_agent_t: turns run on a worker thread, every cst_agent_hooks_t callback becomes a Qt signal, and the two hooks that need an answer (on_permission, on_ask_user) block the worker until the GUI replies; mode, approval and plan mode go through the engine's config. - ToolProvider hands the front end's tools to the engine (owner "app:*"); the Linux one is notification_send with a probe for notify-send/D-Bus. - Main.qml: transcript, tool blocks, consent dialog (Deny/Once/Always -> CST_PERM_*), ask_user, mode switch, Esc to cancel; --selftest stays headless and runs the embedding checks without QML. Android (scripts/, docs/android.md, new): - scripts/build_third_party_android.sh builds static mbedTLS + libcurl for an ABI (CA path /system/etc/security/cacerts); build_android.sh builds cst-agent, libcstcore.a and the test binaries for the phone, can push them and run the whole suite there. - sandbox_exec.c keeps the plain exec path on Android: the seccomp supervisor compiled in and the probe reported "seccomp", but writes outside the workspace were not denied on the device -- claiming a sandbox that does not hold is worse than reporting none. - tests/test_diff.c compares layouts only against real GNU diffutils: toybox's diff prints the same hunk header with its own hunk/context rules. Tests: test_tool_meta (metadata, platform gate, probe reasons, takeover of a built-in name) and test_embed (in-process embedding contract) are new; the existing registry/llm_body assertions now follow availability. 38 test binaries pass on the host, 36/36 on arm64-v8a Android 16, cst-app-selftest passes in both the standalone app/ build and the repo build. Version 0.8.7.
697 lines
18 KiB
C
697 lines
18 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);
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
/* PATH lookup without shelling out: cheap enough for a capability probe. */
|
|
static int on_path(const char *name) {
|
|
const char *path = getenv("PATH");
|
|
if (!path)
|
|
return 0;
|
|
for (const char *p = path;;) {
|
|
const char *colon = strchr(p, ':');
|
|
size_t len = colon ? (size_t)(colon - p) : strlen(p);
|
|
char cand[4096];
|
|
if (len + strlen(name) + 2 < sizeof(cand)) {
|
|
if (len == 0)
|
|
snprintf(cand, sizeof(cand), "./%s", name);
|
|
else
|
|
snprintf(cand, sizeof(cand), "%.*s/%s", (int)len, p, name);
|
|
if (access(cand, X_OK) == 0)
|
|
return 1;
|
|
}
|
|
if (!colon)
|
|
break;
|
|
p = colon + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
int cst_clipboard_available(char *why, size_t why_sz) {
|
|
if (why && why_sz)
|
|
why[0] = 0;
|
|
#ifdef _WIN32
|
|
return 0; /* the Win32 clipboard API is always there */
|
|
#elif defined(__APPLE__)
|
|
return 0; /* pbpaste / pbcopy ship with the OS */
|
|
#else
|
|
static const char *const helpers[] = {"wl-paste", "wl-copy", "xclip", NULL};
|
|
for (int i = 0; helpers[i]; i++)
|
|
if (on_path(helpers[i]))
|
|
return 0;
|
|
if (why && why_sz)
|
|
snprintf(why, why_sz,
|
|
"no clipboard helper on PATH (install wl-clipboard or xclip)");
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
}
|