From fe4af8ef7811ec24edb18842155a99b7dc20a68f Mon Sep 17 00:00:00 2001 From: Richard Lavigne Date: Fri, 13 Jun 2025 10:00:13 -0400 Subject: [PATCH 1/2] Add universal MIME type support and --query feature - Add --type MIMETYPE option for any MIME type (e.g. image/png) - Add --html shortcut for --type text/html - Add --query option to list available clipboard targets - Implement text/binary MIME type whitelist with fallback logic - Binary MIME types (image/png) only register specific atom - Text MIME types (text/html, application/json) register both specific + text fallbacks - All existing functionality preserved for backwards compatibility --- xsel.c | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 163 insertions(+), 22 deletions(-) diff --git a/xsel.c b/xsel.c index 0dd9bd4..d4e4097 100644 --- a/xsel.c +++ b/xsel.c @@ -61,13 +61,14 @@ static Atom null_atom; /* The NULL atom */ static Atom text_atom; /* The TEXT atom */ static Atom utf8_atom; /* The UTF8 atom */ static Atom compound_text_atom; /* The COMPOUND_TEXT atom */ +static Atom mime_atom; /* The MIME type atom */ /* Number of selection targets served by this. - * (MULTIPLE, INCR, TARGETS, TIMESTAMP, DELETE, TEXT, UTF8_STRING and STRING) + * (MULTIPLE, INCR, TARGETS, TIMESTAMP, DELETE, TEXT, UTF8_STRING, STRING and MIME type) * NB. We do not currently serve COMPOUND_TEXT; we can retrieve it but do not * perform charset conversion. */ -#define MAX_NUM_TARGETS 9 +#define MAX_NUM_TARGETS 10 static int NUM_TARGETS; static Atom supported_targets[MAX_NUM_TARGETS]; @@ -81,6 +82,38 @@ static Bool do_follow = False; /* nodaemon: Disable daemon mode if True. */ static Bool no_daemon = False; +/* mime_type: MIME type string for selection target, NULL for default text */ +static char * mime_type = NULL; + +/* + * is_text_mime_type (mime_type) + * + * Check if a MIME type should register text fallbacks (STRING, UTF8_STRING) + * in addition to the specific MIME type. + */ +static Bool +is_text_mime_type (const char * mime_type) +{ + if (!mime_type) return False; + + /* Text-like MIME types that should provide text fallbacks */ + if (strncmp(mime_type, "text/", 5) == 0) return True; + if (strncmp(mime_type, "application/json", 16) == 0) return True; + if (strncmp(mime_type, "application/xml", 15) == 0) return True; + if (strncmp(mime_type, "application/xhtml", 17) == 0) return True; + if (strncmp(mime_type, "application/javascript", 22) == 0) return True; + if (strncmp(mime_type, "application/x-", 14) == 0) { + /* Many application/x-* types are text-based */ + const char * subtype = mime_type + 14; + if (strncmp(subtype, "sh", 2) == 0) return True; + if (strncmp(subtype, "perl", 4) == 0) return True; + if (strncmp(subtype, "python", 6) == 0) return True; + if (strncmp(subtype, "ruby", 4) == 0) return True; + } + + return False; +} + /* logfile: name of file to log error messages to when detached */ static char logfile[MAXFNAME]; @@ -123,7 +156,8 @@ usage (void) printf (" -z, --zeroflush Overwrites selection when zero ('\\0') is received\n"); printf (" -i, --input Read standard input into the selection\n\n"); printf ("Output options\n"); - printf (" -o, --output Write the selection to standard output\n\n"); + printf (" -o, --output Write the selection to standard output\n"); + printf (" --query List available target types\n\n"); printf ("Action options\n"); printf (" -c, --clear Clear the selection\n"); printf (" -d, --delete Request that the selection be cleared and that\n"); @@ -146,6 +180,8 @@ usage (void) printf (" specifies no timeout (default)\n\n"); printf ("Miscellaneous options\n"); printf (" --trim Remove newline ('\\n') char from end of input / output\n"); + printf (" --type MIMETYPE Specify MIME type for selection (e.g. image/png)\n"); + printf (" --html Shortcut for --type text/html\n"); printf (" -l, --logfile Specify file to log errors to when detached.\n"); printf (" -n, --nodetach Do not detach from the controlling terminal. Without\n"); printf (" this option, xsel will fork to become a background\n"); @@ -252,6 +288,7 @@ get_atom_name (Atom atom) if (atom == null_atom) return "NULL"; if (atom == text_atom) return "TEXT"; if (atom == utf8_atom) return "UTF8_STRING"; + if (atom == mime_atom) return mime_type; ret = XGetAtomName (display, atom); strncpy (atom_name, ret, MAXLINE+1); @@ -1457,6 +1494,22 @@ handle_utf8_string (Display * display, Window requestor, Atom property, selection, time, mparent); } +/* + * handle_mime_string (display, requestor, property, sel) + * + * Handle a MIME type request; setting 'sel' as the data + */ +static HandleResult +handle_mime_string (Display * display, Window requestor, Atom property, + unsigned char * sel, Atom selection, Time time, + MultTrack * mparent) +{ + return + change_property (display, requestor, property, mime_atom, 8, + PropModeReplace, sel, xs_strlen(sel), + selection, time, mparent); +} + /* * handle_delete (display, requestor, property) * @@ -1500,14 +1553,22 @@ process_multiple (MultTrack * mt, Bool do_parent) } else if (mt->atoms[i] == multiple_atom) { retval |= handle_multiple (mt->display, mt->requestor, mt->atoms[i+1], mt->sel, mt->selection, mt->time, mt); + } else if (mt->atoms[i] == mime_atom) { + retval |= handle_mime_string (mt->display, mt->requestor, mt->atoms[i+1], + mt->sel, mt->selection, mt->time, mt); + } else if (mt->atoms[i] == delete_atom) { + retval |= handle_delete (mt->display, mt->requestor, mt->atoms[i+1]); + } else if (mime_type && !is_text_mime_type(mime_type)) { + /* ================================================================ + * If non-text MIME type set, everything after this point is ignored + * ================================================================ */ + mt->atoms[i] = None; } else if (mt->atoms[i] == XA_STRING || mt->atoms[i] == text_atom) { retval |= handle_string (mt->display, mt->requestor, mt->atoms[i+1], mt->sel, mt->selection, mt->time, mt); } else if (mt->atoms[i] == utf8_atom) { retval |= handle_utf8_string (mt->display, mt->requestor, mt->atoms[i+1], mt->sel, mt->selection, mt->time, mt); - } else if (mt->atoms[i] == delete_atom) { - retval |= handle_delete (mt->display, mt->requestor, mt->atoms[i+1]); } else if (mt->atoms[i] == None) { /* the only other thing we know to handle is None, for which we * do nothing. This block is, like, __so__ redundant. Welcome to @@ -1676,6 +1737,21 @@ handle_selection_request (XEvent event, unsigned char * sel) hr = handle_multiple (ev.display, ev.requestor, ev.property, sel, ev.selection, ev.time, NULL); } + } else if (ev.target == mime_atom) { + /* Received MIME type request */ + ev.property = xsr->property; + hr = handle_mime_string (ev.display, ev.requestor, ev.property, sel, + ev.selection, ev.time, NULL); + } else if (ev.target == delete_atom) { + /* Received DELETE request */ + ev.property = xsr->property; + hr = handle_delete (ev.display, ev.requestor, ev.property); + retval = False; + } else if (mime_type && !is_text_mime_type(mime_type)) { + /* ================================================================ + * If non-text MIME type set, everything after this point is ignored + * ================================================================ */ + ev.property = None; } else if (ev.target == XA_STRING || ev.target == text_atom) { /* Received STRING or TEXT request */ ev.property = xsr->property; @@ -1686,11 +1762,6 @@ handle_selection_request (XEvent event, unsigned char * sel) ev.property = xsr->property; hr = handle_utf8_string (ev.display, ev.requestor, ev.property, sel, ev.selection, ev.time, NULL); - } else if (ev.target == delete_atom) { - /* Received DELETE request */ - ev.property = xsr->property; - hr = handle_delete (ev.display, ev.requestor, ev.property); - retval = False; } else { /* Cannot convert to requested target. This includes most non-string * datatypes, and INSERT_SELECTION, INSERT_PROPERTY */ @@ -2033,6 +2104,7 @@ main(int argc, char *argv[]) Bool force_input = False, force_output = False; Bool want_clipboard = False, do_delete = False; Bool trim_trailing_newline = False; + Bool do_query = False; Window root; Atom selection = XA_PRIMARY, test_atom; XClassHint * class_hints; @@ -2113,6 +2185,13 @@ main(int argc, char *argv[]) want_clipboard = True; } else if (OPT("--trim")) { trim_trailing_newline = True; + } else if (OPT("--type")) { + i++; if (i >= argc) goto usage_err; + mime_type = argv[i]; + } else if (OPT("--html")) { + mime_type = "text/html"; + } else if (OPT("--query")) { + do_query = True; } else if (OPT("--keep") || OPT("-k")) { do_keep = True; } else if (OPT("--exchange") || OPT("-x")) { @@ -2252,22 +2331,31 @@ main(int argc, char *argv[]) supported_targets[s++] = incr_atom; NUM_TARGETS++; - /* Get the TEXT atom */ - text_atom = XInternAtom (display, "TEXT", False); - supported_targets[s++] = text_atom; - NUM_TARGETS++; + if (!mime_type || is_text_mime_type(mime_type)) { + /* Get the TEXT atom */ + text_atom = XInternAtom (display, "TEXT", False); + supported_targets[s++] = text_atom; + NUM_TARGETS++; + + /* Get the UTF8_STRING atom */ + utf8_atom = XInternAtom (display, "UTF8_STRING", True); + if(utf8_atom != None) { + supported_targets[s++] = utf8_atom; + NUM_TARGETS++; + } else { + utf8_atom = XA_STRING; + } - /* Get the UTF8_STRING atom */ - utf8_atom = XInternAtom (display, "UTF8_STRING", True); - if(utf8_atom != None) { - supported_targets[s++] = utf8_atom; + supported_targets[s++] = XA_STRING; NUM_TARGETS++; - } else { - utf8_atom = XA_STRING; } - supported_targets[s++] = XA_STRING; - NUM_TARGETS++; + /* Get the MIME type atom if specified */ + if (mime_type) { + mime_atom = XInternAtom (display, mime_type, False); + supported_targets[s++] = mime_atom; + NUM_TARGETS++; + } if (NUM_TARGETS > MAX_NUM_TARGETS) { exit_err ("internal error num-targets (%d) > max-num-targets (%d)\n", @@ -2300,6 +2388,59 @@ main(int argc, char *argv[]) _exit (0); } + /* handle query and exit if so */ + if (do_query) { + Atom prop; + XEvent event; + Atom target; + int format; + unsigned long length, bytesafter; + unsigned char * value; + Atom * atoms; + unsigned long i; + + /* Find the selection to query */ + if (want_clipboard) { + selection = XInternAtom (display, "CLIPBOARD", False); + } + + /* Request TARGETS from selection owner */ + prop = XInternAtom (display, "XSEL_QUERY", False); + XConvertSelection (display, selection, targets_atom, prop, window, timestamp); + XSync (display, False); + + /* Wait for the response */ + while (1) { + XNextEvent (display, &event); + if (event.type == SelectionNotify) { + if (event.xselection.property == None) { + printf ("No targets available\n"); + fflush (stdout); + exit (0); + } + + /* Get the TARGETS property */ + XGetWindowProperty (event.xselection.display, event.xselection.requestor, + event.xselection.property, 0L, 1000000, True, + XA_ATOM, &target, &format, &length, &bytesafter, &value); + + if (target == XA_ATOM && format == 32 && length > 0) { + atoms = (Atom *) value; + for (i = 0; i < length; i++) { + printf ("%s\n", get_atom_name (atoms[i])); + } + } else { + printf ("Invalid TARGETS response\n"); + } + + if (value) XFree (value); + break; + } + } + fflush (stdout); + exit (0); + } + /* Find the "CLIPBOARD" selection if required */ if (want_clipboard) { selection = XInternAtom (display, "CLIPBOARD", False); From b8d5f796763fce9f36bf95f5a9f189225ddfbc4f Mon Sep 17 00:00:00 2001 From: Richard Lavigne Date: Fri, 13 Jun 2025 10:24:26 -0400 Subject: [PATCH 2/2] Fix binary data length handling for MIME types - Add sel_length global variable to track actual binary data size - Update handle_mime_string() to use sel_length for binary data - Use xs_strlen() for text data, actual byte count for binary data - Fixes issue where xs_strlen() truncated binary data at null bytes - GIMP can now successfully paste PNG images from xsel --- xsel.c | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/xsel.c b/xsel.c index d4e4097..eb17cfa 100644 --- a/xsel.c +++ b/xsel.c @@ -85,6 +85,9 @@ static Bool no_daemon = False; /* mime_type: MIME type string for selection target, NULL for default text */ static char * mime_type = NULL; +/* sel_length: Actual length of selection data for binary MIME types */ +static size_t sel_length = 0; + /* * is_text_mime_type (mime_type) * @@ -95,7 +98,7 @@ static Bool is_text_mime_type (const char * mime_type) { if (!mime_type) return False; - + /* Text-like MIME types that should provide text fallbacks */ if (strncmp(mime_type, "text/", 5) == 0) return True; if (strncmp(mime_type, "application/json", 16) == 0) return True; @@ -110,7 +113,7 @@ is_text_mime_type (const char * mime_type) if (strncmp(subtype, "python", 6) == 0) return True; if (strncmp(subtype, "ruby", 4) == 0) return True; } - + return False; } @@ -987,6 +990,9 @@ read_input (unsigned char * read_buffer, Bool do_select) print_debug (D_TRACE, "Accumulated %d bytes input", total_input); + /* Set global length for binary data handling */ + sel_length = total_input; + return read_buffer; } @@ -1504,9 +1510,18 @@ handle_mime_string (Display * display, Window requestor, Atom property, unsigned char * sel, Atom selection, Time time, MultTrack * mparent) { + size_t data_length; + + /* Use actual byte length for binary data, strlen for text data */ + if (mime_type && !is_text_mime_type(mime_type) && sel_length > 0) { + data_length = sel_length; + } else { + data_length = xs_strlen(sel); + } + return change_property (display, requestor, property, mime_atom, 8, - PropModeReplace, sel, xs_strlen(sel), + PropModeReplace, sel, data_length, selection, time, mparent); } @@ -1560,7 +1575,7 @@ process_multiple (MultTrack * mt, Bool do_parent) retval |= handle_delete (mt->display, mt->requestor, mt->atoms[i+1]); } else if (mime_type && !is_text_mime_type(mime_type)) { /* ================================================================ - * If non-text MIME type set, everything after this point is ignored + * If non-text MIME type set, everything after this point is ignored * ================================================================ */ mt->atoms[i] = None; } else if (mt->atoms[i] == XA_STRING || mt->atoms[i] == text_atom) { @@ -2398,17 +2413,17 @@ main(int argc, char *argv[]) unsigned char * value; Atom * atoms; unsigned long i; - + /* Find the selection to query */ if (want_clipboard) { selection = XInternAtom (display, "CLIPBOARD", False); } - + /* Request TARGETS from selection owner */ prop = XInternAtom (display, "XSEL_QUERY", False); XConvertSelection (display, selection, targets_atom, prop, window, timestamp); XSync (display, False); - + /* Wait for the response */ while (1) { XNextEvent (display, &event); @@ -2418,12 +2433,12 @@ main(int argc, char *argv[]) fflush (stdout); exit (0); } - + /* Get the TARGETS property */ XGetWindowProperty (event.xselection.display, event.xselection.requestor, event.xselection.property, 0L, 1000000, True, XA_ATOM, &target, &format, &length, &bytesafter, &value); - + if (target == XA_ATOM && format == 32 && length > 0) { atoms = (Atom *) value; for (i = 0; i < length; i++) { @@ -2432,7 +2447,7 @@ main(int argc, char *argv[]) } else { printf ("Invalid TARGETS response\n"); } - + if (value) XFree (value); break; }