changkun/midgard

xclip false read on Linux

changkun opened this issue · 5 comments

Run the following test in internal/clipboard/platform:

func TestLocalClipboardWrite(t *testing.T) {
	platform.Write([]byte("hi"), types.MIMEPlainText)
	buf := platform.Read(types.MIMEImagePNG)
	if buf != nil {
		t.Errorf("write as text but can be captured as image: %s", string(buf))
	}

	buf = nil
	platform.Write([]byte("there"), types.MIMEImagePNG)
	buf = platform.Read(types.MIMEPlainText)
	if buf != nil {
		t.Errorf("write as image but can be captured as text: %s", string(buf))
	}
}
=== RUN   TestLocalClipboardWrite
    clipboard_test.go:35: write as text but can be captured as image: hi
    clipboard_test.go:42: write as image but can be captured as text: there
--- FAIL: TestLocalClipboardWrite (0.01s)
FAIL
exit status 1
FAIL    changkun.de/x/midgard/internal/clipboard/platform       0.008s

Expect a PASS.

This is a bug from xclip, see astrand/xclip#113.

xclip seems to be the only tool (alternatives such as xsel, etc.) that can deal with non-text targets. This was the reason why it was used directly in midgard.

To get rid of dependency, and resolve this issue, we will have to plug cgo and use X11 facilities to deal with the clipboard data.

I have absolutely zero idea why the following implementation does not work when I plug it in Go:

// Copyright 2021 Changkun Ou. All rights reserved.
// Use of this source code is governed by a GPL-3.0
// license that can be found in the LICENSE file.

#include <stdlib.h>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xmu/Atoms.h>

static Display* d;
static Window w;

static Atom selCB;
static Atom mgProp;

static Atom stringAtom;
static Atom imageAtom;

int clipboard_init() {
	XInitThreads();

	d = XOpenDisplay(0);
	w = XCreateSimpleWindow(d, DefaultRootWindow(d), 0, 0, 1, 1, 0, 0, 0);

	selCB      = XInternAtom(d, "CLIPBOARD", True);
	mgProp      = XInternAtom(d, "MIDGARD_DATA", FALSE);

	stringAtom = XInternAtom(d, "UTF8_STRING", True);
	imageAtom  = XInternAtom(d, "image/png", True);
	return 0;
}

unsigned long clipboard_read(char* typ, char **out) {
    Atom target = XInternAtom(d, typ, True);
    if (target == None) {
        return 0;
    }

    XConvertSelection(d, selCB, target, mgProp, w, CurrentTime);
    printf("aw;oeifjaweoifj\n");
    fflush(stdout);
    XEvent event;
    while (1) {
        XNextEvent(d, &event);
        if (event.type != SelectionNotify) {
            continue;
        }
        break;
    }

    unsigned char *data;
    Atom actual;
    int format;
    unsigned long n = 0, size = 0;
    size_t itemsize = 0;
    if (event.xselection.selection != selCB || event.xselection.property != mgProp) {
        return 0;
    }

    int ret = XGetWindowProperty(
        event.xselection.display,
        event.xselection.requestor,
        event.xselection.property,
        0L, (~0L), 0, AnyPropertyType, &actual, &format, &size, &n, &data);
    if (ret != Success) {
        return 0;
    }

    if (actual == target) {
        *out = (char *)malloc(size * sizeof(char));
        memcpy(*out, data, size*sizeof(char));
    }
    XFree(data);
    XDeleteProperty(event.xselection.display,
        event.xselection.requestor, event.xselection.property);
    return size * sizeof(char);
}

int clipboard_write(char *typ, unsigned char *buf, size_t n) {
    Atom target = XInternAtom(d, typ, True);
    if (target == None) {
        return -1;
    }

    XEvent event;
    XSetSelectionOwner(d, selCB, w, CurrentTime);
    if (XGetSelectionOwner(d, selCB) != w) {
        return -2;
    }

    XSelectionRequestEvent* xsr;
    while (1) {
        XNextEvent(d, &event);
        switch (event.type) {
        case SelectionClear:
            printf("cb_write: lost ownership of clipboard selection.\n");
            fflush(stdout);
            return 0;
        case SelectionRequest:
            if (event.xselectionrequest.selection != selCB) {
                break;
            }

            XSelectionRequestEvent * xsr = &event.xselectionrequest;
            XSelectionEvent ev = {0};
            int R = 0;

            ev.type      = SelectionNotify;
            ev.display   = xsr->display;
            ev.requestor = xsr->requestor;
            ev.selection = xsr->selection;
            ev.time      = xsr->time;
            ev.target    = xsr->target;
            ev.property  = xsr->property;

            if (ev.target == stringAtom)
                R = XChangeProperty(ev.display, ev.requestor, ev.property, stringAtom, 8, PropModeReplace, buf, n);
            else if (ev.target == imageAtom)
                R = XChangeProperty(ev.display, ev.requestor, ev.property, imageAtom, 8, PropModeReplace, buf, n);
            else ev.property = None;
            if ((R & 2) == 0) XSendEvent(d, ev.requestor, 0, 0, (XEvent *)&ev);
            break;
        }
    }
}

Notably, the clipboard_write is waiting for other applications to send requests to obtain clipboard data. However, as I observed, the event was sent but the requestor does not receive the event that I sent.

// Copyright 2020 Changkun Ou. All rights reserved.
// Use of this source code is governed by a GPL-3.0
// license that can be found in the LICENSE file.

// +build linux

package platform

/*
#cgo LDFLAGS: -lX11 -lXmu
#include <stdlib.h>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xmu/Atoms.h>
int clipboard_init();
unsigned long clipboard_read(char* typ, char **out);
int clipboard_write(char *typ, unsigned char *buf, size_t n);
*/
import "C"
import (
	"bytes"
	"context"
	"fmt"
	"runtime"
	"time"
	"unsafe"

	"changkun.de/x/midgard/internal/types"
)

func init() {
	if ret := C.clipboard_init(); ret == 0 {
		return
	}
	panic("cannot initialize midgard clipboard!")
}

// Read reads the clipboard data of a given resource type.
// It returns a buf that containing the clipboard data, and ok indicates
// whether the read is success or fail.
func Read(t types.MIME) (buf []byte) {
	runtime.LockOSThread()
	var s string
	switch t {
	case types.MIMEPlainText:
		s = "UTF8_STRING"
	case types.MIMEImagePNG:
		s = "image/png"
	}
	ctyp := C.CString(s)
	defer C.free(unsafe.Pointer(ctyp))

	var data *C.char
	n := C.clipboard_read(ctyp, &data)
	if data == nil {
		return nil
	}
	defer C.free(unsafe.Pointer(data))
	if n == 0 {
		return nil
	}

	return C.GoBytes(unsafe.Pointer(data), C.int(n))
}

// Write writes the given buf as t type to clipboard selection.
// It returns true if the write is success.
func Write(buf []byte, t types.MIME) (ret bool) {
	var typ string
	switch t {
	case types.MIMEPlainText:
		typ = "UTF8_STRING"
	case types.MIMEImagePNG:
		typ = "image/png"
	}

	go func() { // surve as a daemon until the ownership is terminated.
		runtime.LockOSThread()
		cs := C.CString(typ)
		defer C.free(unsafe.Pointer(cs))
		ok := C.clipboard_write(cs, (*C.uchar)(unsafe.Pointer(&buf[0])), C.size_t(len(buf)))
		if ok != C.int(0) {
			fmt.Printf("write failed: %d\n", int(ok))
		}
	}()
	return true // always true
}

As a follow-up record, I've written a clipboard package for Go users: golang.design/s/clipboard.

Now, Midgard is a user of that package, which also brings Windows support for Midgard users.