120 lines
4.6 KiB
C++
120 lines
4.6 KiB
C++
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// SetTitle on the Wayland backend sends xdg_toplevel.set_title with exactly
|
||
|
|
// the view's bytes. It handed title.data() on, which set_title reads up to
|
||
|
|
// a NUL: a view into a longer string sent the whole string, and an empty
|
||
|
|
// view, whose data() is null, sent a null string, which libwayland refuses
|
||
|
|
// with a fatal error on the display.
|
||
|
|
//
|
||
|
|
// The toplevel is a proxy on a wl_display connected to one end of a
|
||
|
|
// socketpair, and the test reads the requests off the other end, so no
|
||
|
|
// compositor is needed. Needs the Wayland backend, hence Linux only.
|
||
|
|
|
||
|
|
#include <wayland-client.h>
|
||
|
|
#include <sys/socket.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
#include <cerrno>
|
||
|
|
#include <cstdlib>
|
||
|
|
#include <cstring>
|
||
|
|
#include "../../lib/xdg-shell-client-protocol.h"
|
||
|
|
|
||
|
|
import Crafter.Graphics;
|
||
|
|
import std;
|
||
|
|
using namespace Crafter;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
int failures = 0;
|
||
|
|
|
||
|
|
void Check(bool ok, std::string_view what) {
|
||
|
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||
|
|
if (!ok) ++failures;
|
||
|
|
}
|
||
|
|
|
||
|
|
// What the display has written so far.
|
||
|
|
std::vector<std::uint8_t> Sent(wl_display* display, int peer) {
|
||
|
|
wl_display_flush(display);
|
||
|
|
std::vector<std::uint8_t> wire;
|
||
|
|
std::uint8_t chunk[4096];
|
||
|
|
for (ssize_t n; (n = recv(peer, chunk, sizeof(chunk), MSG_DONTWAIT)) > 0;) {
|
||
|
|
wire.insert(wire.end(), chunk, chunk + n);
|
||
|
|
}
|
||
|
|
return wire;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The titles of the set_title requests on `object` in `wire`. A request is
|
||
|
|
// its object id, then its size and opcode in one word, then its arguments;
|
||
|
|
// set_title's one argument is a string, its length with the NUL, then its
|
||
|
|
// bytes padded to a word.
|
||
|
|
std::vector<std::string> Titles(const std::vector<std::uint8_t>& wire, std::uint32_t object) {
|
||
|
|
std::vector<std::string> titles;
|
||
|
|
for (std::size_t at = 0; at + 8 <= wire.size();) {
|
||
|
|
std::uint32_t header[2];
|
||
|
|
std::memcpy(header, wire.data() + at, sizeof(header));
|
||
|
|
const std::uint32_t size = header[1] >> 16;
|
||
|
|
if (size < 8 || at + size > wire.size()) break;
|
||
|
|
if (header[0] == object && (header[1] & 0xFFFF) == XDG_TOPLEVEL_SET_TITLE && size >= 12) {
|
||
|
|
std::uint32_t length;
|
||
|
|
std::memcpy(&length, wire.data() + at + 8, sizeof(length));
|
||
|
|
if (length > 0 && 12 + length <= size) {
|
||
|
|
titles.emplace_back(reinterpret_cast<const char*>(wire.data() + at + 12), length - 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
at += size;
|
||
|
|
}
|
||
|
|
return titles;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
int fds[2];
|
||
|
|
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds) != 0) {
|
||
|
|
std::println("FAIL socketpair: {}", std::strerror(errno));
|
||
|
|
return EXIT_FAILURE;
|
||
|
|
}
|
||
|
|
// The display owns fds[0] from here.
|
||
|
|
wl_display* display = wl_display_connect_to_fd(fds[0]);
|
||
|
|
if (display == nullptr) {
|
||
|
|
std::println("FAIL wl_display_connect_to_fd: {}", std::strerror(errno));
|
||
|
|
return EXIT_FAILURE;
|
||
|
|
}
|
||
|
|
// libwayland makes whatever proxy it is asked to bind; nothing answers.
|
||
|
|
wl_registry* registry = wl_display_get_registry(display);
|
||
|
|
auto* toplevel = static_cast<xdg_toplevel*>(wl_registry_bind(registry, 1, &xdg_toplevel_interface, 1));
|
||
|
|
const std::uint32_t id = wl_proxy_get_id(reinterpret_cast<wl_proxy*>(toplevel));
|
||
|
|
Sent(display, fds[1]);
|
||
|
|
|
||
|
|
{
|
||
|
|
// A default-constructed window, which ~Window can tear down without
|
||
|
|
// a display, holding the proxy as its toplevel.
|
||
|
|
Window window;
|
||
|
|
window.xdgToplevel = toplevel;
|
||
|
|
|
||
|
|
const std::string longer = "Crafter.Graphics";
|
||
|
|
window.SetTitle(std::string_view(longer).substr(0, 7));
|
||
|
|
const std::vector<std::string> sliced = Titles(Sent(display, fds[1]), id);
|
||
|
|
Check(sliced == std::vector<std::string>{ "Crafter" }, "a view into a longer string: the view's bytes only");
|
||
|
|
if (sliced != std::vector<std::string>{ "Crafter" }) {
|
||
|
|
for (const std::string& title : sliced) std::println(" set_title \"{}\"", title);
|
||
|
|
}
|
||
|
|
|
||
|
|
window.SetTitle(std::string_view{});
|
||
|
|
const std::vector<std::string> empty = Titles(Sent(display, fds[1]), id);
|
||
|
|
const int error = wl_display_get_error(display);
|
||
|
|
Check(error == 0 && empty == std::vector<std::string>{ "" }, "an empty view: an empty title, not a null string");
|
||
|
|
if (error != 0) std::println(" the display failed: {}", std::strerror(error));
|
||
|
|
// ~Window destroys the toplevel.
|
||
|
|
}
|
||
|
|
wl_registry_destroy(registry);
|
||
|
|
wl_display_disconnect(display);
|
||
|
|
close(fds[1]);
|
||
|
|
|
||
|
|
if (failures != 0) {
|
||
|
|
std::println("{} check(s) failed", failures);
|
||
|
|
return EXIT_FAILURE;
|
||
|
|
}
|
||
|
|
std::println("all checks passed");
|
||
|
|
return EXIT_SUCCESS;
|
||
|
|
}
|