fix: harden redbear compositor Wayland protocol handling
This commit is contained in:
+270
-12
@@ -14,17 +14,36 @@ const WL_DISPLAY_GET_REGISTRY: u16 = 1;
|
||||
const WL_REGISTRY_BIND: u16 = 0;
|
||||
const WL_REGISTRY_GLOBAL: u16 = 0;
|
||||
const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0;
|
||||
const WL_COMPOSITOR_CREATE_REGION: u16 = 1;
|
||||
const WL_FIXES_DESTROY_REGISTRY: u16 = 1;
|
||||
const WL_FIXES_ACK_GLOBAL_REMOVE: u16 = 2;
|
||||
const WL_SEAT_CAPABILITIES: u16 = 0;
|
||||
const WL_SEAT_NAME: u16 = 1;
|
||||
const WL_SHM_CREATE_POOL: u16 = 0;
|
||||
const WL_SHM_FORMAT: u16 = 0;
|
||||
const WL_SHM_POOL_CREATE_BUFFER: u16 = 0;
|
||||
const WL_SURFACE_ATTACH: u16 = 0;
|
||||
const WL_SURFACE_COMMIT: u16 = 5;
|
||||
const WL_OUTPUT_GEOMETRY: u16 = 0;
|
||||
const WL_OUTPUT_MODE: u16 = 1;
|
||||
const WL_OUTPUT_DONE: u16 = 2;
|
||||
const WL_OUTPUT_SCALE: u16 = 3;
|
||||
const WL_OUTPUT_NAME: u16 = 4;
|
||||
const WL_OUTPUT_DESCRIPTION: u16 = 5;
|
||||
const WL_SURFACE_ATTACH: u16 = 1;
|
||||
const WL_SURFACE_SET_OPAQUE_REGION: u16 = 4;
|
||||
const WL_SURFACE_COMMIT: u16 = 6;
|
||||
const WL_REGION_DESTROY: u16 = 0;
|
||||
const WL_CALLBACK_DONE: u16 = 0;
|
||||
const XDG_WM_BASE_CREATE_POSITIONER: u16 = 1;
|
||||
const XDG_WM_BASE_GET_XDG_SURFACE: u16 = 2;
|
||||
const XDG_SURFACE_GET_TOPLEVEL: u16 = 1;
|
||||
const XDG_SURFACE_GET_POPUP: u16 = 2;
|
||||
const XDG_POSITIONER_DESTROY: u16 = 0;
|
||||
const XDG_POSITIONER_SET_SIZE: u16 = 1;
|
||||
const XDG_POPUP_DESTROY: u16 = 0;
|
||||
const XDG_SURFACE_ACK_CONFIGURE: u16 = 4;
|
||||
const XDG_SURFACE_CONFIGURE: u16 = 0;
|
||||
const XDG_TOPLEVEL_CONFIGURE: u16 = 0;
|
||||
const XDG_POPUP_CONFIGURE: u16 = 0;
|
||||
const WL_SHM_FORMAT_XRGB8888: u32 = 1;
|
||||
|
||||
fn push_u32(buf: &mut Vec<u8>, value: u32) {
|
||||
@@ -61,7 +80,18 @@ fn read_u32(data: &[u8], cursor: &mut usize) -> Result<u32, String> {
|
||||
}
|
||||
|
||||
fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result<String, String> {
|
||||
let length = read_u32(data, cursor)? as usize;
|
||||
if *cursor + 4 > data.len() {
|
||||
return Err(String::from(
|
||||
"unexpected end of message while reading string length",
|
||||
));
|
||||
}
|
||||
let length = u32::from_le_bytes([
|
||||
data[*cursor],
|
||||
data[*cursor + 1],
|
||||
data[*cursor + 2],
|
||||
data[*cursor + 3],
|
||||
]) as usize;
|
||||
*cursor += 4;
|
||||
if length == 0 {
|
||||
return Ok(String::new());
|
||||
}
|
||||
@@ -238,11 +268,11 @@ impl WaylandProbe {
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_globals(probe: &mut WaylandProbe) -> Result<HashMap<String, u32>, String> {
|
||||
fn collect_globals(probe: &mut WaylandProbe) -> Result<HashMap<String, (u32, u32)>, String> {
|
||||
let registry_id = probe.get_registry()?;
|
||||
let mut globals = HashMap::new();
|
||||
|
||||
for _ in 0..6 {
|
||||
for _ in 0..9 {
|
||||
let (object_id, opcode, payload) = probe.read_message()?;
|
||||
if object_id != registry_id || opcode != WL_REGISTRY_GLOBAL {
|
||||
return Err(format!(
|
||||
@@ -254,13 +284,124 @@ fn collect_globals(probe: &mut WaylandProbe) -> Result<HashMap<String, u32>, Str
|
||||
let mut cursor = 0;
|
||||
let name = read_u32(&payload, &mut cursor)?;
|
||||
let interface = read_wayland_string(&payload, &mut cursor)?;
|
||||
let _version = read_u32(&payload, &mut cursor)?;
|
||||
globals.insert(interface, name);
|
||||
let version = read_u32(&payload, &mut cursor)?;
|
||||
globals.insert(interface, (name, version));
|
||||
}
|
||||
|
||||
Ok(globals)
|
||||
}
|
||||
|
||||
fn expect_output_metadata(probe: &mut WaylandProbe, output_id: u32) -> Result<(), String> {
|
||||
let mut saw_geometry = false;
|
||||
let mut saw_mode = false;
|
||||
let mut saw_scale = false;
|
||||
let mut saw_name = false;
|
||||
let mut saw_description = false;
|
||||
let mut saw_done = false;
|
||||
|
||||
for _ in 0..6 {
|
||||
let (object_id, opcode, payload) = probe.read_message()?;
|
||||
if object_id != output_id {
|
||||
return Err(format!(
|
||||
"unexpected wl_output event object: expected {}, got {}",
|
||||
output_id, object_id
|
||||
));
|
||||
}
|
||||
|
||||
match opcode {
|
||||
WL_OUTPUT_GEOMETRY => {
|
||||
if payload.len() < 28 {
|
||||
return Err(format!(
|
||||
"short wl_output.geometry payload: {}",
|
||||
payload.len()
|
||||
));
|
||||
}
|
||||
saw_geometry = true;
|
||||
}
|
||||
WL_OUTPUT_MODE => {
|
||||
if payload.len() != 16 {
|
||||
return Err(format!("invalid wl_output.mode payload: {}", payload.len()));
|
||||
}
|
||||
saw_mode = true;
|
||||
}
|
||||
WL_OUTPUT_SCALE => {
|
||||
if payload != 1i32.to_le_bytes() {
|
||||
return Err(format!("unexpected wl_output.scale payload: {payload:?}"));
|
||||
}
|
||||
saw_scale = true;
|
||||
}
|
||||
WL_OUTPUT_NAME => {
|
||||
let mut cursor = 0;
|
||||
let name = read_wayland_string(&payload, &mut cursor)?;
|
||||
if name != "RedBear-0" {
|
||||
return Err(format!("unexpected wl_output.name: {name}"));
|
||||
}
|
||||
saw_name = true;
|
||||
}
|
||||
WL_OUTPUT_DESCRIPTION => {
|
||||
let mut cursor = 0;
|
||||
let description = read_wayland_string(&payload, &mut cursor)?;
|
||||
if description != "Red Bear OS framebuffer output" {
|
||||
return Err(format!("unexpected wl_output.description: {description}"));
|
||||
}
|
||||
saw_description = true;
|
||||
}
|
||||
WL_OUTPUT_DONE => saw_done = true,
|
||||
_ => return Err(format!("unexpected wl_output opcode: {opcode}")),
|
||||
}
|
||||
}
|
||||
|
||||
if saw_geometry && saw_mode && saw_scale && saw_name && saw_description && saw_done {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"incomplete wl_output metadata: geometry={saw_geometry} mode={saw_mode} scale={saw_scale} name={saw_name} description={saw_description} done={saw_done}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_seat_metadata(probe: &mut WaylandProbe, seat_id: u32) -> Result<(), String> {
|
||||
let mut saw_capabilities = false;
|
||||
let mut saw_name = false;
|
||||
|
||||
for _ in 0..2 {
|
||||
let (object_id, opcode, payload) = probe.read_message()?;
|
||||
if object_id != seat_id {
|
||||
return Err(format!(
|
||||
"unexpected wl_seat event object: expected {}, got {}",
|
||||
seat_id, object_id
|
||||
));
|
||||
}
|
||||
match opcode {
|
||||
WL_SEAT_CAPABILITIES => {
|
||||
if payload != 1u32.to_le_bytes() {
|
||||
return Err(format!(
|
||||
"unexpected wl_seat.capabilities payload: {payload:?}"
|
||||
));
|
||||
}
|
||||
saw_capabilities = true;
|
||||
}
|
||||
WL_SEAT_NAME => {
|
||||
let mut cursor = 0;
|
||||
let name = read_wayland_string(&payload, &mut cursor)?;
|
||||
if name != "seat0" {
|
||||
return Err(format!("unexpected wl_seat.name: {name}"));
|
||||
}
|
||||
saw_name = true;
|
||||
}
|
||||
_ => return Err(format!("unexpected wl_seat opcode: {opcode}")),
|
||||
}
|
||||
}
|
||||
|
||||
if saw_capabilities && saw_name {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"incomplete wl_seat metadata: capabilities={saw_capabilities} name={saw_name}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_shm_formats(probe: &mut WaylandProbe, shm_id: u32) -> Result<(), String> {
|
||||
let mut formats = Vec::new();
|
||||
|
||||
@@ -336,6 +477,36 @@ fn expect_xdg_configure(
|
||||
]))
|
||||
}
|
||||
|
||||
fn expect_xdg_popup_configure(
|
||||
probe: &mut WaylandProbe,
|
||||
popup_id: u32,
|
||||
xdg_surface_id: u32,
|
||||
) -> Result<u32, String> {
|
||||
let (object_id, opcode, payload) = probe.read_message()?;
|
||||
if object_id != popup_id || opcode != XDG_POPUP_CONFIGURE || payload.len() != 16 {
|
||||
return Err(format!(
|
||||
"unexpected xdg_popup event: object={} opcode={} payload_len={}",
|
||||
object_id,
|
||||
opcode,
|
||||
payload.len()
|
||||
));
|
||||
}
|
||||
|
||||
let (object_id, opcode, payload) = probe.read_message()?;
|
||||
if object_id != xdg_surface_id || opcode != XDG_SURFACE_CONFIGURE || payload.len() != 4 {
|
||||
return Err(format!(
|
||||
"unexpected popup xdg_surface event: object={} opcode={} payload_len={}",
|
||||
object_id,
|
||||
opcode,
|
||||
payload.len()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(u32::from_le_bytes([
|
||||
payload[0], payload[1], payload[2], payload[3],
|
||||
]))
|
||||
}
|
||||
|
||||
fn exercise_shm_pool(probe: &mut WaylandProbe, shm_id: u32, surface_id: u32) -> Result<(), String> {
|
||||
let temp_path = std::env::temp_dir().join(format!(
|
||||
"redbear-compositor-check-{}-{}.shm",
|
||||
@@ -408,21 +579,50 @@ fn check_wayland_socket() -> Result<(), String> {
|
||||
let globals = collect_globals(&mut probe)?;
|
||||
|
||||
let registry_id = 2;
|
||||
let compositor_name = *globals
|
||||
let (compositor_name, _) = *globals
|
||||
.get("wl_compositor")
|
||||
.ok_or_else(|| String::from("wl_compositor global missing"))?;
|
||||
let shm_name = *globals
|
||||
let (shm_name, _) = *globals
|
||||
.get("wl_shm")
|
||||
.ok_or_else(|| String::from("wl_shm global missing"))?;
|
||||
let xdg_name = *globals
|
||||
let (xdg_name, _) = *globals
|
||||
.get("xdg_wm_base")
|
||||
.ok_or_else(|| String::from("xdg_wm_base global missing"))?;
|
||||
let (fixes_name, _) = *globals
|
||||
.get("wl_fixes")
|
||||
.ok_or_else(|| String::from("wl_fixes global missing"))?;
|
||||
let (output_name, output_version) = *globals
|
||||
.get("wl_output")
|
||||
.ok_or_else(|| String::from("wl_output global missing"))?;
|
||||
let (seat_name, seat_version) = *globals
|
||||
.get("wl_seat")
|
||||
.ok_or_else(|| String::from("wl_seat global missing"))?;
|
||||
|
||||
if output_version < 4 {
|
||||
return Err(format!(
|
||||
"wl_output advertised v{output_version}; v4 required for name/description metadata"
|
||||
));
|
||||
}
|
||||
if seat_version < 2 {
|
||||
return Err(format!(
|
||||
"wl_seat advertised v{seat_version}; v2 required for name metadata"
|
||||
));
|
||||
}
|
||||
|
||||
let compositor_id = probe.bind(registry_id, compositor_name, "wl_compositor", 4)?;
|
||||
let shm_id = probe.bind(registry_id, shm_name, "wl_shm", 1)?;
|
||||
let shm_id = probe.bind(registry_id, shm_name, "wl_shm", 2)?;
|
||||
let xdg_wm_base_id = probe.bind(registry_id, xdg_name, "xdg_wm_base", 1)?;
|
||||
let fixes_id = probe.bind(registry_id, fixes_name, "wl_fixes", 2)?;
|
||||
let output_id = probe.bind(registry_id, output_name, "wl_output", 4)?;
|
||||
let seat_id = probe.bind(registry_id, seat_name, "wl_seat", 5)?;
|
||||
|
||||
let mut payload = Vec::new();
|
||||
push_u32(&mut payload, output_name);
|
||||
probe.send_message(fixes_id, WL_FIXES_ACK_GLOBAL_REMOVE, &payload)?;
|
||||
|
||||
expect_shm_formats(&mut probe, shm_id)?;
|
||||
expect_output_metadata(&mut probe, output_id)?;
|
||||
expect_seat_metadata(&mut probe, seat_id)?;
|
||||
|
||||
let surface_id = probe.alloc_id();
|
||||
probe.send_message(
|
||||
@@ -431,6 +631,18 @@ fn check_wayland_socket() -> Result<(), String> {
|
||||
&surface_id.to_le_bytes(),
|
||||
)?;
|
||||
|
||||
let region_id = probe.alloc_id();
|
||||
probe.send_message(
|
||||
compositor_id,
|
||||
WL_COMPOSITOR_CREATE_REGION,
|
||||
®ion_id.to_le_bytes(),
|
||||
)?;
|
||||
let mut payload = Vec::new();
|
||||
push_u32(&mut payload, region_id);
|
||||
probe.send_message(surface_id, WL_SURFACE_SET_OPAQUE_REGION, &payload)?;
|
||||
probe.send_message(region_id, WL_REGION_DESTROY, &[])?;
|
||||
let _ = probe.read_message()?;
|
||||
|
||||
let xdg_surface_id = probe.alloc_id();
|
||||
let mut payload = Vec::new();
|
||||
push_u32(&mut payload, xdg_surface_id);
|
||||
@@ -450,7 +662,53 @@ fn check_wayland_socket() -> Result<(), String> {
|
||||
&serial.to_le_bytes(),
|
||||
)?;
|
||||
|
||||
exercise_shm_pool(&mut probe, shm_id, surface_id)
|
||||
let positioner_id = probe.alloc_id();
|
||||
probe.send_message(
|
||||
xdg_wm_base_id,
|
||||
XDG_WM_BASE_CREATE_POSITIONER,
|
||||
&positioner_id.to_le_bytes(),
|
||||
)?;
|
||||
let mut payload = Vec::new();
|
||||
push_i32(&mut payload, 64);
|
||||
push_i32(&mut payload, 32);
|
||||
probe.send_message(positioner_id, XDG_POSITIONER_SET_SIZE, &payload)?;
|
||||
|
||||
let popup_surface_id = probe.alloc_id();
|
||||
probe.send_message(
|
||||
compositor_id,
|
||||
WL_COMPOSITOR_CREATE_SURFACE,
|
||||
&popup_surface_id.to_le_bytes(),
|
||||
)?;
|
||||
|
||||
let popup_xdg_surface_id = probe.alloc_id();
|
||||
let mut payload = Vec::new();
|
||||
push_u32(&mut payload, popup_xdg_surface_id);
|
||||
push_u32(&mut payload, popup_surface_id);
|
||||
probe.send_message(xdg_wm_base_id, XDG_WM_BASE_GET_XDG_SURFACE, &payload)?;
|
||||
|
||||
let popup_id = probe.alloc_id();
|
||||
let mut payload = Vec::new();
|
||||
push_u32(&mut payload, popup_id);
|
||||
push_u32(&mut payload, xdg_surface_id);
|
||||
push_u32(&mut payload, positioner_id);
|
||||
probe.send_message(popup_xdg_surface_id, XDG_SURFACE_GET_POPUP, &payload)?;
|
||||
let popup_serial = expect_xdg_popup_configure(&mut probe, popup_id, popup_xdg_surface_id)?;
|
||||
probe.send_message(
|
||||
popup_xdg_surface_id,
|
||||
XDG_SURFACE_ACK_CONFIGURE,
|
||||
&popup_serial.to_le_bytes(),
|
||||
)?;
|
||||
|
||||
probe.send_message(popup_id, XDG_POPUP_DESTROY, &[])?;
|
||||
let _ = probe.read_message()?;
|
||||
probe.send_message(positioner_id, XDG_POSITIONER_DESTROY, &[])?;
|
||||
let _ = probe.read_message()?;
|
||||
|
||||
exercise_shm_pool(&mut probe, shm_id, surface_id)?;
|
||||
|
||||
let mut payload = Vec::new();
|
||||
push_u32(&mut payload, registry_id);
|
||||
probe.send_message(fixes_id, WL_FIXES_DESTROY_REGISTRY, &payload)
|
||||
}
|
||||
|
||||
fn check_binaries() -> Result<(), Vec<String>> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
// Integration test: verifies the compositor's Wayland protocol implementation
|
||||
// by starting a real compositor instance and connecting as a client.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::{Child, Command};
|
||||
@@ -39,6 +40,26 @@ fn read_wayland_string(payload: &[u8], cursor: &mut usize) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn collect_globals(client: &mut WaylandClient, registry: u32) -> HashMap<String, (u32, u32)> {
|
||||
let mut globals = HashMap::new();
|
||||
for _ in 0..9 {
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read global failed");
|
||||
assert_eq!(object_id, registry);
|
||||
assert_eq!(opcode, 0); // wl_registry.global
|
||||
let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut cursor = 4;
|
||||
let iface = read_wayland_string(&payload, &mut cursor);
|
||||
let version = u32::from_le_bytes([
|
||||
payload[cursor],
|
||||
payload[cursor + 1],
|
||||
payload[cursor + 2],
|
||||
payload[cursor + 3],
|
||||
]);
|
||||
globals.insert(iface, (name, version));
|
||||
}
|
||||
globals
|
||||
}
|
||||
|
||||
struct WaylandClient {
|
||||
stream: UnixStream,
|
||||
next_id: u32,
|
||||
@@ -155,7 +176,7 @@ fn test_compositor_globals() {
|
||||
|
||||
// Read global events
|
||||
let mut globals = Vec::new();
|
||||
for _ in 0..6 {
|
||||
for _ in 0..9 {
|
||||
match client.read_message() {
|
||||
Ok((_obj_id, opcode, payload)) => {
|
||||
assert_eq!(opcode, 0); // wl_registry.global
|
||||
@@ -192,6 +213,10 @@ fn test_compositor_globals() {
|
||||
globals.iter().any(|(_, i)| i == "xdg_wm_base"),
|
||||
"xdg_wm_base missing"
|
||||
);
|
||||
assert!(
|
||||
globals.iter().any(|(_, i)| i == "wl_fixes"),
|
||||
"wl_fixes missing"
|
||||
);
|
||||
|
||||
compositor.kill().ok();
|
||||
let _ = std::fs::remove_file(socket);
|
||||
@@ -209,7 +234,7 @@ fn test_compositor_shm_formats() {
|
||||
|
||||
// Read globals to find wl_shm name
|
||||
let mut shm_name = 0u32;
|
||||
for _ in 0..6 {
|
||||
for _ in 0..9 {
|
||||
let (_, _, payload) = client.read_message().expect("read failed");
|
||||
let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut cursor = 4;
|
||||
@@ -248,6 +273,327 @@ fn test_compositor_shm_formats() {
|
||||
let _ = std::fs::remove_file(socket);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compositor_wl_fixes_destroy_registry() {
|
||||
let socket = "/tmp/test-redbear-compositor-wl-fixes.sock";
|
||||
let _ = std::fs::remove_file(socket);
|
||||
|
||||
let mut compositor = start_compositor(socket);
|
||||
let mut client = WaylandClient::connect(socket).expect("failed to connect");
|
||||
|
||||
let registry = client.get_registry().expect("get_registry failed");
|
||||
|
||||
let mut fixes_name = 0u32;
|
||||
for _ in 0..9 {
|
||||
let (_, opcode, payload) = client.read_message().expect("read failed");
|
||||
assert_eq!(opcode, 0); // wl_registry.global
|
||||
let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut cursor = 4;
|
||||
let iface = read_wayland_string(&payload, &mut cursor);
|
||||
if iface == "wl_fixes" {
|
||||
fixes_name = name;
|
||||
}
|
||||
}
|
||||
|
||||
assert_ne!(fixes_name, 0, "wl_fixes global not found");
|
||||
|
||||
let fixes = client
|
||||
.bind(registry, fixes_name, "wl_fixes", 2)
|
||||
.expect("bind wl_fixes failed");
|
||||
|
||||
let removed_global_name = 5u32; // wl_output in the compositor's fixed global table.
|
||||
client
|
||||
.send_message(fixes, 2, &removed_global_name.to_le_bytes())
|
||||
.expect("wl_fixes.ack_global_remove failed");
|
||||
client
|
||||
.send_message(fixes, 1, ®istry.to_le_bytes())
|
||||
.expect("wl_fixes.destroy_registry failed");
|
||||
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read delete_id failed");
|
||||
assert_eq!(object_id, 1, "delete_id must be sent by wl_display");
|
||||
assert_eq!(opcode, 2, "expected wl_display.delete_id");
|
||||
assert_eq!(payload, registry.to_le_bytes());
|
||||
|
||||
compositor.kill().ok();
|
||||
let _ = std::fs::remove_file(socket);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compositor_output_and_seat_metadata() {
|
||||
let socket = "/tmp/test-redbear-compositor-output-seat.sock";
|
||||
let _ = std::fs::remove_file(socket);
|
||||
|
||||
let mut compositor = start_compositor(socket);
|
||||
let mut client = WaylandClient::connect(socket).expect("failed to connect");
|
||||
|
||||
let registry = client.get_registry().expect("get_registry failed");
|
||||
let globals = collect_globals(&mut client, registry);
|
||||
|
||||
let (output_name, output_version) = *globals.get("wl_output").expect("wl_output missing");
|
||||
assert!(
|
||||
output_version >= 4,
|
||||
"wl_output must advertise metadata-capable v4"
|
||||
);
|
||||
let output_id = client
|
||||
.bind(registry, output_name, "wl_output", 4)
|
||||
.expect("bind wl_output failed");
|
||||
|
||||
let mut saw_geometry = false;
|
||||
let mut saw_mode = false;
|
||||
let mut saw_scale = false;
|
||||
let mut saw_name = false;
|
||||
let mut saw_description = false;
|
||||
let mut saw_done = false;
|
||||
for _ in 0..6 {
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read output event failed");
|
||||
assert_eq!(object_id, output_id);
|
||||
match opcode {
|
||||
0 => saw_geometry = true,
|
||||
1 => {
|
||||
assert_eq!(payload.len(), 16);
|
||||
saw_mode = true;
|
||||
}
|
||||
2 => saw_done = true,
|
||||
3 => {
|
||||
assert_eq!(payload, 1i32.to_le_bytes());
|
||||
saw_scale = true;
|
||||
}
|
||||
4 => {
|
||||
let mut cursor = 0;
|
||||
assert_eq!(read_wayland_string(&payload, &mut cursor), "RedBear-0");
|
||||
saw_name = true;
|
||||
}
|
||||
5 => {
|
||||
let mut cursor = 0;
|
||||
assert_eq!(
|
||||
read_wayland_string(&payload, &mut cursor),
|
||||
"Red Bear OS framebuffer output"
|
||||
);
|
||||
saw_description = true;
|
||||
}
|
||||
_ => panic!("unexpected wl_output opcode {opcode}"),
|
||||
}
|
||||
}
|
||||
assert!(saw_geometry && saw_mode && saw_scale && saw_name && saw_description && saw_done);
|
||||
|
||||
let (seat_name, seat_version) = *globals.get("wl_seat").expect("wl_seat missing");
|
||||
assert!(seat_version >= 2, "wl_seat must advertise name-capable v2+");
|
||||
let seat_id = client
|
||||
.bind(registry, seat_name, "wl_seat", 5)
|
||||
.expect("bind wl_seat failed");
|
||||
|
||||
let mut saw_capabilities = false;
|
||||
let mut saw_seat_name = false;
|
||||
for _ in 0..2 {
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read seat event failed");
|
||||
assert_eq!(object_id, seat_id);
|
||||
match opcode {
|
||||
0 => {
|
||||
assert_eq!(payload, 1u32.to_le_bytes());
|
||||
saw_capabilities = true;
|
||||
}
|
||||
1 => {
|
||||
let mut cursor = 0;
|
||||
assert_eq!(read_wayland_string(&payload, &mut cursor), "seat0");
|
||||
saw_seat_name = true;
|
||||
}
|
||||
_ => panic!("unexpected wl_seat opcode {opcode}"),
|
||||
}
|
||||
}
|
||||
assert!(saw_capabilities && saw_seat_name);
|
||||
|
||||
client
|
||||
.send_message(output_id, 0, &[])
|
||||
.expect("wl_output.release failed");
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read output delete_id failed");
|
||||
assert_eq!(object_id, 1);
|
||||
assert_eq!(opcode, 2);
|
||||
assert_eq!(payload, output_id.to_le_bytes());
|
||||
|
||||
compositor.kill().ok();
|
||||
let _ = std::fs::remove_file(socket);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compositor_real_surface_opcodes() {
|
||||
let socket = "/tmp/test-redbear-compositor-real-surface.sock";
|
||||
let _ = std::fs::remove_file(socket);
|
||||
|
||||
let mut compositor = start_compositor(socket);
|
||||
let mut client = WaylandClient::connect(socket).expect("failed to connect");
|
||||
|
||||
let registry = client.get_registry().expect("get_registry failed");
|
||||
let mut globals = HashMap::new();
|
||||
for _ in 0..9 {
|
||||
let (_, opcode, payload) = client.read_message().expect("read failed");
|
||||
assert_eq!(opcode, 0); // wl_registry.global
|
||||
let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut cursor = 4;
|
||||
let iface = read_wayland_string(&payload, &mut cursor);
|
||||
globals.insert(iface, name);
|
||||
}
|
||||
|
||||
let compositor_name = *globals
|
||||
.get("wl_compositor")
|
||||
.expect("wl_compositor global missing");
|
||||
let compositor_id = client
|
||||
.bind(registry, compositor_name, "wl_compositor", 4)
|
||||
.expect("bind wl_compositor failed");
|
||||
|
||||
let surface_id = client.alloc_id();
|
||||
client
|
||||
.send_message(compositor_id, 0, &surface_id.to_le_bytes())
|
||||
.expect("wl_compositor.create_surface failed");
|
||||
|
||||
let region_id = client.alloc_id();
|
||||
client
|
||||
.send_message(compositor_id, 1, ®ion_id.to_le_bytes())
|
||||
.expect("wl_compositor.create_region failed");
|
||||
|
||||
client
|
||||
.send_message(surface_id, 4, ®ion_id.to_le_bytes())
|
||||
.expect("wl_surface.set_opaque_region failed");
|
||||
client
|
||||
.send_message(region_id, 0, &[])
|
||||
.expect("wl_region.destroy failed");
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read region delete_id failed");
|
||||
assert_eq!(object_id, 1);
|
||||
assert_eq!(opcode, 2);
|
||||
assert_eq!(payload, region_id.to_le_bytes());
|
||||
|
||||
let callback_id = client.alloc_id();
|
||||
client
|
||||
.send_message(surface_id, 3, &callback_id.to_le_bytes())
|
||||
.expect("wl_surface.frame failed");
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read frame callback failed");
|
||||
assert_eq!(object_id, callback_id);
|
||||
assert_eq!(opcode, 0);
|
||||
assert_eq!(payload.len(), 4);
|
||||
|
||||
client
|
||||
.send_message(surface_id, 6, &[])
|
||||
.expect("wl_surface.commit failed");
|
||||
client
|
||||
.send_message(surface_id, 0, &[])
|
||||
.expect("wl_surface.destroy failed");
|
||||
let (object_id, opcode, payload) = client
|
||||
.read_message()
|
||||
.expect("read surface delete_id failed");
|
||||
assert_eq!(object_id, 1);
|
||||
assert_eq!(opcode, 2);
|
||||
assert_eq!(payload, surface_id.to_le_bytes());
|
||||
|
||||
compositor.kill().ok();
|
||||
let _ = std::fs::remove_file(socket);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compositor_xdg_popup_lifecycle() {
|
||||
let socket = "/tmp/test-redbear-compositor-xdg-popup.sock";
|
||||
let _ = std::fs::remove_file(socket);
|
||||
|
||||
let mut compositor = start_compositor(socket);
|
||||
let mut client = WaylandClient::connect(socket).expect("failed to connect");
|
||||
|
||||
let registry = client.get_registry().expect("get_registry failed");
|
||||
let mut globals = HashMap::new();
|
||||
for _ in 0..9 {
|
||||
let (_, opcode, payload) = client.read_message().expect("read failed");
|
||||
assert_eq!(opcode, 0); // wl_registry.global
|
||||
let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
|
||||
let mut cursor = 4;
|
||||
let iface = read_wayland_string(&payload, &mut cursor);
|
||||
globals.insert(iface, name);
|
||||
}
|
||||
|
||||
let compositor_name = *globals
|
||||
.get("wl_compositor")
|
||||
.expect("wl_compositor global missing");
|
||||
let xdg_name = *globals.get("xdg_wm_base").expect("xdg_wm_base missing");
|
||||
let compositor_id = client
|
||||
.bind(registry, compositor_name, "wl_compositor", 4)
|
||||
.expect("bind wl_compositor failed");
|
||||
let xdg_wm_base_id = client
|
||||
.bind(registry, xdg_name, "xdg_wm_base", 1)
|
||||
.expect("bind xdg_wm_base failed");
|
||||
|
||||
let parent_surface = client.alloc_id();
|
||||
client
|
||||
.send_message(compositor_id, 0, &parent_surface.to_le_bytes())
|
||||
.expect("create parent surface failed");
|
||||
let parent_xdg = client.alloc_id();
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&parent_xdg.to_le_bytes());
|
||||
payload.extend_from_slice(&parent_surface.to_le_bytes());
|
||||
client
|
||||
.send_message(xdg_wm_base_id, 2, &payload)
|
||||
.expect("get parent xdg_surface failed");
|
||||
|
||||
let positioner = client.alloc_id();
|
||||
client
|
||||
.send_message(xdg_wm_base_id, 1, &positioner.to_le_bytes())
|
||||
.expect("create_positioner failed");
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&64i32.to_le_bytes());
|
||||
payload.extend_from_slice(&32i32.to_le_bytes());
|
||||
client
|
||||
.send_message(positioner, 1, &payload)
|
||||
.expect("positioner.set_size failed");
|
||||
|
||||
let popup_surface = client.alloc_id();
|
||||
client
|
||||
.send_message(compositor_id, 0, &popup_surface.to_le_bytes())
|
||||
.expect("create popup surface failed");
|
||||
let popup_xdg = client.alloc_id();
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&popup_xdg.to_le_bytes());
|
||||
payload.extend_from_slice(&popup_surface.to_le_bytes());
|
||||
client
|
||||
.send_message(xdg_wm_base_id, 2, &payload)
|
||||
.expect("get popup xdg_surface failed");
|
||||
|
||||
let popup = client.alloc_id();
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&popup.to_le_bytes());
|
||||
payload.extend_from_slice(&parent_xdg.to_le_bytes());
|
||||
payload.extend_from_slice(&positioner.to_le_bytes());
|
||||
client
|
||||
.send_message(popup_xdg, 2, &payload)
|
||||
.expect("xdg_surface.get_popup failed");
|
||||
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read popup configure failed");
|
||||
assert_eq!(object_id, popup);
|
||||
assert_eq!(opcode, 0);
|
||||
assert_eq!(payload.len(), 16);
|
||||
let (object_id, opcode, payload) = client
|
||||
.read_message()
|
||||
.expect("read popup surface configure failed");
|
||||
assert_eq!(object_id, popup_xdg);
|
||||
assert_eq!(opcode, 0);
|
||||
assert_eq!(payload.len(), 4);
|
||||
|
||||
client
|
||||
.send_message(popup, 0, &[])
|
||||
.expect("xdg_popup.destroy failed");
|
||||
let (object_id, opcode, payload) = client.read_message().expect("read popup delete_id failed");
|
||||
assert_eq!(object_id, 1);
|
||||
assert_eq!(opcode, 2);
|
||||
assert_eq!(payload, popup.to_le_bytes());
|
||||
|
||||
client
|
||||
.send_message(positioner, 0, &[])
|
||||
.expect("xdg_positioner.destroy failed");
|
||||
let (object_id, opcode, payload) = client
|
||||
.read_message()
|
||||
.expect("read positioner delete_id failed");
|
||||
assert_eq!(object_id, 1);
|
||||
assert_eq!(opcode, 2);
|
||||
assert_eq!(payload, positioner.to_le_bytes());
|
||||
|
||||
compositor.kill().ok();
|
||||
let _ = std::fs::remove_file(socket);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compositor_sync_roundtrip() {
|
||||
let socket = "/tmp/test-redbear-compositor-sync.sock";
|
||||
|
||||
Reference in New Issue
Block a user