a73x

f37ca90d

Fix clipboard send fd lifecycle

a73x   2026-04-09 18:32


diff --git a/src/wayland.zig b/src/wayland.zig
index 98e3014..1da7ede 100644
--- a/src/wayland.zig
+++ b/src/wayland.zig
@@ -389,6 +389,29 @@ const ClipboardTransfer = struct {
    }
};

fn serveOwnedSelectionRequest(
    alloc: std.mem.Allocator,
    text: ?[]const u8,
    mime_type: []const u8,
    fd: i32,
) !bool {
    var handed_off = false;
    defer {
        if (!handed_off) _ = c.close(fd);
    }

    if (text == null) return false;
    if (!(std.mem.eql(u8, mime_type, "text/plain;charset=utf-8") or
        std.mem.eql(u8, mime_type, "text/plain"))) return false;

    const transfer = try ClipboardTransfer.init(alloc, text.?, fd);
    errdefer alloc.free(transfer.payload);
    const thread = try std.Thread.spawn(.{}, ClipboardTransfer.run, .{transfer});
    handed_off = true;
    thread.detach();
    return true;
}

pub const Clipboard = struct {
    alloc: std.mem.Allocator,
    display: *wl.Display,
@@ -478,20 +501,15 @@ pub const Clipboard = struct {
    }

    fn sendOwnedSelection(self: *Clipboard, mime_type: [*:0]const u8, fd: i32) void {
        if (!self.owned_selection.canServeMime(std.mem.span(mime_type))) return;
        const text = self.owned_selection.text orelse return;
        const transfer = ClipboardTransfer.init(self.alloc, text, fd) catch |err| {
        _ = serveOwnedSelectionRequest(
            self.alloc,
            self.owned_selection.text,
            std.mem.span(mime_type),
            fd,
        ) catch |err| {
            std.log.err("clipboard transfer setup failed: {any}", .{err});
            _ = c.close(fd);
            return;
        };
        const thread = std.Thread.spawn(.{}, ClipboardTransfer.run, .{transfer}) catch |err| {
            std.log.err("clipboard transfer thread spawn failed: {any}", .{err});
            self.alloc.free(transfer.payload);
            _ = c.close(fd);
            return;
        };
        thread.detach();
    }
};

@@ -985,6 +1003,40 @@ test "writeClipboardTransfer writes payload and closes fd" {
    try std.testing.expectEqualStrings("clipboard payload", out.items);
}

test "serveOwnedSelectionRequest closes fd for unsupported mime and missing text" {
    {
        const pipefds = try std.posix.pipe();
        defer std.posix.close(pipefds[0]);

        try std.testing.expect(!try serveOwnedSelectionRequest(
            std.testing.allocator,
            null,
            "text/plain",
            pipefds[1],
        ));

        var buf: [1]u8 = undefined;
        const n = try std.posix.read(pipefds[0], &buf);
        try std.testing.expectEqual(@as(usize, 0), n);
    }

    {
        const pipefds = try std.posix.pipe();
        defer std.posix.close(pipefds[0]);

        try std.testing.expect(!try serveOwnedSelectionRequest(
            std.testing.allocator,
            "payload",
            "text/html",
            pipefds[1],
        ));

        var buf: [1]u8 = undefined;
        const n = try std.posix.read(pipefds[0], &buf);
        try std.testing.expectEqual(@as(usize, 0), n);
    }
}

fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void {
    switch (event) {
        .ping => |p| wm_base.pong(p.serial),