fix(deps): backport secure Wayland XML parser

This commit is contained in:
2026-07-10 00:37:49 -04:00
parent fcc8443c45
commit 670f7abe60
24 changed files with 6886 additions and 5 deletions
+172
View File
@@ -0,0 +1,172 @@
use std::cmp;
use proc_macro2::{Literal, TokenStream};
use quote::{format_ident, quote};
use crate::protocol::{Interface, Message, Protocol, Type};
pub(crate) fn generate_interfaces_prefix(protocol: &Protocol) -> TokenStream {
let longest_nulls = protocol.interfaces.iter().fold(0, |max, interface| {
let request_longest_null = interface.requests.iter().fold(0, |max, request| {
if request.all_null() {
cmp::max(request.args.len(), max)
} else {
max
}
});
let events_longest_null = interface.events.iter().fold(0, |max, event| {
if event.all_null() {
cmp::max(event.args.len(), max)
} else {
max
}
});
cmp::max(max, cmp::max(request_longest_null, events_longest_null))
});
let types_null_len = Literal::usize_unsuffixed(longest_nulls);
quote! {
use std::ptr::null;
struct SyncWrapper<T>(T);
unsafe impl<T> Sync for SyncWrapper<T> {}
static types_null: SyncWrapper<[*const wayland_backend::protocol::wl_interface; #types_null_len]> = SyncWrapper([
null::<wayland_backend::protocol::wl_interface>(); #types_null_len
]);
}
}
pub(crate) fn generate_interface(interface: &Interface) -> TokenStream {
let requests = gen_messages(interface, &interface.requests, "requests");
let events = gen_messages(interface, &interface.events, "events");
let interface_ident = format_ident!("{}_interface", interface.name);
let name_value = null_terminated_byte_string_literal(&interface.name);
let version_value = Literal::i32_unsuffixed(interface.version as i32);
let request_count_value = Literal::i32_unsuffixed(interface.requests.len() as i32);
let requests_value = if interface.requests.is_empty() {
quote! { null::<wayland_backend::protocol::wl_message>() }
} else {
let requests_ident = format_ident!("{}_requests", interface.name);
quote! { #requests_ident.0.as_ptr() }
};
let event_count_value = Literal::i32_unsuffixed(interface.events.len() as i32);
let events_value = if interface.events.is_empty() {
quote! { null::<wayland_backend::protocol::wl_message>() }
} else {
let events_ident = format_ident!("{}_events", interface.name);
quote! { #events_ident.0.as_ptr() }
};
quote! {
#requests
#events
pub static #interface_ident: wayland_backend::protocol::wl_interface = wayland_backend::protocol::wl_interface {
name: #name_value as *const u8 as *const std::os::raw::c_char,
version: #version_value,
request_count: #request_count_value,
requests: #requests_value,
event_count: #event_count_value,
events: #events_value,
};
}
}
fn gen_messages(interface: &Interface, messages: &[Message], which: &str) -> TokenStream {
if messages.is_empty() {
return TokenStream::new();
}
let types_arrays = messages.iter().filter_map(|msg| {
if msg.all_null() {
None
} else {
let array_ident = format_ident!("{}_{}_{}_types", interface.name, which, msg.name);
let array_len = Literal::usize_unsuffixed(msg.args.len());
let array_values = msg.args.iter().map(|arg| match (arg.typ, &arg.interface) {
(Type::Object, &Some(ref inter)) | (Type::NewId, &Some(ref inter)) => {
let interface_ident =format_ident!("{}_interface", inter);
quote! { &#interface_ident as *const wayland_backend::protocol::wl_interface }
}
_ => quote! { null::<wayland_backend::protocol::wl_interface>() },
});
Some(quote! {
static #array_ident: SyncWrapper<[*const wayland_backend::protocol::wl_interface; #array_len]> = SyncWrapper([
#(#array_values,)*
]);
})
}
});
let message_array_ident = format_ident!("{}_{}", interface.name, which);
let message_array_len = Literal::usize_unsuffixed(messages.len());
let message_array_values = messages.iter().map(|msg| {
let name_value = null_terminated_byte_string_literal(&msg.name);
let signature_value = Literal::byte_string(&message_signature(msg));
let types_ident = if msg.all_null() {
format_ident!("types_null")
} else {
format_ident!("{}_{}_{}_types", interface.name, which, msg.name)
};
quote! {
wayland_backend::protocol::wl_message {
name: #name_value as *const u8 as *const std::os::raw::c_char,
signature: #signature_value as *const u8 as *const std::os::raw::c_char,
types: #types_ident.0.as_ptr(),
}
}
});
quote! {
#(#types_arrays)*
static #message_array_ident: SyncWrapper<[wayland_backend::protocol::wl_message; #message_array_len]> = SyncWrapper([
#(#message_array_values,)*
]);
}
}
fn message_signature(msg: &Message) -> Vec<u8> {
let mut res = Vec::new();
if msg.since > 1 {
res.extend_from_slice(msg.since.to_string().as_bytes());
}
for arg in &msg.args {
if arg.typ.nullable() && arg.allow_null {
res.push(b'?');
}
match arg.typ {
Type::NewId => {
if arg.interface.is_none() {
res.extend_from_slice(b"su");
}
res.push(b'n');
}
Type::Uint => res.push(b'u'),
Type::Fixed => res.push(b'f'),
Type::String => res.push(b's'),
Type::Object => res.push(b'o'),
Type::Array => res.push(b'a'),
Type::Fd => res.push(b'h'),
Type::Int => res.push(b'i'),
_ => {}
}
}
res.push(0);
res
}
pub fn null_terminated_byte_string_literal(string: &str) -> Literal {
let mut val = Vec::with_capacity(string.len() + 1);
val.extend_from_slice(string.as_bytes());
val.push(0);
Literal::byte_string(&val)
}
+337
View File
@@ -0,0 +1,337 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};
use crate::{
protocol::{Interface, Protocol, Type},
util::{description_to_doc_attr, dotted_to_relname, is_keyword, snake_to_camel, to_doc_attr},
Side,
};
pub fn generate_client_objects(protocol: &Protocol) -> TokenStream {
protocol.interfaces.iter().map(generate_objects_for).collect()
}
fn generate_objects_for(interface: &Interface) -> TokenStream {
let mod_name = Ident::new(&interface.name, Span::call_site());
let mod_doc = interface.description.as_ref().map(description_to_doc_attr);
let iface_name = Ident::new(&snake_to_camel(&interface.name), Span::call_site());
let iface_const_name = format_ident!("{}_INTERFACE", interface.name.to_ascii_uppercase());
let enums = crate::common::generate_enums_for(interface);
let sinces = crate::common::gen_msg_constants(&interface.requests, &interface.events);
let requests = crate::common::gen_message_enum(
&format_ident!("Request"),
Side::Client,
false,
&interface.requests,
);
let events = crate::common::gen_message_enum(
&format_ident!("Event"),
Side::Client,
true,
&interface.events,
);
let parse_body = crate::message_io::gen_parse_body(interface, Side::Client);
let write_body = crate::message_io::gen_write_body(interface, Side::Client);
let methods = gen_methods(interface);
let event_ref = if interface.events.is_empty() {
"This interface has no events."
} else {
"See also the [Event] enum for this interface."
};
let docs = match &interface.description {
Some((short, long)) => format!("{short}\n\n{long}\n\n{event_ref}"),
None => format!("{}\n\n{}", interface.name, event_ref),
};
let doc_attr = to_doc_attr(&docs);
quote! {
#mod_doc
pub mod #mod_name {
use std::sync::Arc;
use std::os::unix::io::OwnedFd;
use super::wayland_client::{
backend::{
Backend, WeakBackend, smallvec, ObjectData, ObjectId, InvalidId,
protocol::{WEnum, Argument, Message, Interface, same_interface}
},
QueueProxyData, Proxy, Connection, Dispatch, QueueHandle, DispatchError, Weak,
};
#enums
#sinces
#requests
#events
#doc_attr
#[derive(Debug, Clone)]
pub struct #iface_name {
id: ObjectId,
version: u32,
data: Option<Arc<dyn ObjectData>>,
backend: WeakBackend,
}
impl std::cmp::PartialEq for #iface_name {
fn eq(&self, other: &#iface_name) -> bool {
self.id == other.id
}
}
impl std::cmp::Eq for #iface_name {}
impl PartialEq<Weak<#iface_name>> for #iface_name {
fn eq(&self, other: &Weak<#iface_name>) -> bool {
self.id == other.id()
}
}
impl std::borrow::Borrow<ObjectId> for #iface_name {
fn borrow(&self) -> &ObjectId {
&self.id
}
}
impl std::hash::Hash for #iface_name {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state)
}
}
impl super::wayland_client::Proxy for #iface_name {
type Request<'request> = Request<'request>;
type Event = Event;
#[inline]
fn interface() -> &'static Interface{
&super::#iface_const_name
}
#[inline]
fn id(&self) -> ObjectId {
self.id.clone()
}
#[inline]
fn version(&self) -> u32 {
self.version
}
#[inline]
fn data<U: Send + Sync + 'static>(&self) -> Option<&U> {
self.data.as_ref().and_then(|arc| arc.data_as_any().downcast_ref::<U>())
}
fn object_data(&self) -> Option<&Arc<dyn ObjectData>> {
self.data.as_ref()
}
fn backend(&self) -> &WeakBackend {
&self.backend
}
fn send_request(&self, req: Self::Request<'_>) -> Result<(), InvalidId> {
let conn = Connection::from_backend(self.backend.upgrade().ok_or(InvalidId)?);
let id = conn.send_request(self, req, None)?;
debug_assert!(id.is_null());
Ok(())
}
fn send_constructor<I: Proxy>(&self, req: Self::Request<'_>, data: Arc<dyn ObjectData>) -> Result<I, InvalidId> {
let conn = Connection::from_backend(self.backend.upgrade().ok_or(InvalidId)?);
let id = conn.send_request(self, req, Some(data))?;
Proxy::from_id(&conn, id)
}
#[inline]
fn from_id(conn: &Connection, id: ObjectId) -> Result<Self, InvalidId> {
if !same_interface(id.interface(), Self::interface()) && !id.is_null() {
return Err(InvalidId);
}
let version = conn.object_info(id.clone()).map(|info| info.version).unwrap_or(0);
let data = conn.get_object_data(id.clone()).ok();
let backend = conn.backend().downgrade();
Ok(#iface_name { id, data, version, backend })
}
#[inline]
fn inert(backend: WeakBackend) -> Self {
#iface_name { id: ObjectId::null(), data: None, version: 0, backend }
}
fn parse_event(conn: &Connection, msg: Message<ObjectId, OwnedFd>) -> Result<(Self, Self::Event), DispatchError> {
#parse_body
}
fn write_request<'a>(&self, conn: &Connection, msg: Self::Request<'a>) -> Result<(Message<ObjectId, std::os::unix::io::BorrowedFd<'a>>, Option<(&'static Interface, u32)>), InvalidId> {
#write_body
}
}
impl #iface_name {
#methods
}
}
}
}
fn gen_methods(interface: &Interface) -> TokenStream {
interface.requests.iter().map(|request| {
let created_interface = request.args.iter().find(|arg| arg.typ == Type::NewId).map(|arg| &arg.interface);
let method_name = format_ident!("{}{}", if is_keyword(&request.name) { "_" } else { "" }, request.name);
let enum_variant = Ident::new(&snake_to_camel(&request.name), Span::call_site());
let fn_args = request.args.iter().flat_map(|arg| {
if arg.typ == Type::NewId {
if arg.interface.is_none() {
// the new_id argument of a bind-like method
// it shoudl expand as a (interface, type) tuple, but the type is already handled by
// the prototype type parameter, so just put a version here
return Some(quote! { version: u32 });
} else {
// this is a regular new_id, skip it
return None;
}
}
let arg_name = format_ident!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name);
let arg_type = if let Some(ref enu) = arg.enum_ {
let enum_type = dotted_to_relname(enu);
quote! { #enum_type }
} else {
match arg.typ {
Type::Uint => quote! { u32 },
Type::Int => quote! { i32 },
Type::Fixed => quote! { f64 },
Type::String => if arg.allow_null { quote!{ Option<String> } } else { quote!{ String } },
Type::Array => if arg.allow_null { quote!{ Option<Vec<u8>> } } else { quote!{ Vec<u8> } },
Type::Fd => quote! { ::std::os::unix::io::BorrowedFd<'_> },
Type::Object => {
let iface = arg.interface.as_ref().unwrap();
let iface_mod = Ident::new(iface, Span::call_site());
let iface_type =
Ident::new(&snake_to_camel(iface), Span::call_site());
if arg.allow_null { quote! { Option<&super::#iface_mod::#iface_type> } } else { quote! { &super::#iface_mod::#iface_type } }
},
Type::NewId => unreachable!(),
Type::Destructor => panic!("An argument cannot have type \"destructor\"."),
}
};
Some(quote! {
#arg_name: #arg_type
})
});
let enum_args = request.args.iter().flat_map(|arg| {
let arg_name = format_ident!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name);
if arg.enum_.is_some() {
Some(quote! { #arg_name: WEnum::Value(#arg_name) })
} else if arg.typ == Type::NewId {
if arg.interface.is_none() {
Some(quote! { #arg_name: (I::interface(), version) })
} else {
None
}
} else if arg.typ == Type::Object {
if arg.allow_null {
Some(quote! { #arg_name: #arg_name.cloned() })
} else {
Some(quote! { #arg_name: #arg_name.clone() })
}
} else {
Some(quote! { #arg_name })
}
});
let doc_attr = request
.description
.as_ref()
.map(description_to_doc_attr);
match created_interface {
Some(Some(ref created_interface)) => {
// a regular creating request
let created_iface_mod = Ident::new(created_interface, Span::call_site());
let created_iface_type = Ident::new(&snake_to_camel(created_interface), Span::call_site());
quote! {
#doc_attr
#[allow(clippy::too_many_arguments)]
pub fn #method_name<U: Send + Sync + 'static, D: Dispatch<super::#created_iface_mod::#created_iface_type, U> + 'static>(&self, #(#fn_args,)* qh: &QueueHandle<D>, udata: U) -> super::#created_iface_mod::#created_iface_type {
self.send_constructor(
Request::#enum_variant {
#(#enum_args),*
},
qh.make_data::<super::#created_iface_mod::#created_iface_type, U>(udata),
).unwrap_or_else(|_| Proxy::inert(self.backend.clone()))
}
}
},
Some(None) => {
// a bind-like request
quote! {
#doc_attr
#[allow(clippy::too_many_arguments)]
pub fn #method_name<I: Proxy + 'static, U: Send + Sync + 'static, D: Dispatch<I, U> + 'static>(&self, #(#fn_args,)* qh: &QueueHandle<D>, udata: U) -> I {
self.send_constructor(
Request::#enum_variant {
#(#enum_args),*
},
qh.make_data::<I, U>(udata),
).unwrap_or_else(|_| Proxy::inert(self.backend.clone()))
}
}
},
None => {
// a non-creating request
quote! {
#doc_attr
#[allow(clippy::too_many_arguments)]
pub fn #method_name(&self, #(#fn_args),*) {
let backend = match self.backend.upgrade() {
Some(b) => b,
None => return,
};
let conn = Connection::from_backend(backend);
let _ = conn.send_request(
self,
Request::#enum_variant {
#(#enum_args),*
},
None
);
}
}
}
}
}).collect()
}
#[cfg(test)]
mod tests {
#[test]
fn client_gen() {
let protocol_file =
std::fs::File::open("./tests/scanner_assets/test-protocol.xml").unwrap();
let protocol_parsed = crate::parse::parse(protocol_file);
let generated: String = super::generate_client_objects(&protocol_parsed).to_string();
let generated = crate::format_rust_code(&generated);
let reference =
std::fs::read_to_string("./tests/scanner_assets/test-client-code.rs").unwrap();
let reference = crate::format_rust_code(&reference);
if reference != generated {
let diff = similar::TextDiff::from_lines(&reference, &generated);
print!("{}", diff.unified_diff().context_radius(10).header("reference", "generated"));
panic!("Generated does not match reference!")
}
}
}
+330
View File
@@ -0,0 +1,330 @@
use std::fmt::Write;
use proc_macro2::{Ident, Literal, Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use crate::{protocol::*, util::*, Side};
pub(crate) fn generate_enums_for(interface: &Interface) -> TokenStream {
interface.enums.iter().map(ToTokens::into_token_stream).collect()
}
impl ToTokens for Enum {
fn to_tokens(&self, tokens: &mut TokenStream) {
let enum_decl;
let enum_impl;
let doc_attr = self.description.as_ref().map(description_to_doc_attr);
let ident = Ident::new(&snake_to_camel(&self.name), Span::call_site());
if self.bitfield {
let entries = self.entries.iter().map(|entry| {
let doc_attr = entry
.description
.as_ref()
.map(description_to_doc_attr)
.or_else(|| entry.summary.as_ref().map(|s| to_doc_attr(s)));
let prefix = if entry.name.chars().next().unwrap().is_numeric() { "_" } else { "" };
let ident = format_ident!("{}{}", prefix, snake_to_camel(&entry.name));
let value = Literal::u32_unsuffixed(entry.value);
quote! {
#doc_attr
const #ident = #value;
}
});
enum_decl = quote! {
bitflags::bitflags! {
#doc_attr
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct #ident: u32 {
#(#entries)*
}
}
};
enum_impl = quote! {
impl std::convert::TryFrom<u32> for #ident {
type Error = ();
fn try_from(val: u32) -> Result<#ident, ()> {
#ident::from_bits(val).ok_or(())
}
}
impl std::convert::From<#ident> for u32 {
fn from(val: #ident) -> u32 {
val.bits()
}
}
};
} else {
let variants = self.entries.iter().map(|entry| {
let doc_attr = entry
.description
.as_ref()
.map(description_to_doc_attr)
.or_else(|| entry.summary.as_ref().map(|s| to_doc_attr(s)));
let prefix = if entry.name.chars().next().unwrap().is_numeric() { "_" } else { "" };
let variant = format_ident!("{}{}", prefix, snake_to_camel(&entry.name));
let value = Literal::u32_unsuffixed(entry.value);
quote! {
#doc_attr
#variant = #value
}
});
enum_decl = quote! {
#doc_attr
#[repr(u32)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum #ident {
#(#variants,)*
}
};
let match_arms = self.entries.iter().map(|entry| {
let value = Literal::u32_unsuffixed(entry.value);
let prefix = if entry.name.chars().next().unwrap().is_numeric() { "_" } else { "" };
let variant = format_ident!("{}{}", prefix, snake_to_camel(&entry.name));
quote! {
#value => Ok(#ident::#variant)
}
});
enum_impl = quote! {
impl std::convert::TryFrom<u32> for #ident {
type Error = ();
fn try_from(val: u32) -> Result<#ident, ()> {
match val {
#(#match_arms,)*
_ => Err(())
}
}
}
impl std::convert::From<#ident> for u32 {
fn from(val: #ident) -> u32 {
val as u32
}
}
};
}
enum_decl.to_tokens(tokens);
enum_impl.to_tokens(tokens);
}
}
pub(crate) fn gen_msg_constants(requests: &[Message], events: &[Message]) -> TokenStream {
let req_constants = requests.iter().enumerate().map(|(opcode, msg)| {
let since_cstname = format_ident!("REQ_{}_SINCE", msg.name.to_ascii_uppercase());
let opcode_cstname = format_ident!("REQ_{}_OPCODE", msg.name.to_ascii_uppercase());
let since = msg.since;
let opcode = opcode as u16;
quote! {
/// The minimal object version supporting this request
pub const #since_cstname: u32 = #since;
/// The wire opcode for this request
pub const #opcode_cstname: u16 = #opcode;
}
});
let evt_constants = events.iter().enumerate().map(|(opcode, msg)| {
let since_cstname = format_ident!("EVT_{}_SINCE", msg.name.to_ascii_uppercase());
let opcode_cstname = format_ident!("EVT_{}_OPCODE", msg.name.to_ascii_uppercase());
let since = msg.since;
let opcode = opcode as u16;
quote! {
/// The minimal object version supporting this event
pub const #since_cstname: u32 = #since;
/// The wire opcode for this event
pub const #opcode_cstname: u16 = #opcode;
}
});
quote! {
#(#req_constants)*
#(#evt_constants)*
}
}
pub(crate) fn gen_message_enum(
name: &Ident,
side: Side,
receiver: bool,
messages: &[Message],
) -> TokenStream {
let variants = messages
.iter()
.map(|msg| {
let mut docs = String::new();
if let Some((ref short, ref long)) = msg.description {
write!(docs, "{}\n\n{}\n", short, long.trim()).unwrap();
}
if let Some(Type::Destructor) = msg.typ {
write!(
docs,
"\nThis is a destructor, once {} this object cannot be used any longer.",
if receiver { "received" } else { "sent" },
)
.unwrap()
}
if msg.since > 1 {
write!(docs, "\nOnly available since version {} of the interface", msg.since)
.unwrap();
}
let doc_attr = to_doc_attr(&docs);
let msg_name = Ident::new(&snake_to_camel(&msg.name), Span::call_site());
let msg_variant_decl =
if msg.args.is_empty() {
msg_name.into_token_stream()
} else {
let fields = msg.args.iter().flat_map(|arg| {
let field_name =
format_ident!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name);
let field_type_inner = if let Some(ref enu) = arg.enum_ {
let enum_type = dotted_to_relname(enu);
quote! { WEnum<#enum_type> }
} else {
match arg.typ {
Type::Uint => quote! { u32 },
Type::Int => quote! { i32 },
Type::Fixed => quote! { f64 },
Type::String => quote! { String },
Type::Array => quote! { Vec<u8> },
Type::Fd => {
if receiver {
quote! { OwnedFd }
} else {
quote! { std::os::unix::io::BorrowedFd<'a> }
}
}
Type::Object => {
if let Some(ref iface) = arg.interface {
let iface_mod = Ident::new(iface, Span::call_site());
let iface_type =
Ident::new(&snake_to_camel(iface), Span::call_site());
quote! { super::#iface_mod::#iface_type }
} else if side == Side::Client {
quote! { super::wayland_client::ObjectId }
} else {
quote! { super::wayland_server::ObjectId }
}
}
Type::NewId if !receiver && side == Side::Client => {
// Client-side sending does not have a pre-existing object
// so skip serializing it
if arg.interface.is_some() {
return None;
} else {
quote! { (&'static Interface, u32) }
}
}
Type::NewId => {
if let Some(ref iface) = arg.interface {
let iface_mod = Ident::new(iface, Span::call_site());
let iface_type =
Ident::new(&snake_to_camel(iface), Span::call_site());
if receiver && side == Side::Server {
quote! { New<super::#iface_mod::#iface_type> }
} else {
quote! { super::#iface_mod::#iface_type }
}
} else {
// bind-like function
if side == Side::Client {
quote! { (String, u32, super::wayland_client::ObjectId) }
} else {
quote! { (String, u32, super::wayland_server::ObjectId) }
}
}
}
Type::Destructor => panic!("An argument cannot have type \"destructor\"."),
}
};
let field_type = if arg.allow_null {
quote! { Option<#field_type_inner> }
} else {
field_type_inner.into_token_stream()
};
let doc_attr = arg
.description
.as_ref()
.map(description_to_doc_attr)
.or_else(|| arg.summary.as_ref().map(|s| to_doc_attr(s)));
Some(quote! {
#doc_attr
#field_name: #field_type
})
});
quote! {
#msg_name {
#(#fields,)*
}
}
};
quote! {
#doc_attr
#msg_variant_decl
}
})
.collect::<Vec<_>>();
let opcodes = messages.iter().enumerate().map(|(opcode, msg)| {
let msg_name = Ident::new(&snake_to_camel(&msg.name), Span::call_site());
let opcode = opcode as u16;
if msg.args.is_empty() {
quote! {
#name::#msg_name => #opcode
}
} else {
quote! {
#name::#msg_name { .. } => #opcode
}
}
});
// Placeholder to allow generic argument to be added later, without ABI
// break.
// TODO Use never type.
let (generic, phantom_variant, phantom_case) = if !receiver {
(
quote! { 'a },
quote! { #[doc(hidden)] __phantom_lifetime { phantom: std::marker::PhantomData<&'a ()>, never: std::convert::Infallible } },
quote! { #name::__phantom_lifetime { never, .. } => match never {} },
)
} else {
(quote! {}, quote! {}, quote! {})
};
quote! {
#[derive(Debug)]
#[non_exhaustive]
pub enum #name<#generic> {
#(#variants,)*
#phantom_variant
}
impl<#generic> #name<#generic> {
#[doc="Get the opcode number of this message"]
pub fn opcode(&self) -> u16 {
match *self {
#(#opcodes,)*
#phantom_case
}
}
}
}
}
+142
View File
@@ -0,0 +1,142 @@
use proc_macro2::TokenStream;
use crate::protocol::{Interface, Message, Protocol, Type};
use quote::{format_ident, quote};
pub fn generate(protocol: &Protocol, with_c_interfaces: bool) -> TokenStream {
let interfaces =
protocol.interfaces.iter().map(|iface| generate_interface(iface, with_c_interfaces));
if with_c_interfaces {
let prefix = super::c_interfaces::generate_interfaces_prefix(protocol);
quote! {
#prefix
#(#interfaces)*
}
} else {
interfaces.collect()
}
}
pub(crate) fn generate_interface(interface: &Interface, with_c: bool) -> TokenStream {
let const_name = format_ident!("{}_INTERFACE", interface.name.to_ascii_uppercase());
let iface_name = &interface.name;
let iface_version = interface.version;
let requests = build_messagedesc_list(&interface.requests);
let events = build_messagedesc_list(&interface.events);
let c_name = format_ident!("{}_interface", interface.name);
if with_c {
let c_iface = super::c_interfaces::generate_interface(interface);
quote! {
pub static #const_name: wayland_backend::protocol::Interface = wayland_backend::protocol::Interface {
name: #iface_name,
version: #iface_version,
requests: #requests,
events: #events,
c_ptr: Some(unsafe { & #c_name }),
};
#c_iface
}
} else {
quote! {
pub static #const_name: wayland_backend::protocol::Interface = wayland_backend::protocol::Interface {
name: #iface_name,
version: #iface_version,
requests: #requests,
events: #events,
c_ptr: None,
};
}
}
}
fn build_messagedesc_list(list: &[Message]) -> TokenStream {
let desc_list = list.iter().map(|message| {
let name = &message.name;
let since = message.since;
let is_destructor = message.typ == Some(Type::Destructor);
let signature = message.args.iter().map(|arg| {
if arg.typ == Type::NewId && arg.interface.is_none() {
// this is a special generic message, it expands to multiple arguments
quote! {
wayland_backend::protocol::ArgumentType::Str(wayland_backend::protocol::AllowNull::No),
wayland_backend::protocol::ArgumentType::Uint,
wayland_backend::protocol::ArgumentType::NewId
}
} else {
let typ = arg.typ.common_type();
if arg.typ.nullable() {
if arg.allow_null {
quote! { wayland_backend::protocol::ArgumentType::#typ(wayland_backend::protocol::AllowNull::Yes) }
} else {
quote! { wayland_backend::protocol::ArgumentType::#typ(wayland_backend::protocol::AllowNull::No) }
}
} else {
quote! { wayland_backend::protocol::ArgumentType::#typ }
}
}
});
let child_interface = match message
.args
.iter()
.find(|arg| arg.typ == Type::NewId)
.and_then(|arg| arg.interface.as_ref())
{
Some(name) => {
let target_iface = format_ident!("{}_INTERFACE", name.to_ascii_uppercase());
quote! { Some(&#target_iface) }
}
None => quote! { None },
};
let arg_interfaces = message.args.iter().filter(|arg| arg.typ == Type::Object).map(|arg| {
match arg.interface {
Some(ref name) => {
let target_iface = format_ident!("{}_INTERFACE", name.to_ascii_uppercase());
quote! { &#target_iface }
}
None => {
quote! { &wayland_backend::protocol::ANONYMOUS_INTERFACE }
}
}
});
quote! {
wayland_backend::protocol::MessageDesc {
name: #name,
signature: &[ #(#signature),* ],
since: #since,
is_destructor: #is_destructor,
child_interface: #child_interface,
arg_interfaces: &[ #(#arg_interfaces),* ],
}
}
});
quote!(
&[ #(#desc_list),* ]
)
}
#[cfg(test)]
mod tests {
#[test]
fn interface_gen() {
let protocol_file =
std::fs::File::open("./tests/scanner_assets/test-protocol.xml").unwrap();
let protocol_parsed = crate::parse::parse(protocol_file);
let generated: String = super::generate(&protocol_parsed, true).to_string();
let generated = crate::format_rust_code(&generated);
let reference =
std::fs::read_to_string("./tests/scanner_assets/test-interfaces.rs").unwrap();
let reference = crate::format_rust_code(&reference);
if reference != generated {
let diff = similar::TextDiff::from_lines(&reference, &generated);
print!("{}", diff.unified_diff().context_radius(10).header("reference", "generated"));
panic!("Generated does not match reference!")
}
}
}
+142
View File
@@ -0,0 +1,142 @@
//! Wayland protocol code-generation machinnery
//!
//! This crate provides procedural macros for generating the rust code associated with a
//! Wayland XML protocol specification, for use with the `wayland-client`, `wayland-server`
//! and `wayland-backend` crates.
//!
//! Before trying to use this crate, you may check if the protocol extension you want to use
//! is not already exposed in the `wayland-protocols` crate.
//!
//! ## Example usage
//!
//! Below is a template for generating the code for a custom protocol client-side. Server-side
//! is identical, just replacing `client` by `server`. The path to the XML file is relative to the
//! crate root.
//!
//! ```rust,ignore
//! // Generate the bindings in their own module
//! pub mod my_protocol {
//! use wayland_client;
//! // import objects from the core protocol if needed
//! use wayland_client::protocol::*;
//!
//! // This module hosts a low-level representation of the protocol objects
//! // you will not need to interact with it yourself, but the code generated
//! // by the generate_client_code! macro will use it
//! pub mod __interfaces {
//! // import the interfaces from the core protocol if needed
//! use wayland_client::protocol::__interfaces::*;
//! wayland_scanner::generate_interfaces!("./path/to/the/protocol.xml");
//! }
//! use self::__interfaces::*;
//!
//! // This macro generates the actual types that represent the wayland objects of
//! // your custom protocol
//! wayland_scanner::generate_client_code!("./path/to/the/protocol.xml");
//! }
//! ```
use std::{ffi::OsString, path::PathBuf};
mod c_interfaces;
mod client_gen;
mod common;
mod interfaces;
mod message_io;
mod parse;
mod protocol;
mod server_gen;
mod token;
mod util;
/// Proc-macro for generating low-level interfaces associated with an XML specification
#[proc_macro]
pub fn generate_interfaces(stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
let path: OsString = token::parse_lit_str_token(stream).into();
let path = if let Some(manifest_dir) = std::env::var_os("CARGO_MANIFEST_DIR") {
let mut buf = PathBuf::from(manifest_dir);
buf.push(path);
buf
} else {
path.into()
};
let file = match std::fs::File::open(&path) {
Ok(file) => file,
Err(e) => panic!("Failed to open protocol file {}: {}", path.display(), e),
};
let protocol = parse::parse(file);
interfaces::generate(&protocol, true).into()
}
/// Proc-macro for generating client-side API associated with an XML specification
#[proc_macro]
pub fn generate_client_code(stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
let path: OsString = token::parse_lit_str_token(stream).into();
let path = if let Some(manifest_dir) = std::env::var_os("CARGO_MANIFEST_DIR") {
let mut buf = PathBuf::from(manifest_dir);
buf.push(path);
buf
} else {
path.into()
};
let file = match std::fs::File::open(&path) {
Ok(file) => file,
Err(e) => panic!("Failed to open protocol file {}: {}", path.display(), e),
};
let protocol = parse::parse(file);
client_gen::generate_client_objects(&protocol).into()
}
/// Proc-macro for generating server-side API associated with an XML specification
#[proc_macro]
pub fn generate_server_code(stream: proc_macro::TokenStream) -> proc_macro::TokenStream {
let path: OsString = token::parse_lit_str_token(stream).into();
let path = if let Some(manifest_dir) = std::env::var_os("CARGO_MANIFEST_DIR") {
let mut buf = PathBuf::from(manifest_dir);
buf.push(path);
buf
} else {
path.into()
};
let file = match std::fs::File::open(&path) {
Ok(file) => file,
Err(e) => panic!("Failed to open protocol file {}: {}", path.display(), e),
};
let protocol = parse::parse(file);
server_gen::generate_server_objects(&protocol).into()
}
#[cfg(test)]
fn format_rust_code(code: &str) -> String {
use std::{
io::Write,
process::{Command, Stdio},
};
if let Ok(mut proc) = Command::new("rustfmt")
.arg("--emit=stdout")
.arg("--edition=2018")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
//.stderr(Stdio::null())
.spawn()
{
{
let stdin = proc.stdin.as_mut().unwrap();
stdin.write_all(code.as_bytes()).unwrap();
}
if let Ok(output) = proc.wait_with_output() {
if output.status.success() {
return std::str::from_utf8(&output.stdout).unwrap().to_owned();
}
}
}
panic!("Rustfmt failed!");
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum Side {
/// wayland client applications
Client,
/// wayland compositors
Server,
}
+305
View File
@@ -0,0 +1,305 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};
use crate::{protocol::*, util::*, Side};
pub(crate) fn gen_parse_body(interface: &Interface, side: Side) -> TokenStream {
let msgs = match side {
Side::Client => &interface.events,
Side::Server => &interface.requests,
};
let object_type = Ident::new(
match side {
Side::Client => "Proxy",
Side::Server => "Resource",
},
Span::call_site(),
);
let msg_type = Ident::new(
match side {
Side::Client => "Event",
Side::Server => "Request",
},
Span::call_site(),
);
let match_arms = msgs.iter().enumerate().map(|(opcode, msg)| {
let opcode = opcode as u16;
let msg_name = Ident::new(&snake_to_camel(&msg.name), Span::call_site());
let args_pat = msg.args.iter().map(|arg| {
let arg_name = Ident::new(
&format!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name),
Span::call_site(),
);
match arg.typ {
Type::Uint => quote!{ Some(Argument::Uint(#arg_name)) },
Type::Int => quote!{ Some(Argument::Int(#arg_name)) },
Type::String => quote!{ Some(Argument::Str(#arg_name)) },
Type::Fixed => quote!{ Some(Argument::Fixed(#arg_name)) },
Type::Array => quote!{ Some(Argument::Array(#arg_name)) },
Type::Object => quote!{ Some(Argument::Object(#arg_name)) },
Type::NewId => quote!{ Some(Argument::NewId(#arg_name)) },
Type::Fd => quote!{ Some(Argument::Fd(#arg_name)) },
Type::Destructor => panic!("Argument {}.{}.{} has type destructor ?!", interface.name, msg.name, arg.name),
}
});
let args_iter = msg.args.iter().map(|_| quote!{ arg_iter.next() });
let arg_names = msg.args.iter().map(|arg| {
let arg_name = format_ident!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name);
if arg.enum_.is_some() {
quote! { #arg_name: From::from(#arg_name as u32) }
} else {
match arg.typ {
Type::Uint | Type::Int | Type::Fd => quote!{ #arg_name },
Type::Fixed => quote!{ #arg_name: (#arg_name as f64) / 256.},
Type::String => {
if arg.allow_null {
quote! {
#arg_name: #arg_name.as_ref().map(|s| String::from_utf8_lossy(s.as_bytes()).into_owned())
}
} else {
quote! {
#arg_name: String::from_utf8_lossy(#arg_name.as_ref().unwrap().as_bytes()).into_owned()
}
}
},
Type::Object => {
let create_proxy = if let Some(ref created_interface) = arg.interface {
let created_iface_mod = Ident::new(created_interface, Span::call_site());
let created_iface_type = Ident::new(&snake_to_camel(created_interface), Span::call_site());
quote! {
match <super::#created_iface_mod::#created_iface_type as #object_type>::from_id(conn, #arg_name.clone()) {
Ok(p) => p,
Err(_) => return Err(DispatchError::BadMessage {
sender_id: msg.sender_id,
interface: Self::interface().name,
opcode: msg.opcode
}),
}
}
} else {
quote! { #arg_name.clone() }
};
if arg.allow_null {
quote! {
#arg_name: if #arg_name.is_null() { None } else { Some(#create_proxy) }
}
} else {
quote! {
#arg_name: #create_proxy
}
}
},
Type::NewId => {
let create_proxy = if let Some(ref created_interface) = arg.interface {
let created_iface_mod = Ident::new(created_interface, Span::call_site());
let created_iface_type = Ident::new(&snake_to_camel(created_interface), Span::call_site());
quote! {
match <super::#created_iface_mod::#created_iface_type as #object_type>::from_id(conn, #arg_name.clone()) {
Ok(p) => p,
Err(_) => return Err(DispatchError::BadMessage {
sender_id: msg.sender_id,
interface: Self::interface().name,
opcode: msg.opcode,
}),
}
}
} else if side == Side::Server {
quote! { New::wrap(#arg_name.clone()) }
} else {
quote! { #arg_name.clone() }
};
if arg.allow_null {
if side == Side::Server {
quote! {
#arg_name: if #arg_name.is_null() { None } else { Some(New::wrap(#create_proxy)) }
}
} else {
quote! {
#arg_name: if #arg_name.is_null() { None } else { Some(#create_proxy) }
}
}
} else if side == Side::Server {
quote! {
#arg_name: New::wrap(#create_proxy)
}
} else {
quote! {
#arg_name: #create_proxy
}
}
},
Type::Array => {
if arg.allow_null {
quote! { if #arg_name.len() == 0 { None } else { Some(*#arg_name) } }
} else {
quote! { #arg_name: *#arg_name }
}
},
Type::Destructor => unreachable!(),
}
}
});
quote! {
#opcode => {
if let (#(#args_pat),*) = (#(#args_iter),*) {
Ok((me, #msg_type::#msg_name { #(#arg_names),* }))
} else {
Err(DispatchError::BadMessage { sender_id: msg.sender_id, interface: Self::interface().name, opcode: msg.opcode })
}
}
}
});
quote! {
let me = Self::from_id(conn, msg.sender_id.clone()).unwrap();
let mut arg_iter = msg.args.into_iter();
match msg.opcode {
#(#match_arms),*
_ => Err(DispatchError::BadMessage { sender_id: msg.sender_id, interface: Self::interface().name, opcode: msg.opcode }),
}
}
}
pub(crate) fn gen_write_body(interface: &Interface, side: Side) -> TokenStream {
let msgs = match side {
Side::Client => &interface.requests,
Side::Server => &interface.events,
};
let msg_type = Ident::new(
match side {
Side::Client => "Request",
Side::Server => "Event",
},
Span::call_site(),
);
let arms = msgs.iter().enumerate().map(|(opcode, msg)| {
let msg_name = Ident::new(&snake_to_camel(&msg.name), Span::call_site());
let opcode = opcode as u16;
let arg_names = msg.args.iter().flat_map(|arg| {
if arg.typ == Type::NewId && arg.interface.is_some() && side == Side::Client {
None
} else {
Some(format_ident!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name))
}
});
let mut child_spec = None;
let args = msg.args.iter().flat_map(|arg| {
let arg_name = format_ident!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name);
match arg.typ {
Type::Int => vec![if arg.enum_.is_some() { quote!{ Argument::Int(Into::<u32>::into(#arg_name) as i32) } } else { quote!{ Argument::Int(#arg_name) } }],
Type::Uint => vec![if arg.enum_.is_some() { quote!{ Argument::Uint(#arg_name.into()) } } else { quote!{ Argument::Uint(#arg_name) } }],
Type::Fd => vec![quote!{ Argument::Fd(#arg_name) }],
Type::Fixed => vec![quote! { Argument::Fixed((#arg_name * 256.) as i32) }],
Type::Object => if arg.allow_null {
if side == Side::Server {
vec![quote! { if let Some(obj) = #arg_name { Argument::Object(Resource::id(&obj)) } else { Argument::Object(ObjectId::null()) } }]
} else {
vec![quote! { if let Some(obj) = #arg_name { Argument::Object(Proxy::id(&obj)) } else { Argument::Object(ObjectId::null()) } }]
}
} else if side == Side::Server {
vec![quote!{ Argument::Object(Resource::id(&#arg_name)) }]
} else {
vec![quote!{ Argument::Object(Proxy::id(&#arg_name)) }]
},
Type::Array => if arg.allow_null {
vec![quote! { if let Some(array) = #arg_name { Argument::Array(Box::new(array)) } else { Argument::Array(Box::new(Vec::new()))}}]
} else {
vec![quote! { Argument::Array(Box::new(#arg_name)) }]
},
Type::String => if arg.allow_null {
vec![quote! { Argument::Str(#arg_name.map(|s| Box::new(std::ffi::CString::new(s).unwrap()))) }]
} else {
vec![quote! { Argument::Str(Some(Box::new(std::ffi::CString::new(#arg_name).unwrap()))) }]
},
Type::NewId => if side == Side::Client {
if let Some(ref created_interface) = arg.interface {
let created_iface_mod = Ident::new(created_interface, Span::call_site());
let created_iface_type = Ident::new(&snake_to_camel(created_interface), Span::call_site());
assert!(child_spec.is_none());
child_spec = Some(quote! { {
let my_info = conn.object_info(self.id())?;
Some((super::#created_iface_mod::#created_iface_type::interface(), my_info.version))
} });
vec![quote! { Argument::NewId(ObjectId::null()) }]
} else {
assert!(child_spec.is_none());
child_spec = Some(quote! {
Some((#arg_name.0, #arg_name.1))
});
vec![
quote! {
Argument::Str(Some(Box::new(std::ffi::CString::new(#arg_name.0.name).unwrap())))
},
quote! {
Argument::Uint(#arg_name.1)
},
quote! {
Argument::NewId(ObjectId::null())
},
]
}
} else {
// server-side NewId is the same as Object
if arg.allow_null {
vec![quote! { if let Some(obj) = #arg_name { Argument::NewId(Resource::id(&obj)) } else { Argument::NewId(ObjectId::null()) } }]
} else {
vec![quote!{ Argument::NewId(Resource::id(&#arg_name)) }]
}
},
Type::Destructor => panic!("Argument {}.{}.{} has type destructor ?!", interface.name, msg.name, arg.name),
}
});
let args = if msg.args.is_empty() {
quote! {
smallvec::SmallVec::new()
}
} else if msg.args.len() <= 4 {
// Note: Keep in sync with `wayland_backend::protocol::INLINE_ARGS`.
// Fits in SmallVec inline capacity
quote! { {
let mut vec = smallvec::SmallVec::new();
#(
vec.push(#args);
)*
vec
} }
} else {
quote! {
smallvec::SmallVec::from_vec(vec![#(#args),*])
}
};
if side == Side::Client {
let child_spec = child_spec.unwrap_or_else(|| quote! { None });
quote! {
#msg_type::#msg_name { #(#arg_names),* } => {
let child_spec = #child_spec;
let args = #args;
Ok((Message {
sender_id: self.id.clone(),
opcode: #opcode,
args
}, child_spec))
}
}
} else {
quote! {
#msg_type::#msg_name { #(#arg_names),* } => Ok(Message {
sender_id: self.id.clone(),
opcode: #opcode,
args: #args,
})
}
}
});
quote! {
match msg {
#(#arms,)*
#msg_type::__phantom_lifetime { never, .. } => match never {}
}
}
}
+405
View File
@@ -0,0 +1,405 @@
use super::protocol::*;
use std::{
io::{BufRead, BufReader, Read},
str::FromStr,
};
use quick_xml::{
events::{attributes::Attributes, Event},
Reader,
};
pub fn parse<S: Read>(stream: S) -> Protocol {
let mut reader = Reader::from_reader(BufReader::new(stream));
let reader_config = reader.config_mut();
reader_config.trim_text(true);
reader_config.expand_empty_elements = true;
parse_protocol(reader)
}
fn decode_utf8_or_panic(txt: Vec<u8>) -> String {
match String::from_utf8(txt) {
Ok(txt) => txt,
Err(e) => panic!("Invalid UTF8: '{}'", String::from_utf8_lossy(&e.into_bytes())),
}
}
fn parse_or_panic<T: FromStr>(txt: &[u8]) -> T {
match std::str::from_utf8(txt).ok().and_then(|val| val.parse().ok()) {
Some(version) => version,
None => panic!(
"Invalid value '{}' for parsing type '{}'",
String::from_utf8_lossy(txt),
std::any::type_name::<T>()
),
}
}
fn init_protocol<R: BufRead>(reader: &mut Reader<R>) -> Protocol {
// Check two firsts lines for protocol tag
for _ in 0..3 {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Decl(_) | Event::DocType(_) | Event::Comment(_)) => {
continue;
}
Ok(Event::Start(bytes)) => {
assert!(bytes.name().into_inner() == b"protocol", "Missing protocol toplevel tag");
if let Some(attr) = bytes
.attributes()
.filter_map(|res| res.ok())
.find(|attr| attr.key.into_inner() == b"name")
{
return Protocol::new(decode_utf8_or_panic(attr.value.into_owned()));
} else {
panic!("Protocol must have a name");
}
}
_ => panic!("Ill-formed protocol file"),
}
}
panic!("Ill-formed protocol file");
}
fn parse_protocol<R: BufRead>(mut reader: Reader<R>) -> Protocol {
let mut protocol = init_protocol(&mut reader);
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Start(bytes)) => {
match bytes.name().into_inner() {
b"copyright" => {
// parse the copyright
let mut copyright = String::new();
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Text(text)) => {
if let Ok(text) = text.decode() {
copyright.push_str(&text);
}
}
Ok(Event::CData(cdata)) => {
if let Ok(cdata) = String::from_utf8(cdata.into_inner().into())
{
copyright.push_str(&cdata);
}
}
Ok(Event::GeneralRef(byte_ref)) => {
if let Ok(Some(c)) = byte_ref.resolve_char_ref() {
copyright.push(c);
} else if let Ok(content) = byte_ref.xml10_content() {
if let Some(s) =
quick_xml::escape::resolve_xml_entity(&content)
{
copyright.push_str(s);
}
}
}
Ok(Event::End(bytes)) => {
assert!(
bytes.name().into_inner() == "copyright".as_bytes(),
"Ill-formed protocol file"
);
break;
}
e => {
panic!("Ill-formed protocol file: {e:?}");
}
}
}
protocol.copyright = Some(copyright)
}
b"interface" => {
protocol.interfaces.push(parse_interface(&mut reader, bytes.attributes()));
}
b"description" => {
protocol.description =
Some(parse_description(&mut reader, bytes.attributes()));
}
name => panic!(
"Ill-formed protocol file: unexpected token `{}` in protocol {}",
String::from_utf8_lossy(name),
protocol.name
),
}
}
Ok(Event::End(bytes)) => {
let name = bytes.name().into_inner();
assert!(
name == b"protocol",
"Unexpected closing token `{}`",
String::from_utf8_lossy(name)
);
break;
}
// ignore comments
Ok(Event::Comment(_)) => {}
e => panic!("Ill-formed protocol file: unexpected token {e:?}"),
}
}
protocol
}
fn parse_interface<R: BufRead>(reader: &mut Reader<R>, attrs: Attributes) -> Interface {
let mut interface = Interface::new();
for attr in attrs.filter_map(|res| res.ok()) {
match attr.key.into_inner() {
b"name" => interface.name = decode_utf8_or_panic(attr.value.into_owned()),
b"version" => interface.version = parse_or_panic(&attr.value),
_ => {}
}
}
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Start(bytes)) => match bytes.name().into_inner() {
b"description" => {
interface.description = Some(parse_description(reader, bytes.attributes()))
}
b"request" => interface.requests.push(parse_request(reader, bytes.attributes())),
b"event" => interface.events.push(parse_event(reader, bytes.attributes())),
b"enum" => interface.enums.push(parse_enum(reader, bytes.attributes())),
name => panic!("Unexpected token: `{}`", String::from_utf8_lossy(name)),
},
Ok(Event::End(bytes)) if bytes.name().into_inner() == b"interface" => break,
_ => {}
}
}
interface
}
fn parse_description<R: BufRead>(reader: &mut Reader<R>, attrs: Attributes) -> (String, String) {
let mut summary = String::new();
for attr in attrs.filter_map(|res| res.ok()) {
if attr.key.into_inner() == b"summary" {
summary = String::from_utf8_lossy(&attr.value)
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
}
}
let mut description = String::new();
// Some protocols have comments inside their descriptions, so we need to parse them in a loop and
// concatenate the parts into a single block of text
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Text(bytes)) => {
if !description.is_empty() {
description.push_str("\n\n");
}
description.push_str(&bytes.decode().unwrap_or_default())
}
Ok(Event::End(bytes)) if bytes.name().into_inner() == b"description" => break,
Ok(Event::Comment(_)) => {}
e => panic!("Ill-formed protocol file: {e:?}"),
}
}
(summary, description)
}
fn parse_request<R: BufRead>(reader: &mut Reader<R>, attrs: Attributes) -> Message {
let mut request = Message::new();
for attr in attrs.filter_map(|res| res.ok()) {
match attr.key.into_inner() {
b"name" => request.name = decode_utf8_or_panic(attr.value.into_owned()),
b"type" => request.typ = Some(parse_type(&attr.value)),
b"since" => request.since = parse_or_panic(&attr.value),
_ => {}
}
}
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Start(bytes)) => match bytes.name().into_inner() {
b"description" => {
request.description = Some(parse_description(reader, bytes.attributes()))
}
b"arg" => request.args.push(parse_arg(reader, bytes.attributes())),
name => panic!("Unexpected token: `{}`", String::from_utf8_lossy(name)),
},
Ok(Event::End(bytes)) if bytes.name().into_inner() == b"request" => break,
_ => {}
}
}
request
}
fn parse_enum<R: BufRead>(reader: &mut Reader<R>, attrs: Attributes) -> Enum {
let mut enu = Enum::new();
for attr in attrs.filter_map(|res| res.ok()) {
match attr.key.into_inner() {
b"name" => enu.name = decode_utf8_or_panic(attr.value.into_owned()),
b"since" => enu.since = parse_or_panic(&attr.value),
b"bitfield" if attr.value.as_ref() == b"true" => enu.bitfield = true,
b"bitfield" => {}
_ => {}
}
}
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Start(bytes)) => match bytes.name().into_inner() {
b"description" => {
enu.description = Some(parse_description(reader, bytes.attributes()))
}
b"entry" => enu.entries.push(parse_entry(reader, bytes.attributes())),
name => panic!("Unexpected token: `{}`", String::from_utf8_lossy(name)),
},
Ok(Event::End(bytes)) if bytes.name().into_inner() == b"enum" => break,
_ => {}
}
}
enu
}
fn parse_event<R: BufRead>(reader: &mut Reader<R>, attrs: Attributes) -> Message {
let mut event = Message::new();
for attr in attrs.filter_map(|res| res.ok()) {
match attr.key.into_inner() {
b"name" => event.name = decode_utf8_or_panic(attr.value.into_owned()),
b"type" => event.typ = Some(parse_type(&attr.value)),
b"since" => event.since = parse_or_panic(&attr.value),
_ => {}
}
}
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Start(bytes)) => match bytes.name().into_inner() {
b"description" => {
event.description = Some(parse_description(reader, bytes.attributes()))
}
b"arg" => event.args.push(parse_arg(reader, bytes.attributes())),
name => panic!("Unexpected token: `{}`", String::from_utf8_lossy(name)),
},
Ok(Event::End(bytes)) if bytes.name().into_inner() == b"event" => break,
_ => {}
}
}
event
}
fn parse_arg<R: BufRead>(reader: &mut Reader<R>, attrs: Attributes) -> Arg {
let mut arg = Arg::new();
for attr in attrs.filter_map(|res| res.ok()) {
match attr.key.into_inner() {
b"name" => arg.name = decode_utf8_or_panic(attr.value.into_owned()),
b"type" => arg.typ = parse_type(&attr.value),
b"summary" => {
arg.summary = Some(
String::from_utf8_lossy(&attr.value)
.split_whitespace()
.collect::<Vec<_>>()
.join(" "),
)
}
b"interface" => arg.interface = Some(parse_or_panic(&attr.value)),
b"allow-null" if attr.value.as_ref() == b"true" => arg.allow_null = true,
b"allow-null" => {}
b"enum" => arg.enum_ = Some(decode_utf8_or_panic(attr.value.into_owned())),
_ => {}
}
}
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Start(bytes)) => match bytes.name().into_inner() {
b"description" => {
arg.description = Some(parse_description(reader, bytes.attributes()))
}
name => panic!("Unexpected token: `{}`", String::from_utf8_lossy(name)),
},
Ok(Event::End(bytes)) if bytes.name().into_inner() == b"arg" => break,
_ => {}
}
}
arg
}
fn parse_type(txt: &[u8]) -> Type {
match txt {
b"int" => Type::Int,
b"uint" => Type::Uint,
b"fixed" => Type::Fixed,
b"string" => Type::String,
b"object" => Type::Object,
b"new_id" => Type::NewId,
b"array" => Type::Array,
b"fd" => Type::Fd,
b"destructor" => Type::Destructor,
e => panic!("Unexpected type: {}", String::from_utf8_lossy(e)),
}
}
fn parse_entry<R: BufRead>(reader: &mut Reader<R>, attrs: Attributes) -> Entry {
let mut entry = Entry::new();
for attr in attrs.filter_map(|res| res.ok()) {
match attr.key.into_inner() {
b"name" => entry.name = decode_utf8_or_panic(attr.value.into_owned()),
b"value" => {
entry.value = if attr.value.starts_with(b"0x") {
if let Some(val) = std::str::from_utf8(&attr.value[2..])
.ok()
.and_then(|s| u32::from_str_radix(s, 16).ok())
{
val
} else {
panic!("Invalid number: {}", String::from_utf8_lossy(&attr.value))
}
} else {
parse_or_panic(&attr.value)
};
}
b"since" => entry.since = parse_or_panic(&attr.value),
b"summary" => {
entry.summary = Some(
String::from_utf8_lossy(&attr.value)
.split_whitespace()
.collect::<Vec<_>>()
.join(" "),
)
}
_ => {}
}
}
loop {
match reader.read_event_into(&mut Vec::new()) {
Ok(Event::Start(bytes)) => match bytes.name().into_inner() {
b"description" => {
entry.description = Some(parse_description(reader, bytes.attributes()))
}
name => panic!("Unexpected token: `{}`", String::from_utf8_lossy(name)),
},
Ok(Event::End(bytes)) if bytes.name().into_inner() == b"entry" => break,
_ => {}
}
}
entry
}
#[cfg(test)]
mod tests {
#[test]
fn xml_parse() {
let protocol_file =
std::fs::File::open("./tests/scanner_assets/test-protocol.xml").unwrap();
let _ = crate::parse::parse(protocol_file);
}
#[test]
fn headerless_xml_parse() {
let protocol_file =
std::fs::File::open("./tests/scanner_assets/test-headerless-protocol.xml").unwrap();
let _ = crate::parse::parse(protocol_file);
}
}
+154
View File
@@ -0,0 +1,154 @@
use proc_macro2::TokenStream;
use quote::quote;
#[derive(Clone, Debug)]
pub struct Protocol {
pub name: String,
pub copyright: Option<String>,
pub description: Option<(String, String)>,
pub interfaces: Vec<Interface>,
}
impl Protocol {
pub fn new(name: String) -> Protocol {
Protocol { name, copyright: None, description: None, interfaces: Vec::new() }
}
}
#[derive(Clone, Debug)]
pub struct Interface {
pub name: String,
pub version: u32,
pub description: Option<(String, String)>,
pub requests: Vec<Message>,
pub events: Vec<Message>,
pub enums: Vec<Enum>,
}
impl Interface {
pub fn new() -> Interface {
Interface {
name: String::new(),
version: 1,
description: None,
requests: Vec::new(),
events: Vec::new(),
enums: Vec::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct Message {
pub name: String,
pub typ: Option<Type>,
pub since: u32,
pub description: Option<(String, String)>,
pub args: Vec<Arg>,
}
impl Message {
pub fn new() -> Message {
Message { name: String::new(), typ: None, since: 1, description: None, args: Vec::new() }
}
pub fn all_null(&self) -> bool {
self.args
.iter()
.all(|a| !((a.typ == Type::Object || a.typ == Type::NewId) && a.interface.is_some()))
}
}
#[derive(Clone, Debug)]
pub struct Arg {
pub name: String,
pub typ: Type,
pub interface: Option<String>,
pub summary: Option<String>,
pub description: Option<(String, String)>,
pub allow_null: bool,
pub enum_: Option<String>,
}
impl Arg {
pub fn new() -> Arg {
Arg {
name: String::new(),
typ: Type::Object,
interface: None,
summary: None,
description: None,
allow_null: false,
enum_: None,
}
}
}
#[derive(Clone, Debug)]
pub struct Enum {
pub name: String,
pub since: u16,
pub description: Option<(String, String)>,
pub entries: Vec<Entry>,
pub bitfield: bool,
}
impl Enum {
pub fn new() -> Enum {
Enum {
name: String::new(),
since: 1,
description: None,
entries: Vec::new(),
bitfield: false,
}
}
}
#[derive(Clone, Debug)]
pub struct Entry {
pub name: String,
pub value: u32,
pub since: u16,
pub description: Option<(String, String)>,
pub summary: Option<String>,
}
impl Entry {
pub fn new() -> Entry {
Entry { name: String::new(), value: 0, since: 1, description: None, summary: None }
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Type {
Int,
Uint,
Fixed,
String,
Object,
NewId,
Array,
Fd,
Destructor,
}
impl Type {
pub fn nullable(self) -> bool {
matches!(self, Type::String | Type::Object)
}
pub fn common_type(self) -> TokenStream {
match self {
Type::Int => quote!(Int),
Type::Uint => quote!(Uint),
Type::Fixed => quote!(Fixed),
Type::Array => quote!(Array),
Type::Fd => quote!(Fd),
Type::String => quote!(Str),
Type::Object => quote!(Object),
Type::NewId => quote!(NewId),
Type::Destructor => panic!("Destructor is not a valid argument type."),
}
}
}
+298
View File
@@ -0,0 +1,298 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote};
use crate::{
protocol::{Interface, Protocol, Type},
util::{description_to_doc_attr, dotted_to_relname, is_keyword, snake_to_camel, to_doc_attr},
Side,
};
pub fn generate_server_objects(protocol: &Protocol) -> TokenStream {
protocol
.interfaces
.iter()
.filter(|iface| iface.name != "wl_display")
.map(generate_objects_for)
.collect()
}
fn generate_objects_for(interface: &Interface) -> TokenStream {
let mod_name = Ident::new(&interface.name, Span::call_site());
let mod_doc = interface.description.as_ref().map(description_to_doc_attr);
let iface_name = Ident::new(&snake_to_camel(&interface.name), Span::call_site());
let iface_const_name = format_ident!("{}_INTERFACE", interface.name.to_ascii_uppercase());
let enums = crate::common::generate_enums_for(interface);
let msg_constants = crate::common::gen_msg_constants(&interface.requests, &interface.events);
let requests = crate::common::gen_message_enum(
&format_ident!("Request"),
Side::Server,
true,
&interface.requests,
);
let events = crate::common::gen_message_enum(
&format_ident!("Event"),
Side::Server,
false,
&interface.events,
);
let parse_body = if interface.name == "wl_registry" {
quote! { unimplemented!("`wl_registry` is implemented internally in `wayland-server`") }
} else {
crate::message_io::gen_parse_body(interface, Side::Server)
};
let write_body = crate::message_io::gen_write_body(interface, Side::Server);
let methods = gen_methods(interface);
let event_ref = if interface.requests.is_empty() {
"This interface has no requests."
} else {
"See also the [Request] enum for this interface."
};
let docs = match &interface.description {
Some((short, long)) => format!("{short}\n\n{long}\n\n{event_ref}"),
None => format!("{}\n\n{}", interface.name, event_ref),
};
let doc_attr = to_doc_attr(&docs);
quote! {
#mod_doc
pub mod #mod_name {
use std::sync::Arc;
use std::os::unix::io::OwnedFd;
use super::wayland_server::{
backend::{
smallvec, ObjectData, ObjectId, InvalidId, WeakHandle,
protocol::{WEnum, Argument, Message, Interface, same_interface}
},
Resource, Dispatch, DisplayHandle, DispatchError, ResourceData, New, Weak,
};
#enums
#msg_constants
#requests
#events
#doc_attr
#[derive(Debug, Clone)]
pub struct #iface_name {
id: ObjectId,
version: u32,
data: Option<Arc<dyn std::any::Any + Send + Sync + 'static>>,
handle: WeakHandle,
}
impl std::cmp::PartialEq for #iface_name {
#[inline]
fn eq(&self, other: &#iface_name) -> bool {
self.id == other.id
}
}
impl std::cmp::Eq for #iface_name {}
impl PartialEq<Weak<#iface_name>> for #iface_name {
#[inline]
fn eq(&self, other: &Weak<#iface_name>) -> bool {
self.id == other.id()
}
}
impl std::borrow::Borrow<ObjectId> for #iface_name {
#[inline]
fn borrow(&self) -> &ObjectId {
&self.id
}
}
impl std::hash::Hash for #iface_name {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state)
}
}
impl super::wayland_server::Resource for #iface_name {
type Request = Request;
type Event<'event> = Event<'event>;
#[inline]
fn interface() -> &'static Interface{
&super::#iface_const_name
}
#[inline]
fn id(&self) -> ObjectId {
self.id.clone()
}
#[inline]
fn version(&self) -> u32 {
self.version
}
#[inline]
fn data<U: 'static>(&self) -> Option<&U> {
self.data.as_ref().and_then(|arc| (&**arc).downcast_ref::<ResourceData<Self, U>>()).map(|data| &data.udata)
}
#[inline]
fn object_data(&self) -> Option<&Arc<dyn std::any::Any + Send + Sync>> {
self.data.as_ref()
}
fn handle(&self) -> &WeakHandle {
&self.handle
}
#[inline]
fn from_id(conn: &DisplayHandle, id: ObjectId) -> Result<Self, InvalidId> {
if !same_interface(id.interface(), Self::interface()) && !id.is_null(){
return Err(InvalidId)
}
let version = conn.object_info(id.clone()).map(|info| info.version).unwrap_or(0);
let data = conn.get_object_data(id.clone()).ok();
Ok(#iface_name { id, data, version, handle: conn.backend_handle().downgrade() })
}
fn send_event(&self, evt: Self::Event<'_>) -> Result<(), InvalidId> {
let handle = DisplayHandle::from(self.handle.upgrade().ok_or(InvalidId)?);
handle.send_event(self, evt)
}
fn parse_request(conn: &DisplayHandle, msg: Message<ObjectId, OwnedFd>) -> Result<(Self, Self::Request), DispatchError> {
#parse_body
}
fn write_event<'a>(&self, conn: &DisplayHandle, msg: Self::Event<'a>) -> Result<Message<ObjectId, std::os::unix::io::BorrowedFd<'a>>, InvalidId> {
#write_body
}
fn __set_object_data(&mut self, odata: std::sync::Arc<dyn std::any::Any + Send + Sync + 'static>) {
self.data = Some(odata);
}
}
impl #iface_name {
#methods
}
}
}
}
fn gen_methods(interface: &Interface) -> TokenStream {
interface
.events
.iter()
.map(|request| {
let method_name = format_ident!(
"{}{}",
if is_keyword(&request.name) { "_" } else { "" },
request.name
);
let enum_variant = Ident::new(&snake_to_camel(&request.name), Span::call_site());
let fn_args = request.args.iter().flat_map(|arg| {
let arg_name =
format_ident!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name);
let arg_type = if let Some(ref enu) = arg.enum_ {
let enum_type = dotted_to_relname(enu);
quote! { #enum_type }
} else {
match arg.typ {
Type::Uint => quote! { u32 },
Type::Int => quote! { i32 },
Type::Fixed => quote! { f64 },
Type::String => {
if arg.allow_null {
quote! { Option<String> }
} else {
quote! { String }
}
}
Type::Array => {
if arg.allow_null {
quote! { Option<Vec<u8>> }
} else {
quote! { Vec<u8> }
}
}
Type::Fd => quote! { ::std::os::unix::io::BorrowedFd<'_> },
Type::Object | Type::NewId => {
let iface = arg.interface.as_ref().unwrap();
let iface_mod = Ident::new(iface, Span::call_site());
let iface_type = Ident::new(&snake_to_camel(iface), Span::call_site());
if arg.allow_null {
quote! { Option<&super::#iface_mod::#iface_type> }
} else {
quote! { &super::#iface_mod::#iface_type }
}
}
Type::Destructor => panic!("An argument cannot have type \"destructor\"."),
}
};
Some(quote! {
#arg_name: #arg_type
})
});
let enum_args = request.args.iter().flat_map(|arg| {
let arg_name =
format_ident!("{}{}", if is_keyword(&arg.name) { "_" } else { "" }, arg.name);
if arg.enum_.is_some() {
Some(quote! { #arg_name: WEnum::Value(#arg_name) })
} else if arg.typ == Type::Object || arg.typ == Type::NewId {
if arg.allow_null {
Some(quote! { #arg_name: #arg_name.cloned() })
} else {
Some(quote! { #arg_name: #arg_name.clone() })
}
} else {
Some(quote! { #arg_name })
}
});
let doc_attr = request.description.as_ref().map(description_to_doc_attr);
quote! {
#doc_attr
#[allow(clippy::too_many_arguments)]
pub fn #method_name(&self, #(#fn_args),*) {
let _ = self.send_event(
Event::#enum_variant {
#(#enum_args),*
}
);
}
}
})
.collect()
}
#[cfg(test)]
mod tests {
#[test]
fn server_gen() {
let protocol_file =
std::fs::File::open("./tests/scanner_assets/test-protocol.xml").unwrap();
let protocol_parsed = crate::parse::parse(protocol_file);
let generated: String = super::generate_server_objects(&protocol_parsed).to_string();
let generated = crate::format_rust_code(&generated);
let reference =
std::fs::read_to_string("./tests/scanner_assets/test-server-code.rs").unwrap();
let reference = crate::format_rust_code(&reference);
if reference != generated {
let diff = similar::TextDiff::from_lines(&reference, &generated);
print!("{}", diff.unified_diff().context_radius(10).header("reference", "generated"));
panic!("Generated does not match reference!")
}
}
}
+186
View File
@@ -0,0 +1,186 @@
// `bytes`, `next_chr`, `parse_lit_str`, `parse_lit_str_cooked` and `parse_lit_str_raw` are adapted
// from syn:
// https://github.com/dtolnay/syn/blob/362ee2d02df3f1b2e74c7b7a4cf2ed3c106404c9/src/lit.rs#L1062-L1167
// and
// https://github.com/dtolnay/syn/blob/362ee2d02df3f1b2e74c7b7a4cf2ed3c106404c9/src/lit.rs#L1327-L1388
/// Get the byte at offset idx, or a default of `b'\0'` if we're looking
/// past the end of the input buffer.
fn byte(s: &str, idx: usize) -> u8 {
if idx < s.len() {
s.as_bytes()[idx]
} else {
0
}
}
fn next_chr(s: &str) -> char {
s.chars().next().unwrap_or('\0')
}
// Returns (content, suffix).
fn parse_lit_str(s: &str) -> String {
match byte(s, 0) {
b'"' => parse_lit_str_cooked(s),
b'r' => parse_lit_str_raw(s),
_ => unreachable!(),
}
}
// Clippy false positive
// https://github.com/rust-lang-nursery/rust-clippy/issues/2329
#[allow(clippy::needless_continue)]
fn parse_lit_str_cooked(mut s: &str) -> String {
assert_eq!(byte(s, 0), b'"');
s = &s[1..];
let mut content = String::new();
'outer: loop {
let ch = match byte(s, 0) {
b'"' => break,
b'\\' => {
let b = byte(s, 1);
s = &s[2..];
match b {
b'x' => {
let (byte, rest) = backslash_x(s);
s = rest;
assert!(byte <= 0x80, "Invalid \\x byte in string literal");
char::from_u32(u32::from(byte)).unwrap()
}
b'u' => {
let (chr, rest) = backslash_u(s);
s = rest;
chr
}
b'n' => '\n',
b'r' => '\r',
b't' => '\t',
b'\\' => '\\',
b'0' => '\0',
b'\'' => '\'',
b'"' => '"',
b'\r' | b'\n' => loop {
let ch = next_chr(s);
if ch.is_whitespace() {
s = &s[ch.len_utf8()..];
} else {
continue 'outer;
}
},
b => panic!("unexpected byte {b:?} after \\ character in byte literal"),
}
}
b'\r' => {
assert_eq!(byte(s, 1), b'\n', "Bare CR not allowed in string");
s = &s[2..];
'\n'
}
_ => {
let ch = next_chr(s);
s = &s[ch.len_utf8()..];
ch
}
};
content.push(ch);
}
assert!(s.starts_with('"'));
content
}
fn parse_lit_str_raw(mut s: &str) -> String {
assert_eq!(byte(s, 0), b'r');
s = &s[1..];
let mut pounds = 0;
while byte(s, pounds) == b'#' {
pounds += 1;
}
assert_eq!(byte(s, pounds), b'"');
let close = s.rfind('"').unwrap();
for end in s[close + 1..close + 1 + pounds].bytes() {
assert_eq!(end, b'#');
}
s[pounds + 1..close].to_owned()
}
fn backslash_x(s: &str) -> (u8, &str) {
let mut ch = 0;
let b0 = byte(s, 0);
let b1 = byte(s, 1);
ch += 0x10
* match b0 {
b'0'..=b'9' => b0 - b'0',
b'a'..=b'f' => 10 + (b0 - b'a'),
b'A'..=b'F' => 10 + (b0 - b'A'),
_ => panic!("unexpected non-hex character after \\x"),
};
ch += match b1 {
b'0'..=b'9' => b1 - b'0',
b'a'..=b'f' => 10 + (b1 - b'a'),
b'A'..=b'F' => 10 + (b1 - b'A'),
_ => panic!("unexpected non-hex character after \\x"),
};
(ch, &s[2..])
}
fn backslash_u(mut s: &str) -> (char, &str) {
if byte(s, 0) != b'{' {
panic!("{}", "expected { after \\u");
}
s = &s[1..];
let mut ch = 0;
let mut digits = 0;
loop {
let b = byte(s, 0);
let digit = match b {
b'0'..=b'9' => b - b'0',
b'a'..=b'f' => 10 + b - b'a',
b'A'..=b'F' => 10 + b - b'A',
b'_' if digits > 0 => {
s = &s[1..];
continue;
}
b'}' if digits == 0 => panic!("invalid empty unicode escape"),
b'}' => break,
_ => panic!("unexpected non-hex character after \\u"),
};
if digits == 6 {
panic!("overlong unicode escape (must have at most 6 hex digits)");
}
ch *= 0x10;
ch += u32::from(digit);
digits += 1;
s = &s[1..];
}
assert!(byte(s, 0) == b'}');
s = &s[1..];
if let Some(ch) = char::from_u32(ch) {
(ch, s)
} else {
panic!("character code {ch:x} is not a valid unicode character");
}
}
// End of code adapted from syn
pub fn parse_lit_str_token(mut stream: proc_macro::TokenStream) -> String {
loop {
let mut iter = stream.into_iter();
let token = iter.next().expect("expected string argument");
assert!(iter.next().is_none(), "unexpected trailing token");
let literal = match token {
proc_macro::TokenTree::Literal(literal) => literal,
proc_macro::TokenTree::Group(group) => {
stream = group.stream();
continue;
}
_ => panic!("expected string argument found `{token:?}`"),
};
return parse_lit_str(&literal.to_string());
}
}
+115
View File
@@ -0,0 +1,115 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
pub(crate) fn to_doc_attr(text: &str) -> TokenStream {
let text = text.lines().map(str::trim).collect::<Vec<_>>().join("\n");
let text = text.trim();
quote!(#[doc = #text])
}
pub(crate) fn description_to_doc_attr((short, long): &(String, String)) -> TokenStream {
to_doc_attr(&format!("{short}\n\n{long}"))
}
pub fn is_keyword(txt: &str) -> bool {
matches!(
txt,
"abstract"
| "alignof"
| "as"
| "become"
| "box"
| "break"
| "const"
| "continue"
| "crate"
| "do"
| "else"
| "enum"
| "extern"
| "false"
| "final"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "macro"
| "match"
| "mod"
| "move"
| "mut"
| "offsetof"
| "override"
| "priv"
| "proc"
| "pub"
| "pure"
| "ref"
| "return"
| "Self"
| "self"
| "sizeof"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "typeof"
| "unsafe"
| "unsized"
| "use"
| "virtual"
| "where"
| "while"
| "yield"
| "__handler"
| "__object"
)
}
pub fn is_camel_keyword(txt: &str) -> bool {
matches!(txt, "Self")
}
pub fn snake_to_camel(input: &str) -> String {
let result = input
.split('_')
.flat_map(|s| {
let mut first = true;
s.chars().map(move |c| {
if first {
first = false;
c.to_ascii_uppercase()
} else {
c
}
})
})
.collect::<String>();
if is_camel_keyword(&result) {
format!("_{}", &result)
} else {
result
}
}
pub fn dotted_to_relname(input: &str) -> TokenStream {
let mut it = input.split('.');
match (it.next(), it.next()) {
(Some(module), Some(name)) => {
let module = Ident::new(module, Span::call_site());
let ident = Ident::new(&snake_to_camel(name), Span::call_site());
quote::quote!(super::#module::#ident)
}
(Some(name), None) => {
Ident::new(&snake_to_camel(name), Span::call_site()).into_token_stream()
}
_ => unreachable!(),
}
}