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
7 changes: 5 additions & 2 deletions include/neatvnc.h
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ void nvnc_set_normalised_pointer_fn(struct nvnc* self,
void nvnc_set_new_client_fn(struct nvnc* self, nvnc_client_fn);

/**
* Set a handler for clipboard text received from clients.
* Set a handler for clipboard text received from clients. The text is always
* delivered to the handler as UTF-8; Latin-1 cut text is converted.
*/
void nvnc_set_cut_text_fn(struct nvnc*, nvnc_cut_text_fn fn);

Expand Down Expand Up @@ -781,7 +782,9 @@ struct nvnc_display* nvnc_desktop_layout_get_display(
const struct nvnc_desktop_layout*, uint8_t display_index);

/**
* Broadcast clipboard text to all connected clients.
* Broadcast clipboard text to all connected clients. The text must be UTF-8;
* it is sent as-is to extended-clipboard clients and converted to Latin-1 for
* other clients, where code points that do not fit are replaced with '?'.
*/
void nvnc_send_cut_text(struct nvnc*, const char* text, uint32_t len);

Expand Down
30 changes: 30 additions & 0 deletions include/utf8.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2026 Norbert Schultz
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/

#pragma once

#include <unistd.h>

#define LATIN1_TO_UTF8_MAX_SIZE(x) ((size_t)(x) * 2 + 1)
#define UTF8_TO_LATIN1_MAX_SIZE(x) ((size_t)(x) + 1)

/* NUL-terminates dst; returns the length excluding the terminator. */
size_t latin1_to_utf8(char* dst, const char* src, size_t src_len);

/* Code points above 0xff are replaced with '?'. NUL-terminates dst; returns
* the length excluding the terminator.
*/
size_t utf8_to_latin1(char* dst, const char* src, size_t src_len);
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ sources_dep = declare_dependency(
'src/parallel-deflate.c',
'src/compositor.c',
'src/region.c',
'src/utf8.c',
]
)

Expand Down
114 changes: 87 additions & 27 deletions src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "transform-util.h"
#include "type-macros.h"
#include "server.h"
#include "utf8.h"

#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -1493,6 +1494,37 @@ static char* crlf_to_lf(const char* src, size_t len)
return lf_buf;
}

static void notify_cut_text_utf8(struct nvnc_client* client, const char* text,
uint32_t len)
{
struct nvnc* server = client->server;

if (!server->cut_text_fn)
return;

server->cut_text_fn(client, text, len);
}

static void notify_cut_text_latin1(struct nvnc_client* client, const char* text,
uint32_t len)
{
struct nvnc* server = client->server;

if (!server->cut_text_fn)
return;

char* utf8 = malloc(LATIN1_TO_UTF8_MAX_SIZE(len));
if (!utf8) {
nvnc_log(NVNC_LOG_ERROR, "OOM: %m");
return;
}

size_t utf8_len = latin1_to_utf8(utf8, text, len);

server->cut_text_fn(client, utf8, utf8_len);
free(utf8);
}

static void process_client_ext_clipboard_provide(struct nvnc_client* client,
unsigned char* zlib_data, size_t zlib_len)
{
Expand Down Expand Up @@ -1562,9 +1594,7 @@ static void process_client_ext_clipboard_provide(struct nvnc_client* client,
}
size_t converted_len = strlen(converted_buf);

nvnc_cut_text_fn fn = client->server->cut_text_fn;
if (fn)
fn(client, converted_buf, converted_len);
notify_cut_text_utf8(client, converted_buf, converted_len);

free(converted_buf);
}
Expand Down Expand Up @@ -1680,9 +1710,7 @@ static int process_client_cut_text(struct nvnc_client* client)
size_t msg_size = sizeof(*msg) + length;

if (msg_size <= left_to_process) {
nvnc_cut_text_fn fn = client->server->cut_text_fn;
if (fn)
fn(client, msg->text, length);
notify_cut_text_latin1(client, msg->text, length);

return msg_size;
}
Expand Down Expand Up @@ -1792,10 +1820,8 @@ static void process_big_cut_text(struct nvnc_client* client)
(unsigned char*)client->cut_text.buffer,
client->cut_text.length);
} else {
nvnc_cut_text_fn fn = client->server->cut_text_fn;
if (fn)
fn(client, client->cut_text.buffer,
client->cut_text.length);
notify_cut_text_latin1(client, client->cut_text.buffer,
client->cut_text.length);
}

free(client->cut_text.buffer);
Expand Down Expand Up @@ -1860,25 +1886,12 @@ static void send_cut_text_to_client(struct nvnc_client* client,
stream_write(client->net_stream, text, len);
}

EXPORT
void nvnc_send_cut_text(struct nvnc* server, const char* text, uint32_t len)
static void send_cut_text_utf8(struct nvnc* server,
const char* text, uint32_t len)
{
struct nvnc_client* client;

bool ext_clipboard_in_use = false;
LIST_FOREACH (client, &server->clients, link) {
if (client_has_encoding(client, RFB_ENCODING_EXTENDED_CLIPBOARD)) {
ext_clipboard_in_use = true;
break;
}
}

if (ext_clipboard_in_use) {
ext_clipboard_save_provide_msg(server, text, len);
} else if (server->ext_clipboard_provide_msg.buffer) {
free(server->ext_clipboard_provide_msg.buffer);
server->ext_clipboard_provide_msg.buffer = NULL;
}
ext_clipboard_save_provide_msg(server, text, len);

LIST_FOREACH (client, &server->clients, link) {
if (client_has_encoding(client, RFB_ENCODING_EXTENDED_CLIPBOARD)) {
Expand All @@ -1890,10 +1903,57 @@ void nvnc_send_cut_text(struct nvnc* server, const char* text, uint32_t len)
send_ext_clipboard_provide(client);
else if (client->ext_clipboard_caps & RFB_EXT_CLIPBOARD_ACTION_NOTIFY)
send_ext_clipboard_notify(client);
}
}
}

static void send_cut_text_latin1(struct nvnc* server,
const char* text, uint32_t len)
{
struct nvnc_client* client;

char* latin1_buf = malloc(UTF8_TO_LATIN1_MAX_SIZE(len));
if (!latin1_buf) {
nvnc_log(NVNC_LOG_ERROR, "OOM: %m");
return;
}

size_t latin1_len = utf8_to_latin1(latin1_buf, text, len);

LIST_FOREACH (client, &server->clients, link) {
if (!client_has_encoding(client, RFB_ENCODING_EXTENDED_CLIPBOARD)) {
send_cut_text_to_client(client, latin1_buf, latin1_len);
}
}

free(latin1_buf);
}

EXPORT
void nvnc_send_cut_text(struct nvnc* server, const char* text, uint32_t len)
{
struct nvnc_client* client;

bool latin1_in_use = false;
bool utf8_in_use = false;

LIST_FOREACH (client, &server->clients, link) {
if (client_has_encoding(client, RFB_ENCODING_EXTENDED_CLIPBOARD)) {
utf8_in_use = true;
} else {
send_cut_text_to_client(client, text, len);
latin1_in_use = true;
}
}

if (utf8_in_use) {
send_cut_text_utf8(server, text, len);
} else {
free(server->ext_clipboard_provide_msg.buffer);
server->ext_clipboard_provide_msg.buffer = NULL;
}

if (latin1_in_use)
send_cut_text_latin1(server, text, len);
}

static struct nvnc_desktop_layout* unpack_desktop_layout(uint16_t width,
Expand Down
107 changes: 107 additions & 0 deletions src/utf8.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2026 Norbert Schultz
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/

#include "utf8.h"
#include <stdbool.h>
#include <stdint.h>

size_t latin1_to_utf8(char* dst, const char* src, size_t src_len)
{
size_t o = 0;
for (size_t i = 0; i < src_len; ++i) {
uint8_t c = src[i];
if (c < 0x80) {
dst[o++] = c;
} else {
dst[o++] = 0xc0 | (c >> 6);
dst[o++] = 0x80 | (c & 0x3f);
}
}

dst[o] = 0;
return o;
}

size_t utf8_to_latin1(char* dst, const char* src, size_t src_len)
{
size_t o = 0;
for (size_t i = 0; i < src_len;) {
uint8_t c = src[i];
uint32_t cp;
uint32_t min_cp;
size_t n;

if (c < 0x80) {
dst[o++] = c;
i++;
continue;
} else if ((c & 0xe0) == 0xc0) {
cp = c & 0x1f;
n = 2;
min_cp = 0x80;
} else if ((c & 0xf0) == 0xe0) {
cp = c & 0x0f;
n = 3;
min_cp = 0x800;
} else if ((c & 0xf8) == 0xf0) {
cp = c & 0x07;
n = 4;
min_cp = 0x10000;
} else {
/* Stray continuation byte or invalid lead byte. */
dst[o++] = '?';
i++;
continue;
}

if (i + n > src_len) {
/* Truncated sequence at end of input. */
dst[o++] = '?';
break;
}

bool valid = true;
for (size_t k = 1; k < n; ++k) {
uint8_t cc = src[i + k];
if ((cc & 0xc0) != 0x80) {
valid = false;
break;
}
cp = (cp << 6) | (cc & 0x3f);
}

if (!valid) {
/* Missing continuation byte; resync on the bad byte. */
dst[o++] = '?';
i++;
continue;
}

/* Reject overlong encodings, UTF-16 surrogates and code points
* beyond the Unicode range.
*/
if (cp < min_cp || (cp >= 0xd800 && cp <= 0xdfff) ||
cp > 0x10ffff)
dst[o++] = '?';
else
dst[o++] = cp <= 0xff ? (char)cp : '?';

i += n;
}

dst[o] = 0;
return o;
}
6 changes: 6 additions & 0 deletions test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ base64 = executable('base64', 'test-base64.c',
)
test('base64', base64)

utf8 = executable('utf8', 'test-utf8.c',
include_directories: inc,
dependencies: dependencies
)
test('utf8', utf8)

if nettle.found() and python3.found()
rfb_test_server = executable('rfb-test-server', 'rfb-test-server.c',
include_directories: inc,
Expand Down
Loading