| /* | 
 |  * 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); | 
 |     } | 
 | } |