Skip to content

Commit

Permalink
GUACAMOLE-1973: Add support for XTerm bracketed-paste mode
Browse files Browse the repository at this point in the history
  • Loading branch information
scottp-dpaw committed Jul 31, 2024
1 parent a5a9100 commit 25206c9
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/terminal/terminal-handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ static bool* __guac_terminal_get_flag(guac_terminal* term, int num, char private
switch (num) {
case 1: return &(term->application_cursor_keys); /* DECCKM */
case 25: return &(term->cursor_visible); /* DECTECM */
case 2004: return &(term->bracketed_paste_mode); /* XTerm bracketed-paste */
}
}

Expand Down
140 changes: 138 additions & 2 deletions src/terminal/terminal.c
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ void guac_terminal_reset(guac_terminal* term) {
term->application_cursor_keys = false;
term->automatic_carriage_return = false;
term->insert_mode = false;
term->bracketed_paste_mode = false;

/* Reset tabs */
term->tab_interval = 8;
Expand Down Expand Up @@ -1574,6 +1575,141 @@ int guac_terminal_send_string(guac_terminal* term, const char* data) {

}

#define IS_UTF8_START_1_BYTE(c) ((c & 0x80) == 0x00)
#define IS_UTF8_START_2_BYTE(c) ((c & 0xe0) == 0xc0)
#define IS_UTF8_START_3_BYTE(c) ((c & 0xf0) == 0xe0)
#define IS_UTF8_START_4_BYTE(c) ((c & 0xf8) == 0xf0)
#define IS_UTF8_CONTINUATION(c) ((c & 0xc0) == 0x80)

int guac_terminal_send_clipboard(guac_terminal *term) {

/* Allocate a temporary buffer for filtering the clipboard contents.
* As we're removing characters, we know it will be at most the size
* of the original plus the two bracketed paste markers. */
char *filtered = guac_mem_alloc(term->clipboard->length +
strlen(GUAC_TERMINAL_BRACKETED_PASTE_START) +
strlen(GUAC_TERMINAL_BRACKETED_PASTE_STOP));
uint8_t *src_ptr = (uint8_t *)term->clipboard->buffer;
uint8_t *src_end = (uint8_t *)(term->clipboard->buffer + term->clipboard->length);
uint8_t *dst_ptr = (uint8_t *)filtered;

/* Keep track of exactly how much data we've sieved */
int filtered_len = 0;

/* Send the paste start sequence */
if (term->bracketed_paste_mode) {
size_t seq_len = strlen(GUAC_TERMINAL_BRACKETED_PASTE_START);
memcpy(dst_ptr, GUAC_TERMINAL_BRACKETED_PASTE_START, seq_len);
dst_ptr += seq_len;
filtered_len += seq_len;
}

while (src_ptr < src_end) {

/* Allow UTF-8 codepoints.
* A valid UTF-8 sequence is between one and four bytes in length, and
* we can confirm the validity by testing the start bits of each byte.
*
* A Unicode codepoint is only valid for the smallest UTF-8 sequence that
* it fits into; larger UTF-8 sequences can only contain larger codepoints.
* Therefore, some bits in the sequence are required to be used as part of
* the codepoint number.
*
* If the sequence is valid, copy it in full. */

/* UTF-8 1-byte codepoint (U+0000 to U+007F)
* Start bits: 0xxxxxxx */
if (IS_UTF8_START_1_BYTE(src_ptr[0])) {

/* Exclude Unicode CO (U+0000 to U+001F) control characters, except
* for tab (U+0009), line feed (U+000A) and carriage return (U+000D). */
if (!((src_ptr[0] >= 0x00) && (src_ptr[0] < 0x20)) ||
(src_ptr[0] == 0x09) || (src_ptr[0] == 0x0a) || (src_ptr[0] == 0x0d)) {
dst_ptr[0] = src_ptr[0];
dst_ptr++;
filtered_len++;
src_ptr++;
continue;
}
}

/* UTF-8 2-byte codepoint (U+0080 to U+07FF)
* Start bits: 110xxxxx 10xxxxxx
* Required: xxxYYYYx xxxxxxxx */
else if (IS_UTF8_START_2_BYTE(src_ptr[0])) {
if ((src_ptr + 1 < src_end) &&
IS_UTF8_CONTINUATION(src_ptr[1]) &&
((src_ptr[0] & 0x1e) != 0x00)) {

/* Exclude Unicode C1 (U+0080 to U+009F) control characters: 11000010 100xxxxx */
if ((src_ptr[0] != 0xc2) || ((src_ptr[1] & 0xe0) != 0x80)) {
dst_ptr[0] = src_ptr[0];
dst_ptr[1] = src_ptr[1];
dst_ptr += 2;
filtered_len += 2;
src_ptr += 2;
continue;
}
}
}

/* UTF-8 3-byte codepoint (U+0800 to U+FFFF)
* Start bits: 1110xxxx 10xxxxxx 10xxxxxx
* Required: xxxxYYYY xxYxxxxx xxxxxxxx */
else if (IS_UTF8_START_3_BYTE(src_ptr[0])) {
if ((src_ptr + 2 < src_end) &&
IS_UTF8_CONTINUATION(src_ptr[1]) &&
IS_UTF8_CONTINUATION(src_ptr[2]) &&
(((src_ptr[0] & 0x0f) != 0x00) ||
((src_ptr[1] & 0x20) != 0x00))) {
dst_ptr[0] = src_ptr[0];
dst_ptr[1] = src_ptr[1];
dst_ptr[2] = src_ptr[2];
dst_ptr += 3;
filtered_len += 3;
src_ptr += 3;
continue;
}
}

/* UTF-8 4-byte codepoint (U+010000 to U+10FFFF)
* Start bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
* Required: xxxxxYYY xxYYxxxx xxxxxxxx xxxxxxxx */
else if (IS_UTF8_START_4_BYTE(src_ptr[0])) {
if ((src_ptr + 3 < src_end) &&
IS_UTF8_CONTINUATION(src_ptr[1]) &&
IS_UTF8_CONTINUATION(src_ptr[2]) &&
IS_UTF8_CONTINUATION(src_ptr[3]) &&
(((src_ptr[0] & 0x07) != 0x00) ||
((src_ptr[1] & 0x30) != 0x00))) {
dst_ptr[0] = src_ptr[0];
dst_ptr[1] = src_ptr[1];
dst_ptr[2] = src_ptr[2];
dst_ptr[3] = src_ptr[3];
dst_ptr += 4;
filtered_len += 4;
src_ptr += 4;
continue;
}
}

/* If the sequence is invalid, skip to the next byte. */
src_ptr++;
}

/* Send the paste stop sequence */
if (term->bracketed_paste_mode) {
size_t seq_len = strlen(GUAC_TERMINAL_BRACKETED_PASTE_STOP);
memcpy(dst_ptr, GUAC_TERMINAL_BRACKETED_PASTE_STOP, seq_len);
dst_ptr += seq_len;
filtered_len += seq_len;
}

int result = guac_terminal_send_data(term, filtered, filtered_len);
guac_mem_free(filtered);
return result;
}

static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) {

/* Ignore user input if terminal is not started */
Expand Down Expand Up @@ -1605,7 +1741,7 @@ static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed

/* Ctrl+Shift+V or Cmd+v (mac style) shortcuts for paste */
if ((keysym == 'V' && term->mod_ctrl) || (keysym == 'v' && term->mod_meta))
return guac_terminal_send_data(term, term->clipboard->buffer, term->clipboard->length);
return guac_terminal_send_clipboard(term);

/*
* Ctrl+Shift+C and Cmd+c shortcuts for copying are not handled, as
Expand Down Expand Up @@ -1937,7 +2073,7 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user,

/* Paste contents of clipboard on right or middle mouse button up */
if ((released_mask & GUAC_CLIENT_MOUSE_RIGHT) || (released_mask & GUAC_CLIENT_MOUSE_MIDDLE))
return guac_terminal_send_data(term, term->clipboard->buffer, term->clipboard->length);
return guac_terminal_send_clipboard(term);

/* If left mouse button was just released, stop selection */
if (released_mask & GUAC_CLIENT_MOUSE_LEFT)
Expand Down
5 changes: 5 additions & 0 deletions src/terminal/terminal/terminal-priv.h
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,11 @@ struct guac_terminal {
*/
bool automatic_carriage_return;

/**
* Whether the current application supports bracketed paste mode.
*/
bool bracketed_paste_mode;

/**
* Whether insert mode is enabled (DECIM).
*/
Expand Down
29 changes: 29 additions & 0 deletions src/terminal/terminal/terminal.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@
*/
#define GUAC_TERMINAL_PIPE_AUTOFLUSH 2

/*
* Sequence to send to the client at the start of pasted clipboard data
* when in XTerm bracketed paste mode.
*/
#define GUAC_TERMINAL_BRACKETED_PASTE_START "\x1B[200~"

/*
* Sequence to send to the client at the end of pasted clipboard data
* when in XTerm bracketed paste mode.
*/
#define GUAC_TERMINAL_BRACKETED_PASTE_STOP "\x1B[201~"


/**
* Represents a terminal emulator which uses a given Guacamole client to
* render itself.
Expand Down Expand Up @@ -542,6 +555,22 @@ int guac_terminal_send_data(guac_terminal* term, const char* data, int length);
*/
int guac_terminal_send_string(guac_terminal* term, const char* data);

/**
* Sends the terminal clipboard contents after sanitisation. If terminal input
* is currently coming from a stream due to a prior call to
* guac_terminal_send_stream(), any input which would normally result from
* invoking this function is dropped.
*
* @param term
* The terminal which should receive the given data on STDIN.
*
* @return
* The number of bytes written to STDIN, or a negative value if an error
* occurs preventing the data from being written. This should always be
* the size of the data given unless data is intentionally dropped.
*/
int guac_terminal_send_clipboard(guac_terminal* term);

/**
* Writes the given buffer to the given terminal's STDOUT. All requested bytes
* will be written unless an error occurs. This function may block until space
Expand Down

0 comments on commit 25206c9

Please sign in to comment.