| /* |
| * 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 "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: |
| break; |
| case Z_STREAM_END: |
| *size = stream.total_out; |
| inflateEnd(&stream); |
| return out; |
| 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_update_info(VncState *vs, QemuClipboardInfo *info) |
| { |
| 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_notify(Notifier *notifier, void *data) |
| { |
| VncState *vs = container_of(notifier, VncState, cbpeer.notifier); |
| QemuClipboardNotify *notify = data; |
| |
| switch (notify->type) { |
| case QEMU_CLIPBOARD_UPDATE_INFO: |
| vnc_clipboard_update_info(vs, notify->info); |
| return; |
| case QEMU_CLIPBOARD_RESET_SERIAL: |
| /* ignore */ |
| return; |
| } |
| } |
| |
| 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); |
| |
| if (!vs->cbpeer.notifier.notify) { |
| vs->cbpeer.name = "vnc"; |
| vs->cbpeer.notifier.notify = vnc_clipboard_notify; |
| vs->cbpeer.request = vnc_clipboard_request; |
| qemu_clipboard_peer_register(&vs->cbpeer); |
| } |
| } |