Skip to content

Commit 6021ddc

Browse files
committed
GUACAMOLE-1973: Add support for XTerm bracketed-paste mode
1 parent a5a9100 commit 6021ddc

File tree

4 files changed

+173
-2
lines changed

4 files changed

+173
-2
lines changed

src/terminal/terminal-handlers.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ static bool* __guac_terminal_get_flag(guac_terminal* term, int num, char private
439439
switch (num) {
440440
case 1: return &(term->application_cursor_keys); /* DECCKM */
441441
case 25: return &(term->cursor_visible); /* DECTECM */
442+
case 2004: return &(term->bracketed_paste_mode); /* XTerm bracketed-paste */
442443
}
443444
}
444445

src/terminal/terminal.c

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ void guac_terminal_reset(guac_terminal* term) {
231231
term->application_cursor_keys = false;
232232
term->automatic_carriage_return = false;
233233
term->insert_mode = false;
234+
term->bracketed_paste_mode = false;
234235

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

15751576
}
15761577

1578+
#define IS_UTF8_START_1_BYTE(c) ((c & 0x80) == 0x00)
1579+
#define IS_UTF8_START_2_BYTE(c) ((c & 0xe0) == 0xc0)
1580+
#define IS_UTF8_START_3_BYTE(c) ((c & 0xf0) == 0xe0)
1581+
#define IS_UTF8_START_4_BYTE(c) ((c & 0xf8) == 0xf0)
1582+
#define IS_UTF8_CONTINUATION(c) ((c & 0xc0) == 0x80)
1583+
1584+
int guac_terminal_send_clipboard(guac_terminal *term) {
1585+
1586+
/* Allocate a temporary buffer for filtering the clipboard contents.
1587+
* As we're removing characters, we know it will be at most the size
1588+
* of the original plus the two bracketed paste markers. */
1589+
char *filtered = guac_mem_alloc(term->clipboard->length +
1590+
strlen(GUAC_TERMINAL_BRACKETED_PASTE_START) +
1591+
strlen(GUAC_TERMINAL_BRACKETED_PASTE_STOP));
1592+
uint8_t *src_ptr = (uint8_t *)term->clipboard->buffer;
1593+
uint8_t *src_end = (uint8_t *)(term->clipboard->buffer + term->clipboard->length);
1594+
uint8_t *dst_ptr = (uint8_t *)filtered;
1595+
1596+
/* Keep track of exactly how much data we've sieved */
1597+
int filtered_len = 0;
1598+
1599+
/* Send the paste start sequence */
1600+
if (term->bracketed_paste_mode) {
1601+
size_t seq_len = strlen(GUAC_TERMINAL_BRACKETED_PASTE_START);
1602+
memcpy(dst_ptr, GUAC_TERMINAL_BRACKETED_PASTE_START, seq_len);
1603+
dst_ptr += seq_len;
1604+
filtered_len += seq_len;
1605+
}
1606+
1607+
while (src_ptr < src_end) {
1608+
1609+
/* Allow UTF-8 codepoints.
1610+
* A valid UTF-8 sequence is between one and four bytes in length, and
1611+
* we can confirm the validity by testing the start bits of each byte.
1612+
*
1613+
* A Unicode codepoint is only valid for the smallest UTF-8 sequence that
1614+
* it fits into; larger UTF-8 sequences can only contain larger codepoints.
1615+
* Therefore, some bits in the sequence are required to be used as part of
1616+
* the codepoint number.
1617+
*
1618+
* If the sequence is valid, copy it in full. */
1619+
1620+
/* UTF-8 1-byte codepoint (U+0000 to U+007F)
1621+
* Start bits: 0xxxxxxx */
1622+
if (IS_UTF8_START_1_BYTE(src_ptr[0])) {
1623+
1624+
/* Exclude Unicode CO (U+0000 to U+001F) control characters, except
1625+
* for tab (U+0009), line feed (U+000A) and carriage return (U+000D). */
1626+
if (!((src_ptr[0] >= 0x00) && (src_ptr[0] < 0x20)) ||
1627+
(src_ptr[0] == 0x09) || (src_ptr[0] == 0x0a) || (src_ptr[0] == 0x0d)) {
1628+
dst_ptr[0] = src_ptr[0];
1629+
dst_ptr++;
1630+
filtered_len++;
1631+
src_ptr++;
1632+
continue;
1633+
}
1634+
}
1635+
1636+
/* UTF-8 2-byte codepoint (U+0080 to U+07FF)
1637+
* Start bits: 110xxxxx 10xxxxxx
1638+
* Required: xxxYYYYx xxxxxxxx */
1639+
else if (IS_UTF8_START_2_BYTE(src_ptr[0])) {
1640+
if ((src_ptr + 1 < src_end) &&
1641+
IS_UTF8_CONTINUATION(src_ptr[1]) &&
1642+
((src_ptr[0] & 0x1e) != 0x00)) {
1643+
1644+
/* Exclude Unicode C1 (U+0080 to U+009F) control characters: 11000010 100xxxxx */
1645+
if ((src_ptr[0] != 0xc2) || ((src_ptr[1] & 0xe0) != 0x80)) {
1646+
dst_ptr[0] = src_ptr[0];
1647+
dst_ptr[1] = src_ptr[1];
1648+
dst_ptr += 2;
1649+
filtered_len += 2;
1650+
src_ptr += 2;
1651+
continue;
1652+
}
1653+
}
1654+
}
1655+
1656+
/* UTF-8 3-byte codepoint (U+0800 to U+FFFF)
1657+
* Start bits: 1110xxxx 10xxxxxx 10xxxxxx
1658+
* Required: xxxxYYYY xxYxxxxx xxxxxxxx */
1659+
else if (IS_UTF8_START_3_BYTE(src_ptr[0])) {
1660+
if ((src_ptr + 2 < src_end) &&
1661+
IS_UTF8_CONTINUATION(src_ptr[1]) &&
1662+
IS_UTF8_CONTINUATION(src_ptr[2]) &&
1663+
(((src_ptr[0] & 0x0f) != 0x00) ||
1664+
((src_ptr[1] & 0x20) != 0x00))) {
1665+
dst_ptr[0] = src_ptr[0];
1666+
dst_ptr[1] = src_ptr[1];
1667+
dst_ptr[2] = src_ptr[2];
1668+
dst_ptr += 3;
1669+
filtered_len += 3;
1670+
src_ptr += 3;
1671+
continue;
1672+
}
1673+
}
1674+
1675+
/* UTF-8 4-byte codepoint (U+10000 to U+1FFFF)
1676+
* Start bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
1677+
* Required: xxxxxYYY xxYYxxxx xxxxxxxx xxxxxxxx */
1678+
else if (IS_UTF8_START_4_BYTE(src_ptr[0])) {
1679+
if ((src_ptr + 3 < src_end) &&
1680+
IS_UTF8_CONTINUATION(src_ptr[1]) &&
1681+
IS_UTF8_CONTINUATION(src_ptr[2]) &&
1682+
IS_UTF8_CONTINUATION(src_ptr[3]) &&
1683+
(((src_ptr[0] & 0x07) != 0x00) ||
1684+
((src_ptr[1] & 0x30) != 0x00))) {
1685+
dst_ptr[0] = src_ptr[0];
1686+
dst_ptr[1] = src_ptr[1];
1687+
dst_ptr[2] = src_ptr[2];
1688+
dst_ptr[3] = src_ptr[3];
1689+
dst_ptr += 4;
1690+
filtered_len += 4;
1691+
src_ptr += 4;
1692+
continue;
1693+
}
1694+
}
1695+
1696+
/* If the sequence is invalid, skip to the next byte. */
1697+
src_ptr++;
1698+
}
1699+
1700+
/* Send the paste stop sequence */
1701+
if (term->bracketed_paste_mode) {
1702+
size_t seq_len = strlen(GUAC_TERMINAL_BRACKETED_PASTE_STOP);
1703+
memcpy(dst_ptr, GUAC_TERMINAL_BRACKETED_PASTE_STOP, seq_len);
1704+
dst_ptr += seq_len;
1705+
filtered_len += seq_len;
1706+
}
1707+
1708+
int result = guac_terminal_send_data(term, filtered, filtered_len);
1709+
guac_mem_free(filtered);
1710+
return result;
1711+
}
1712+
15771713
static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) {
15781714

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

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

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

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

19422078
/* If left mouse button was just released, stop selection */
19432079
if (released_mask & GUAC_CLIENT_MOUSE_LEFT)

src/terminal/terminal/terminal-priv.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,11 @@ struct guac_terminal {
422422
*/
423423
bool automatic_carriage_return;
424424

425+
/**
426+
* Whether the current application supports bracketed paste mode.
427+
*/
428+
bool bracketed_paste_mode;
429+
425430
/**
426431
* Whether insert mode is enabled (DECIM).
427432
*/

src/terminal/terminal/terminal.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@
111111
*/
112112
#define GUAC_TERMINAL_PIPE_AUTOFLUSH 2
113113

114+
/*
115+
* Sequence to send to the client at the start of pasted clipboard data
116+
* when in XTerm bracketed paste mode.
117+
*/
118+
#define GUAC_TERMINAL_BRACKETED_PASTE_START "\x1B[200~"
119+
120+
/*
121+
* Sequence to send to the client at the end of pasted clipboard data
122+
* when in XTerm bracketed paste mode.
123+
*/
124+
#define GUAC_TERMINAL_BRACKETED_PASTE_STOP "\x1B[201~"
125+
126+
114127
/**
115128
* Represents a terminal emulator which uses a given Guacamole client to
116129
* render itself.
@@ -542,6 +555,22 @@ int guac_terminal_send_data(guac_terminal* term, const char* data, int length);
542555
*/
543556
int guac_terminal_send_string(guac_terminal* term, const char* data);
544557

558+
/**
559+
* Sends the terminal clipboard contents after sanitisation. If terminal input
560+
* is currently coming from a stream due to a prior call to
561+
* guac_terminal_send_stream(), any input which would normally result from
562+
* invoking this function is dropped.
563+
*
564+
* @param term
565+
* The terminal which should receive the given data on STDIN.
566+
*
567+
* @return
568+
* The number of bytes written to STDIN, or a negative value if an error
569+
* occurs preventing the data from being written. This should always be
570+
* the size of the data given unless data is intentionally dropped.
571+
*/
572+
int guac_terminal_send_clipboard(guac_terminal* term);
573+
545574
/**
546575
* Writes the given buffer to the given terminal's STDOUT. All requested bytes
547576
* will be written unless an error occurs. This function may block until space

0 commit comments

Comments
 (0)