Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions src/terminal/terminal.c
Original file line number Diff line number Diff line change
Expand Up @@ -1555,6 +1555,79 @@ int guac_terminal_send_data(guac_terminal* term, const char* data, int length) {

}

/**
* Sends clipboard contents as keyboard input, converting LF and CRLF to
* CR (and passing bare CR through as CR). Clipboard data is normalized
* on receipt so CRLF pairs are stored as LF, but bare CR can still be
* present. When sending the data as keyboard input, it must be CR, since
* CR represents the Enter key. LF is just a text newline and isn't
* reliably treated as Enter (e.g. on Windows). This also avoids CRLF
* being interpreted as two Enters (CRCR).
*
* This has to be done server-side. When the user pastes via right-click
* or middle-click, the browser only sends a mouse event; the server then
* pastes directly from its clipboard buffer. The client doesn't get a
* chance to intercept or normalize the content at that point.
*
* @param term
* The terminal whose clipboard contents should be sent.
*
* @return
* The number of bytes written to STDIN (zero if the clipboard was empty),
* or a negative value (if an error occurred preventing the
* data from being written).
*/
static int guac_terminal_send_clipboard(guac_terminal* term) {

int length = term->clipboard->length;

if (length <= 0)
return 0;

char* buf = guac_mem_alloc(length);

if (buf == NULL)
return -1;

int written = guac_terminal_paste_normalize_newlines(
term->clipboard->buffer, length, buf);

int ret = guac_terminal_send_data(term, buf, written);
guac_mem_free(buf);
return ret;

}

int guac_terminal_paste_normalize_newlines(const char* src, int length,
char* dest) {

const char* end = src + length;
char* out = dest;

while (src < end) {

if (*src == '\r') {
*(out++) = '\r';
src++;

/* CRLF collapses to a single CR */
if (src < end && *src == '\n')
src++;
}
else if (*src == '\n') {
*(out++) = '\r';
src++;
}
else {
*(out++) = *(src++);
}

}

return out - dest;

}

int guac_terminal_send_string(guac_terminal* term, const char* data) {

/* Block all other sources of input if input is coming from a stream */
Expand Down Expand Up @@ -1827,7 +1900,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);

/* If Shift+Tab (Backtab), send the appropriate escape sequence */
if (term->mod_shift && keysym == 0xFF09) {
Expand Down Expand Up @@ -2194,7 +2267,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
22 changes: 22 additions & 0 deletions src/terminal/terminal/terminal.h
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,28 @@ int guac_terminal_send_mouse(guac_terminal* term, guac_user* user,
*/
int guac_terminal_send_data(guac_terminal* term, const char* data, int length);

/**
* Rewrites clipboard bytes so each line terminator becomes a single CR. LF is
* replaced with CR, CRLF collapses to one CR, and bare CR passes through. All
* other bytes are copied verbatim. The output is never longer than the input,
* so the destination buffer only needs to be as large as the input.
*
* @param src
* The bytes to rewrite.
*
* @param length
* The number of bytes available at src.
*
* @param dest
* The destination buffer. Must be at least length bytes large. May not
* overlap src.
*
* @return
* The number of bytes written to dest.
*/
int guac_terminal_paste_normalize_newlines(const char* src, int length,
char* dest);

/**
* Sends the given string as if typed by the user. If terminal input is
* currently coming from a stream due to a prior call to
Expand Down
1 change: 1 addition & 0 deletions src/terminal/tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ check_PROGRAMS = test_terminal
TESTS = $(check_PROGRAMS)

test_terminal_SOURCES = \
paste/normalize-newlines.c \
selection-point/enclose-text.c \
selection-point/point-after.c \
selection-point/rounding.c
Expand Down
114 changes: 114 additions & 0 deletions src/terminal/tests/paste/normalize-newlines.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

#include "terminal/terminal.h"

#include <CUnit/CUnit.h>
#include <string.h>

/**
* Runs guac_terminal_paste_normalize_newlines() against the given input and
* verifies both the returned length and the resulting bytes match expected.
*/
static void assert_normalized(const char* in, int in_len,
const char* expected, int expected_len) {

char out[64];
int written = guac_terminal_paste_normalize_newlines(in, in_len, out);

CU_ASSERT_EQUAL(expected_len, written);
CU_ASSERT_EQUAL(0, memcmp(out, expected, expected_len));

}

/**
* Verifies that bare LF is rewritten as CR.
*/
void test_paste__lf_to_cr() {
assert_normalized("a\nb", 3, "a\rb", 3);
}

/**
* Verifies that CRLF pairs collapse to a single CR (not CRCR).
*/
void test_paste__crlf_to_single_cr() {
assert_normalized("a\r\nb", 4, "a\rb", 3);
}

/**
* Verifies that bare CR passes through as CR.
*/
void test_paste__bare_cr_passthrough() {
assert_normalized("a\rb", 3, "a\rb", 3);
}

/**
* Verifies that a mixed sequence of LF, CRLF, and bare CR each become exactly
* one CR.
*/
void test_paste__mixed_endings() {
assert_normalized("a\nb\r\nc\rd", 8, "a\rb\rc\rd", 7);
}

/**
* Verifies that consecutive line endings each produce one CR. Catches
* cursor-advancement errors that would let one CRLF's LF be consumed by the
* next iteration.
*/
void test_paste__consecutive_endings() {
assert_normalized("\n\n", 2, "\r\r", 2);
assert_normalized("\r\n\r\n", 4, "\r\r", 2);
assert_normalized("\r\r", 2, "\r\r", 2);
assert_normalized("\n\r\n\r", 4, "\r\r\r", 3);
}

/**
* Verifies that a trailing bare CR at end of input is emitted without reading
* past the buffer.
*/
void test_paste__trailing_cr() {
assert_normalized("abc\r", 4, "abc\r", 4);
}

/**
* Verifies that an input with no line endings is copied verbatim.
*/
void test_paste__no_endings() {
assert_normalized("hello world", 11, "hello world", 11);
}

/**
* Verifies that zero-length input writes nothing.
*/
void test_paste__empty() {
char out[1] = { 0x7F };
int written = guac_terminal_paste_normalize_newlines("", 0, out);
CU_ASSERT_EQUAL(0, written);
CU_ASSERT_EQUAL((char) 0x7F, out[0]);
}

/**
* Verifies that embedded NUL bytes are preserved (the function is byte-driven,
* not C-string driven).
*/
void test_paste__embedded_nul() {
const char in[] = { 'a', '\0', '\n', 'b' };
const char expected[] = { 'a', '\0', '\r', 'b' };
assert_normalized(in, sizeof(in), expected, sizeof(expected));
}
Loading