#include "cst_clipboard.h" #include "cst_attach.h" #include "os_compat.h" #include #include #include #include #ifdef _WIN32 #include #else #include #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; }