use nom::AsBytes; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::name::{Namespace, ResolveResult}; use crate::prelude::*; use crate::util::xml::*; pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-bind"; // TODO remove `pub` in newtypes, introduce validation #[derive(PartialEq, Eq, Debug)] pub struct Name(pub String); #[derive(PartialEq, Eq, Debug)] pub struct Server(pub String); #[derive(PartialEq, Eq, Debug)] pub struct Resource(pub(super) String); #[derive(PartialEq, Eq, Debug)] pub struct Jid { pub name: Name, pub server: Server, pub resource: Resource, } impl Jid { pub fn to_string(&self) -> String { format!("{}@{}/{}", self.name.0, self.server.0, self.resource.0) } } /// Request to bind to a resource. /// /// Example: /// ```xml /// /// mobile /// /// ``` /// #[derive(PartialEq, Eq, Debug)] pub struct BindRequest(pub Resource); pub struct BindRequestParser(BindRequestParserInner); enum BindRequestParserInner { Initial, /// Consumed start and expects InBind(Option), /// Consumed start InBindResourceInitial, /// Consumer start and inner text InBindResourceEnd(String), } impl FromXmlTag for BindRequest { const NS: &'static str = XMLNS; const NAME: &'static str = "bind"; } impl FromXml for BindRequest { type P = BindRequestParser; fn parse() -> Self::P { BindRequestParser(BindRequestParserInner::Initial) } } impl Parser for BindRequestParser { type Output = Result; fn consume<'a>( self: Self, namespace: ResolveResult, event: &Event<'a>, ) -> Continuation { // TODO validate tag names and namespaces use BindRequestParserInner::*; match self.0 { Initial => { let Event::Start(bytes) = event else { return Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))); }; if bytes.name().0 != BindRequest::NAME.as_bytes() { return Continuation::Final(Err(ffail!( "Unexpected XML tag: {:?}", bytes.name() ))); } let ResolveResult::Bound(Namespace(ns)) = namespace else { return Continuation::Final(Err(ffail!("No namespace provided"))); }; if ns != XMLNS.as_bytes() { return Continuation::Final(Err(ffail!("Incorrect namespace"))); } Continuation::Continue(BindRequestParser(InBind(None))) } InBind(resource) => match event { Event::Start(bytes) => { Continuation::Continue(BindRequestParser(InBindResourceInitial)) } Event::End(bytes) => { let Some(resource) = resource else { return Continuation::Final(Err(ffail!("No resource was provided"))); }; Continuation::Final(Ok(BindRequest(Resource(resource)))) } _ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))), }, InBindResourceInitial => { let Event::Text(text) = event else { return Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))); }; let resource = match std::str::from_utf8(text.as_bytes()) { Ok(e) => e.to_string(), Err(err) => return Continuation::Final(Err(err.into())), }; Continuation::Continue(BindRequestParser(InBindResourceEnd(resource))) } InBindResourceEnd(resource) => { let Event::End(bytes) = event else { return Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))); }; Continuation::Continue(BindRequestParser(InBind(Some(resource)))) } } } } pub struct BindResponse(pub Jid); impl ToXml for BindResponse { fn serialize(&self, events: &mut Vec>) { events.extend_from_slice(&[ Event::Start(BytesStart::new( r#"bind xmlns="urn:ietf:params:xml:ns:xmpp-bind""#, )), Event::Start(BytesStart::new(r#"jid"#)), Event::Text(BytesText::new(self.0.to_string().as_str()).into_owned()), Event::End(BytesEnd::new("jid")), Event::End(BytesEnd::new("bind")), ]); } } #[cfg(test)] mod tests { use quick_xml::NsReader; use super::*; #[tokio::test] async fn parse_message() { let input = r#"mobile"#; let mut reader = NsReader::from_reader(input.as_bytes()); let mut buf = vec![]; let (ns, event) = reader .read_resolved_event_into_async(&mut buf) .await .unwrap(); let mut parser = BindRequest::parse().consume(ns, &event); let result = loop { match parser { Continuation::Final(res) => break res, Continuation::Continue(next) => { let (ns, event) = reader .read_resolved_event_into_async(&mut buf) .await .unwrap(); parser = next.consume(ns, &event); } } } .unwrap(); assert_eq!(result, BindRequest(Resource("mobile".to_string())),) } }