Browse Source
This patch adds support for cut+paste to the qemu vnc server, which allows the vnc client exchange clipbaord data with qemu and other peers like the qemu vdagent implementation. Signed-off-by: Gerd Hoffmann <kraxel@redhat.com> Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com> Message-id: 20210519053940.1888907-1-kraxel@redhat.com Message-Id: <20210519053940.1888907-8-kraxel@redhat.com>pull/121/head
4 changed files with 363 additions and 6 deletions
@ -0,0 +1,323 @@ |
|||
/*
|
|||
* QEMU VNC display driver -- clipboard support |
|||
* |
|||
* Copyright (C) 2021 Gerd Hoffmann <kraxel@redhat.com> |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
* of this software and associated documentation files (the "Software"), to deal |
|||
* in the Software without restriction, including without limitation the rights |
|||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
* copies of the Software, and to permit persons to whom the Software is |
|||
* furnished to do so, subject to the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be included in |
|||
* all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
|||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
* THE SOFTWARE. |
|||
*/ |
|||
|
|||
#include "qemu/osdep.h" |
|||
#include "qemu-common.h" |
|||
#include "vnc.h" |
|||
#include "vnc-jobs.h" |
|||
|
|||
static uint8_t *inflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size) |
|||
{ |
|||
z_stream stream = { |
|||
.next_in = in, |
|||
.avail_in = in_len, |
|||
.zalloc = Z_NULL, |
|||
.zfree = Z_NULL, |
|||
}; |
|||
uint32_t out_len = 8; |
|||
uint8_t *out = g_malloc(out_len); |
|||
int ret; |
|||
|
|||
stream.next_out = out + stream.total_out; |
|||
stream.avail_out = out_len - stream.total_out; |
|||
|
|||
ret = inflateInit(&stream); |
|||
if (ret != Z_OK) { |
|||
goto err; |
|||
} |
|||
|
|||
while (stream.avail_in) { |
|||
ret = inflate(&stream, Z_FINISH); |
|||
switch (ret) { |
|||
case Z_OK: |
|||
case Z_STREAM_END: |
|||
break; |
|||
case Z_BUF_ERROR: |
|||
out_len <<= 1; |
|||
if (out_len > (1 << 20)) { |
|||
goto err_end; |
|||
} |
|||
out = g_realloc(out, out_len); |
|||
stream.next_out = out + stream.total_out; |
|||
stream.avail_out = out_len - stream.total_out; |
|||
break; |
|||
default: |
|||
goto err_end; |
|||
} |
|||
} |
|||
|
|||
*size = stream.total_out; |
|||
inflateEnd(&stream); |
|||
|
|||
return out; |
|||
|
|||
err_end: |
|||
inflateEnd(&stream); |
|||
err: |
|||
g_free(out); |
|||
return NULL; |
|||
} |
|||
|
|||
static uint8_t *deflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size) |
|||
{ |
|||
z_stream stream = { |
|||
.next_in = in, |
|||
.avail_in = in_len, |
|||
.zalloc = Z_NULL, |
|||
.zfree = Z_NULL, |
|||
}; |
|||
uint32_t out_len = 8; |
|||
uint8_t *out = g_malloc(out_len); |
|||
int ret; |
|||
|
|||
stream.next_out = out + stream.total_out; |
|||
stream.avail_out = out_len - stream.total_out; |
|||
|
|||
ret = deflateInit(&stream, Z_DEFAULT_COMPRESSION); |
|||
if (ret != Z_OK) { |
|||
goto err; |
|||
} |
|||
|
|||
while (ret != Z_STREAM_END) { |
|||
ret = deflate(&stream, Z_FINISH); |
|||
switch (ret) { |
|||
case Z_OK: |
|||
case Z_STREAM_END: |
|||
break; |
|||
case Z_BUF_ERROR: |
|||
out_len <<= 1; |
|||
if (out_len > (1 << 20)) { |
|||
goto err_end; |
|||
} |
|||
out = g_realloc(out, out_len); |
|||
stream.next_out = out + stream.total_out; |
|||
stream.avail_out = out_len - stream.total_out; |
|||
break; |
|||
default: |
|||
goto err_end; |
|||
} |
|||
} |
|||
|
|||
*size = stream.total_out; |
|||
deflateEnd(&stream); |
|||
|
|||
return out; |
|||
|
|||
err_end: |
|||
deflateEnd(&stream); |
|||
err: |
|||
g_free(out); |
|||
return NULL; |
|||
} |
|||
|
|||
static void vnc_clipboard_send(VncState *vs, uint32_t count, uint32_t *dwords) |
|||
{ |
|||
int i; |
|||
|
|||
vnc_lock_output(vs); |
|||
vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT); |
|||
vnc_write_u8(vs, 0); |
|||
vnc_write_u8(vs, 0); |
|||
vnc_write_u8(vs, 0); |
|||
vnc_write_s32(vs, -(count * sizeof(uint32_t))); /* -(message length) */ |
|||
for (i = 0; i < count; i++) { |
|||
vnc_write_u32(vs, dwords[i]); |
|||
} |
|||
vnc_unlock_output(vs); |
|||
vnc_flush(vs); |
|||
} |
|||
|
|||
static void vnc_clipboard_provide(VncState *vs, |
|||
QemuClipboardInfo *info, |
|||
QemuClipboardType type) |
|||
{ |
|||
uint32_t flags = 0; |
|||
g_autofree uint8_t *buf = NULL; |
|||
g_autofree void *zbuf = NULL; |
|||
uint32_t zsize; |
|||
|
|||
switch (type) { |
|||
case QEMU_CLIPBOARD_TYPE_TEXT: |
|||
flags |= VNC_CLIPBOARD_TEXT; |
|||
break; |
|||
default: |
|||
return; |
|||
} |
|||
flags |= VNC_CLIPBOARD_PROVIDE; |
|||
|
|||
buf = g_malloc(info->types[type].size + 4); |
|||
buf[0] = (info->types[type].size >> 24) & 0xff; |
|||
buf[1] = (info->types[type].size >> 16) & 0xff; |
|||
buf[2] = (info->types[type].size >> 8) & 0xff; |
|||
buf[3] = (info->types[type].size >> 0) & 0xff; |
|||
memcpy(buf + 4, info->types[type].data, info->types[type].size); |
|||
zbuf = deflate_buffer(buf, info->types[type].size + 4, &zsize); |
|||
if (!zbuf) { |
|||
return; |
|||
} |
|||
|
|||
vnc_lock_output(vs); |
|||
vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT); |
|||
vnc_write_u8(vs, 0); |
|||
vnc_write_u8(vs, 0); |
|||
vnc_write_u8(vs, 0); |
|||
vnc_write_s32(vs, -(sizeof(uint32_t) + zsize)); /* -(message length) */ |
|||
vnc_write_u32(vs, flags); |
|||
vnc_write(vs, zbuf, zsize); |
|||
vnc_unlock_output(vs); |
|||
vnc_flush(vs); |
|||
} |
|||
|
|||
static void vnc_clipboard_notify(Notifier *notifier, void *data) |
|||
{ |
|||
VncState *vs = container_of(notifier, VncState, cbpeer.update); |
|||
QemuClipboardInfo *info = data; |
|||
QemuClipboardType type; |
|||
bool self_update = info->owner == &vs->cbpeer; |
|||
uint32_t flags = 0; |
|||
|
|||
if (info != vs->cbinfo) { |
|||
qemu_clipboard_info_unref(vs->cbinfo); |
|||
vs->cbinfo = qemu_clipboard_info_ref(info); |
|||
vs->cbpending = 0; |
|||
if (!self_update) { |
|||
if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { |
|||
flags |= VNC_CLIPBOARD_TEXT; |
|||
} |
|||
flags |= VNC_CLIPBOARD_NOTIFY; |
|||
vnc_clipboard_send(vs, 1, &flags); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
if (self_update) { |
|||
return; |
|||
} |
|||
|
|||
for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) { |
|||
if (vs->cbpending & (1 << type)) { |
|||
vs->cbpending &= ~(1 << type); |
|||
vnc_clipboard_provide(vs, info, type); |
|||
} |
|||
} |
|||
} |
|||
|
|||
static void vnc_clipboard_request(QemuClipboardInfo *info, |
|||
QemuClipboardType type) |
|||
{ |
|||
VncState *vs = container_of(info->owner, VncState, cbpeer); |
|||
uint32_t flags = 0; |
|||
|
|||
if (type == QEMU_CLIPBOARD_TYPE_TEXT) { |
|||
flags |= VNC_CLIPBOARD_TEXT; |
|||
} |
|||
if (!flags) { |
|||
return; |
|||
} |
|||
flags |= VNC_CLIPBOARD_REQUEST; |
|||
|
|||
vnc_clipboard_send(vs, 1, &flags); |
|||
} |
|||
|
|||
void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data) |
|||
{ |
|||
if (flags & VNC_CLIPBOARD_CAPS) { |
|||
/* need store caps somewhere ? */ |
|||
return; |
|||
} |
|||
|
|||
if (flags & VNC_CLIPBOARD_NOTIFY) { |
|||
QemuClipboardInfo *info = |
|||
qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); |
|||
if (flags & VNC_CLIPBOARD_TEXT) { |
|||
info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; |
|||
} |
|||
qemu_clipboard_update(info); |
|||
qemu_clipboard_info_unref(info); |
|||
return; |
|||
} |
|||
|
|||
if (flags & VNC_CLIPBOARD_PROVIDE && |
|||
vs->cbinfo && |
|||
vs->cbinfo->owner == &vs->cbpeer) { |
|||
uint32_t size = 0; |
|||
g_autofree uint8_t *buf = inflate_buffer(data, len - 4, &size); |
|||
if ((flags & VNC_CLIPBOARD_TEXT) && |
|||
buf && size >= 4) { |
|||
uint32_t tsize = read_u32(buf, 0); |
|||
uint8_t *tbuf = buf + 4; |
|||
if (tsize < size) { |
|||
qemu_clipboard_set_data(&vs->cbpeer, vs->cbinfo, |
|||
QEMU_CLIPBOARD_TYPE_TEXT, |
|||
tsize, tbuf, true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (flags & VNC_CLIPBOARD_REQUEST && |
|||
vs->cbinfo && |
|||
vs->cbinfo->owner != &vs->cbpeer) { |
|||
if ((flags & VNC_CLIPBOARD_TEXT) && |
|||
vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { |
|||
if (vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].data) { |
|||
vnc_clipboard_provide(vs, vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT); |
|||
} else { |
|||
vs->cbpending |= (1 << QEMU_CLIPBOARD_TYPE_TEXT); |
|||
qemu_clipboard_request(vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text) |
|||
{ |
|||
QemuClipboardInfo *info = |
|||
qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); |
|||
|
|||
qemu_clipboard_set_data(&vs->cbpeer, info, QEMU_CLIPBOARD_TYPE_TEXT, |
|||
len, text, true); |
|||
qemu_clipboard_info_unref(info); |
|||
} |
|||
|
|||
void vnc_server_cut_text_caps(VncState *vs) |
|||
{ |
|||
uint32_t caps[2]; |
|||
|
|||
if (!vnc_has_feature(vs, VNC_FEATURE_CLIPBOARD_EXT)) { |
|||
return; |
|||
} |
|||
|
|||
caps[0] = (VNC_CLIPBOARD_PROVIDE | |
|||
VNC_CLIPBOARD_NOTIFY | |
|||
VNC_CLIPBOARD_REQUEST | |
|||
VNC_CLIPBOARD_CAPS | |
|||
VNC_CLIPBOARD_TEXT); |
|||
caps[1] = 0; |
|||
vnc_clipboard_send(vs, 2, caps); |
|||
|
|||
vs->cbpeer.name = "vnc"; |
|||
vs->cbpeer.update.notify = vnc_clipboard_notify; |
|||
vs->cbpeer.request = vnc_clipboard_request; |
|||
qemu_clipboard_peer_register(&vs->cbpeer); |
|||
} |
|||
Loading…
Reference in new issue