diff --git a/src/calloop.rs b/src/calloop.rs index 3c38372..3cf2db1 100644 --- a/src/calloop.rs +++ b/src/calloop.rs @@ -140,7 +140,7 @@ fn handle_result( } else { eis::connection::DisconnectReason::Protocol }; - connection.disconnected(reason, &err.to_string()); + connection.disconnected(reason, Some(&err.to_string())); let _ = connection.flush(); } @@ -168,7 +168,7 @@ fn process_handshake( if !handle.has_interface("ei_seat") || !handle.has_interface("ei_device") { handle.disconnected( eis::connection::DisconnectReason::Protocol, - "Need `ei_seat` and `ei_device`", + Some("Need `ei_seat` and `ei_device`"), ); let _ = request_converter.handle().flush(); return Err(HandshakeError::MissingInterface.into()); diff --git a/src/eiproto.rs.jinja b/src/eiproto.rs.jinja index 88c066b..75b3a8e 100644 --- a/src/eiproto.rs.jinja +++ b/src/eiproto.rs.jinja @@ -22,7 +22,7 @@ // GENERATED FILE -{# TODO handle context_type #} +{# TODO More complete handling of nullable types? #} use crate::wire; @@ -32,7 +32,9 @@ use crate::wire; {%- macro arg_type(arg, owned, generic) -%} {%- if arg.enum != None -%} {{ arg.enum.name|camel }} + {%- elif arg.protocol_type == 'string' and owned and arg.allow_null -%} Option {%- elif arg.protocol_type == 'string' and owned -%} String + {%- elif arg.protocol_type == 'string' and arg.allow_null -%} Option<&str> {%- elif arg.protocol_type == 'string' -%} &str {%- elif arg.protocol_type == 'int32' -%} i32 {%- elif arg.protocol_type == 'uint32' -%} u32 @@ -182,7 +184,7 @@ pub mod {{interface.plainname}} { let args = &[ {%- for arg in outgoing.arguments %} {% if arg.interface_arg_for %} - wire::Arg::{{arg.protocol_type|camel}}({{arg.name|camel}}::NAME), + wire::Arg::{{arg.protocol_type|camel}}(Some({{arg.name|camel}}::NAME)), {% else %} wire::Arg::{{arg.protocol_type|camel}}({{arg.name}} {% if arg.protocol_type == 'new_id' %} diff --git a/src/eiproto_ei.rs b/src/eiproto_ei.rs index 5c6eae9..6efcb2d 100644 --- a/src/eiproto_ei.rs +++ b/src/eiproto_ei.rs @@ -542,7 +542,7 @@ pub mod connection { /// The reason for being disconnected. reason: DisconnectReason, /// An explanation for debugging purposes. - explanation: String, + explanation: Option, }, /// Seat presence information. /// diff --git a/src/eiproto_eis.rs b/src/eiproto_eis.rs index d25420c..e71f70b 100644 --- a/src/eiproto_eis.rs +++ b/src/eiproto_eis.rs @@ -476,7 +476,7 @@ pub mod connection { &self, last_serial: u32, reason: DisconnectReason, - explanation: &str, + explanation: Option<&str>, ) -> () { let args = &[ wire::Arg::Uint32(last_serial.into()), @@ -1478,7 +1478,7 @@ pub mod device { .new_object(InterfaceName::NAME.to_string(), version); let args = &[ wire::Arg::NewId(object.id().into()), - wire::Arg::String(InterfaceName::NAME), + wire::Arg::String(Some(InterfaceName::NAME)), wire::Arg::Uint32(version.into()), ]; diff --git a/src/event.rs b/src/event.rs index b157208..955af9b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -976,7 +976,7 @@ pub struct Disconnected { /// Reason for disconnection. pub reason: ei::connection::DisconnectReason, /// Explanation for debugging purposes. - pub explanation: String, + pub explanation: Option, } /// High-level translation of the seat description events ending with [`ei_seat.done`](ei::seat::Event::Done). diff --git a/src/request.rs b/src/request.rs index 1c355d4..32a0847 100644 --- a/src/request.rs +++ b/src/request.rs @@ -75,8 +75,7 @@ impl Connection { /// /// Will panic if an internal Mutex is poisoned. // TODO(axka, 2025-07-08): rename to something imperative like `notify_disconnection` - // TODO(axka, 2025-07-08): `explanation` must support NULL: https://gitlab.freedesktop.org/libinput/libei/-/commit/267716a7609730914b24adf5860ec8d2cf2e7636 - pub fn disconnected(&self, reason: DisconnectReason, explanation: &str) { + pub fn disconnected(&self, reason: DisconnectReason, explanation: Option<&str>) { let seats = self .0 .seats @@ -393,6 +392,7 @@ impl EisRequestConverter { self.handle_touchscreen_request(touchscreen, request)?; } } + Ok(()) } diff --git a/src/wire/arg.rs b/src/wire/arg.rs index 064a3d1..408efe6 100644 --- a/src/wire/arg.rs +++ b/src/wire/arg.rs @@ -19,7 +19,7 @@ pub enum Arg<'a> { Int64(i64), Float(f32), Fd(BorrowedFd<'a>), - String(&'a str), + String(Option<&'a str>), NewId(u64), Id(u64), } @@ -56,7 +56,10 @@ impl Arg<'_> { Arg::Float(value) => buf.extend(value.to_ne_bytes()), // XXX unwrap? Arg::Fd(value) => fds.extend([value.try_clone_to_owned().unwrap()]), - Arg::String(value) => { + Arg::String(None) => { + buf.extend(0u32.to_ne_bytes()); + } + Arg::String(Some(value)) => { // Write 32-bit length, including NUL let len = value.len() as u32 + 1; buf.extend(len.to_ne_bytes()); @@ -145,11 +148,11 @@ impl OwnedArg for OwnedFd { } } -impl OwnedArg for String { +impl OwnedArg for Option { fn parse(buf: &mut ByteStream) -> Result { let mut len = u32::parse(buf)?; if len == 0 { - return Ok(String::new()); + return Ok(None); } let bytes = buf.read_n(len as usize - 1)?; // Exclude NUL let string = String::from_utf8(bytes.collect())?; @@ -159,11 +162,21 @@ impl OwnedArg for String { len += 1; buf.read::<1>()?; } - Ok(string) + Ok(Some(string)) + } + + fn as_arg(&self) -> Arg<'_> { + Arg::String(self.as_deref()) + } +} + +impl OwnedArg for String { + fn parse(buf: &mut ByteStream) -> Result { + Option::::parse(buf)?.ok_or(ParseError::InvalidNull) } fn as_arg(&self) -> Arg<'_> { - Arg::String(self) + Arg::String(Some(self)) } } diff --git a/src/wire/mod.rs b/src/wire/mod.rs index 89d533a..c340ad9 100644 --- a/src/wire/mod.rs +++ b/src/wire/mod.rs @@ -122,6 +122,8 @@ pub enum ParseError { HeaderLength(u32), /// Message length didn't match header. MessageLength(u32, u32), + /// NULL for non-nullable argument + InvalidNull, } impl fmt::Display for ParseError { @@ -142,6 +144,9 @@ impl fmt::Display for ParseError { Self::MessageLength(a, b) => { write!(f, "message length didn't match header ({a} != {b})") } + Self::InvalidNull => { + write!(f, "NULL value for non-nullable argument") + } } } }