1pub mod actions;
15
16use anyhow::Result;
17use bytes::Bytes;
18use http_body_util::{BodyExt, Full};
19use hyper::{Request, Response};
20use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event as XmlEvent};
21use quick_xml::{Reader, Writer};
22use std::io::Cursor;
23use std::net::SocketAddr;
24use std::sync::Arc;
25use tokio::sync::mpsc;
26
27use crate::llm::action_helper::call_llm;
28use crate::llm::ollama_client::OllamaClient;
29use crate::llm::ActionResult;
30#[cfg(not(target_arch = "wasm32"))]
31use crate::logging::emit::Log;
32use crate::server::connection::ConnectionId;
33use crate::state::app_state::AppState;
34use crate::state::server::{
35 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo, ServerId,
36};
37
38pub use actions::XmlRpcProtocol;
39
40#[cfg(not(target_arch = "wasm32"))]
43pub const MAX_REQUEST_BODY_BYTES: usize = 4 * 1024 * 1024;
44
45#[cfg(not(target_arch = "wasm32"))]
58const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
59
60#[cfg(not(target_arch = "wasm32"))]
73const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
74
75#[cfg(not(target_arch = "wasm32"))]
82const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
83
84#[cfg(not(target_arch = "wasm32"))]
90const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
91 Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
92
93#[cfg(not(target_arch = "wasm32"))]
103const FAULT_INTERNAL_ERROR: i32 = -32603;
104#[cfg(not(target_arch = "wasm32"))]
106const FAULT_SERVER_BUSY: i32 = -32000;
107
108pub struct XmlRpcServer;
110
111#[cfg(not(target_arch = "wasm32"))]
112impl XmlRpcServer {
113 pub async fn spawn_with_llm_actions(
115 listen_addr: SocketAddr,
116 llm_client: OllamaClient,
117 app_state: Arc<AppState>,
118 status_tx: mpsc::UnboundedSender<String>,
119 server_id: ServerId,
120 ) -> Result<SocketAddr> {
121 let listener =
122 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
123 let local_addr = listener.local_addr()?;
124 Log::new(Some(&status_tx)).info(format!("XML-RPC server listening on {}", local_addr));
125
126 let protocol = Arc::new(XmlRpcProtocol::new());
127
128 let task_registrar = app_state.clone();
129 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
130 let accept_handle = tokio::spawn(async move {
131 loop {
132 match crate::server::accept_bounded::accept_bounded(
133 &listener,
134 &limiter,
135 CONNECTION_CAP_REFUSAL,
136 "XML-RPC",
137 Some(&status_tx),
138 )
139 .await
140 {
141 Ok((stream, remote_addr, permit)) => {
142 let connection_id =
143 ConnectionId::new(app_state.get_next_unified_id().await);
144 Log::new(Some(&status_tx)).info(format!(
145 "XML-RPC connection {} from {}",
146 connection_id, remote_addr
147 ));
148
149 let local_addr_conn = stream.local_addr().unwrap_or(listen_addr);
151 let now = crate::utils::clock::Instant::now();
152
153 let conn_state = ServerConnectionState {
154 id: connection_id,
155 remote_addr,
156 local_addr: local_addr_conn,
157 bytes_sent: 0,
158 bytes_received: 0,
159 packets_sent: 0,
160 packets_received: 0,
161 last_activity: now,
162 status: ConnectionStatus::Active,
163 status_changed_at: now,
164 protocol_info: ProtocolConnectionInfo::empty(),
165 };
166
167 app_state
168 .add_connection_to_server(server_id, conn_state)
169 .await;
170 let _ = status_tx.send("__UPDATE_UI__".to_string());
171
172 let llm_clone = llm_client.clone();
173 let state_clone = app_state.clone();
174 let status_clone = status_tx.clone();
175 let protocol_clone = protocol.clone();
176
177 let task_owner = app_state.clone();
180 task_owner
181 .spawn_server_task(server_id, async move {
182 let _permit = permit;
185
186 let spoke = matches!(
197 tokio::time::timeout(
198 FIRST_BYTE_READ_TIMEOUT,
199 stream.peek(&mut [0u8; 1]),
200 )
201 .await,
202 Ok(Ok(n)) if n > 0
203 );
204
205 let io = hyper_util::rt::TokioIo::new(stream);
206
207 let activity = std::sync::Arc::new(
213 crate::server::accept_bounded::ConnectionActivity::new(),
214 );
215 let activity_for_service = std::sync::Arc::clone(&activity);
216
217 let service = hyper::service::service_fn(|req| {
219 let llm = llm_clone.clone();
220 let state = state_clone.clone();
221 let status = status_clone.clone();
222 let proto = protocol_clone.clone();
223 let activity = std::sync::Arc::clone(&activity_for_service);
224 async move {
225 let _busy = activity.busy();
226 handle_xmlrpc_request(
227 req,
228 connection_id,
229 server_id,
230 remote_addr,
231 llm,
232 state,
233 status,
234 proto,
235 )
236 .await
237 }
238 });
239
240 if !spoke {
242 Log::new(Some(&status_clone)).debug(format!(
243 "XML-RPC peer {} sent nothing for {}s; closing before \
244 any request",
245 remote_addr,
246 FIRST_BYTE_READ_TIMEOUT.as_secs()
247 ));
248 } else {
249 let conn = hyper::server::conn::http1::Builder::new()
250 .serve_connection(io, service);
251 tokio::pin!(conn);
252 tokio::select! {
253 result = &mut conn => {
254 if let Err(err) = result {
255 Log::new(Some(&status_clone)).error(
256 format!(
257 "XML-RPC connection {} error: \
258 {}",
259 connection_id, err
260 ),
261 );
262 }
263 }
264 _ = crate::server::accept_bounded::watch_idle(
265 std::sync::Arc::clone(&activity),
266 IDLE_BETWEEN_REQUESTS_TIMEOUT,
267 ) => {
268 Log::new(Some(&status_clone)).debug(format!(
269 "XML-RPC connection {} idle for {}s; closing",
270 connection_id,
271 IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
272 ));
273 }
274 }
275 }
276
277 state_clone
279 .close_connection_on_server(server_id, connection_id)
280 .await;
281 Log::new(Some(&status_clone))
282 .info(format!("XML-RPC connection {} closed", connection_id));
283 let _ = status_clone.send("__UPDATE_UI__".to_string());
284 })
285 .await;
286 }
287 Err(e) => {
288 Log::new(Some(&status_tx))
289 .error(format!("Failed to accept XML-RPC connection: {}", e));
290 break;
291 }
292 }
293 }
294 });
295
296 task_registrar
297 .register_server_task(server_id, accept_handle)
298 .await;
299
300 Ok(local_addr)
301 }
302}
303
304#[cfg(not(target_arch = "wasm32"))]
306async fn handle_xmlrpc_request(
307 req: Request<hyper::body::Incoming>,
308 connection_id: ConnectionId,
309 server_id: ServerId,
310 remote_addr: SocketAddr,
311 llm_client: OllamaClient,
312 app_state: Arc<AppState>,
313 status_tx: mpsc::UnboundedSender<String>,
314 protocol: Arc<XmlRpcProtocol>,
315) -> Result<Response<Full<Bytes>>> {
316 let (parts, body) = req.into_parts();
319 let body_bytes = http_body_util::Limited::new(body, MAX_REQUEST_BODY_BYTES)
320 .collect()
321 .await
322 .map_err(|e| anyhow::anyhow!("Failed to read request body: {}", e))?
323 .to_bytes();
324
325 let body_str = String::from_utf8_lossy(&body_bytes);
326
327 let log = Log::new(Some(&status_tx));
328 log.debug(format!(
329 "XML-RPC request: {} {} ({} bytes)",
330 parts.method,
331 parts.uri,
332 body_bytes.len()
333 ));
334
335 log.trace(format!("XML-RPC request body:\n{}", body_str));
337
338 if parts.method != hyper::Method::POST {
340 let fault_xml = generate_fault(-32600, "Invalid request: XML-RPC requires POST method");
341 Log::new(Some(&status_tx)).debug(format!(
342 "XML-RPC error: invalid method {} (expected POST)",
343 parts.method
344 ));
345 return Ok(Response::builder()
346 .status(200)
347 .header("Content-Type", "text/xml; charset=utf-8")
348 .body(Full::new(Bytes::from(fault_xml)))
349 .unwrap());
350 }
351
352 let method_call = match parse_method_call(&body_str) {
354 Ok(call) => call,
355 Err(e) => {
356 tracing::warn!("XML-RPC parse error (decision=reject_malformed_request): {e:#}");
357 Log::new(Some(&status_tx)).warn(format!("XML-RPC parse error: {}", e));
358 let fault_xml = generate_fault(-32700, "Parse error: malformed XML-RPC methodCall");
361 return Ok(Response::builder()
362 .status(200)
363 .header("Content-Type", "text/xml; charset=utf-8")
364 .body(Full::new(Bytes::from(fault_xml)))
365 .unwrap());
366 }
367 };
368
369 Log::new(Some(&status_tx)).debug(format!(
370 "XML-RPC method: {} ({} params)",
371 method_call.method_name,
372 method_call.params.len()
373 ));
374
375 let event = actions::create_method_call_event(&method_call);
377
378 let execution_result = match call_llm(
380 &llm_client,
381 &app_state,
382 server_id,
383 Some(connection_id),
384 &event,
385 protocol.as_ref(),
386 )
387 .await
388 {
389 Ok(result) => result,
390 Err(e) => {
391 let failure = crate::utils::WireFailure::classify(&e);
395 let (fault_code, decision) = if failure.is_overloaded() {
396 (FAULT_SERVER_BUSY, "fail_closed_llm_overloaded")
397 } else {
398 (FAULT_INTERNAL_ERROR, "fail_closed_llm_error")
399 };
400 tracing::error!(
401 "XML-RPC LLM error (decision={decision}, fault_code={fault_code}): {e:#}"
402 );
403 Log::new(Some(&status_tx))
404 .warn(format!("XML-RPC LLM error (decision={}): {}", decision, e));
405 let fault_xml = generate_fault(fault_code, failure.text());
407 return Ok(Response::builder()
408 .status(200)
409 .header("Content-Type", "text/xml; charset=utf-8")
410 .body(Full::new(Bytes::from(fault_xml)))
411 .unwrap());
412 }
413 };
414
415 for msg in execution_result.messages {
417 let _ = status_tx.send(msg);
418 }
419
420 fn first_output(result: &ActionResult) -> Option<String> {
423 match result {
424 ActionResult::Output(bytes) => Some(String::from_utf8_lossy(bytes).to_string()),
425 ActionResult::Multiple(inner) => inner.iter().find_map(first_output),
426 _ => None,
427 }
428 }
429 let mut response_xml = execution_result
430 .protocol_results
431 .iter()
432 .find_map(first_output)
433 .unwrap_or_default();
434
435 if response_xml.is_empty() {
437 tracing::warn!(
438 "XML-RPC: model produced no response action for method {} (decision=fail_closed_no_action, fault_code={})",
439 method_call.method_name,
440 FAULT_INTERNAL_ERROR
441 );
442 Log::new(Some(&status_tx)).warn(
443 "XML-RPC: LLM did not generate a response (decision=fail_closed_no_action)".to_string(),
444 );
445 response_xml = generate_fault(
446 FAULT_INTERNAL_ERROR,
447 crate::utils::WireFailure::Unavailable.text(),
448 );
449 }
450
451 let log = Log::new(Some(&status_tx));
452 log.trace(format!("XML-RPC response:\n{}", response_xml));
453 log.debug(format!(
454 "XML-RPC {} -> {} bytes",
455 method_call.method_name,
456 response_xml.len()
457 ));
458
459 Ok(Response::builder()
460 .status(200)
461 .header("Content-Type", "text/xml; charset=utf-8")
462 .body(Full::new(Bytes::from(response_xml)))
463 .unwrap())
464}
465
466#[derive(Debug, Clone)]
468pub struct MethodCall {
469 pub method_name: String,
470 pub params: Vec<XmlRpcValue>,
471}
472
473#[derive(Debug, Clone, PartialEq)]
475pub enum XmlRpcValue {
476 Int(i32),
477 I8(i64), Boolean(bool),
479 String(String),
480 Double(f64),
481 DateTime(String), Base64(Vec<u8>),
483 Array(Vec<XmlRpcValue>),
484 Struct(Vec<(String, XmlRpcValue)>), Nil, }
487
488#[cfg(not(target_arch = "wasm32"))]
497pub const MAX_VALUE_DEPTH: usize = 64;
498
499#[cfg(not(target_arch = "wasm32"))]
501enum Container {
502 Array(Vec<XmlRpcValue>),
503 Struct {
504 members: Vec<(String, XmlRpcValue)>,
505 pending_name: Option<String>,
506 },
507}
508
509#[cfg(not(target_arch = "wasm32"))]
525pub fn parse_method_call(xml: &str) -> Result<MethodCall> {
526 let mut reader = Reader::from_str(xml);
527 reader.config_mut().trim_text(true);
528
529 let mut method_name: Option<String> = None;
530 let mut params: Vec<XmlRpcValue> = Vec::new();
531 let mut buf = Vec::new();
532
533 let mut saw_method_call = false;
534 let mut method_call_closed = false;
535 let mut in_method_name = false;
536 let mut in_member_name = false;
537
538 let mut value_frames: Vec<Option<XmlRpcValue>> = Vec::new();
541 let mut containers: Vec<Container> = Vec::new();
542 let mut current_type: Option<Vec<u8>> = None;
544 let mut current_param: Option<XmlRpcValue> = None;
545
546 fn set_frame(frames: &mut [Option<XmlRpcValue>], value: XmlRpcValue) {
548 if let Some(frame) = frames.last_mut() {
549 *frame = Some(value);
550 }
551 }
552
553 fn emit(
555 containers: &mut [Container],
556 current_param: &mut Option<XmlRpcValue>,
557 value: XmlRpcValue,
558 ) {
559 match containers.last_mut() {
560 Some(Container::Array(items)) => items.push(value),
561 Some(Container::Struct {
562 members,
563 pending_name,
564 }) => {
565 let name = pending_name.take().unwrap_or_default();
566 members.push((name, value));
567 }
568 None => *current_param = Some(value),
569 }
570 }
571
572 loop {
573 match reader.read_event_into(&mut buf) {
574 Ok(XmlEvent::Start(ref e)) => {
575 let name = e.name().as_ref().to_vec();
576 match name.as_slice() {
577 b"methodCall" => saw_method_call = true,
578 b"methodName" => in_method_name = true,
579 b"param" => current_param = None,
580 b"value" | b"array" | b"struct" => {
587 if value_frames.len() + containers.len() >= MAX_VALUE_DEPTH {
588 return Err(anyhow::anyhow!(
589 "value nesting deeper than {} levels",
590 MAX_VALUE_DEPTH
591 ));
592 }
593 match name.as_slice() {
594 b"value" => {
595 value_frames.push(None);
596 current_type = None;
597 }
598 b"array" => containers.push(Container::Array(Vec::new())),
599 _ => containers.push(Container::Struct {
600 members: Vec::new(),
601 pending_name: None,
602 }),
603 }
604 }
605 b"name" => in_member_name = true,
606 b"i4" | b"int" | b"i8" | b"boolean" | b"string" | b"double"
607 | b"dateTime.iso8601" | b"base64" | b"nil" => {
608 current_type = Some(name);
609 }
610 _ => {}
611 }
612 }
613 Ok(XmlEvent::End(ref e)) => match e.name().as_ref() {
614 b"methodCall" => method_call_closed = true,
615 b"methodName" => in_method_name = false,
616 b"name" => in_member_name = false,
617 b"value" => {
618 let value = value_frames
621 .pop()
622 .flatten()
623 .unwrap_or_else(|| XmlRpcValue::String(String::new()));
624 emit(&mut containers, &mut current_param, value);
625 current_type = None;
626 }
627 b"param" => {
628 params.push(
629 current_param
630 .take()
631 .unwrap_or_else(|| XmlRpcValue::String(String::new())),
632 );
633 }
634 b"array" => {
635 if let Some(Container::Array(items)) = containers.pop() {
636 set_frame(&mut value_frames, XmlRpcValue::Array(items));
637 }
638 }
639 b"struct" => {
640 if let Some(Container::Struct { members, .. }) = containers.pop() {
641 set_frame(&mut value_frames, XmlRpcValue::Struct(members));
642 }
643 }
644 tag @ (b"i4" | b"int" | b"i8" | b"boolean" | b"string" | b"double"
645 | b"dateTime.iso8601" | b"base64" | b"nil") => {
646 if value_frames.last().map(|f| f.is_none()).unwrap_or(false) {
648 set_frame(&mut value_frames, empty_typed_value(tag));
649 }
650 current_type = None;
651 }
652 _ => {}
653 },
654 Ok(XmlEvent::Text(e)) => {
655 let text = e.unescape()?.to_string();
656 if in_method_name {
657 method_name = Some(text);
658 } else if in_member_name {
659 if let Some(Container::Struct { pending_name, .. }) = containers.last_mut() {
660 *pending_name = Some(text);
661 }
662 } else if !value_frames.is_empty() {
663 let value = typed_value(current_type.as_deref(), &text)?;
664 set_frame(&mut value_frames, value);
665 }
666 }
667 Ok(XmlEvent::Empty(ref e)) => {
668 let tag = e.name().as_ref().to_vec();
670 if !value_frames.is_empty() {
671 match tag.as_slice() {
672 b"i4" | b"int" | b"i8" | b"boolean" | b"string" | b"double"
673 | b"dateTime.iso8601" | b"base64" | b"nil" => {
674 set_frame(&mut value_frames, empty_typed_value(&tag));
675 }
676 b"value" => {
677 emit(
678 &mut containers,
679 &mut current_param,
680 XmlRpcValue::String(String::new()),
681 );
682 }
683 _ => {}
684 }
685 } else if tag.as_slice() == b"value" {
686 emit(
687 &mut containers,
688 &mut current_param,
689 XmlRpcValue::String(String::new()),
690 );
691 }
692 }
693 Ok(XmlEvent::Eof) => break,
694 Err(e) => return Err(anyhow::anyhow!("XML parse error: {}", e)),
695 _ => {}
696 }
697 buf.clear();
698 }
699
700 if !saw_method_call {
704 return Err(anyhow::anyhow!("not an XML-RPC methodCall document"));
705 }
706 if !method_call_closed {
709 return Err(anyhow::anyhow!("truncated methodCall document"));
710 }
711 let method_name = match method_name {
712 Some(name) if !name.is_empty() => name,
713 _ => return Err(anyhow::anyhow!("methodCall has no methodName")),
714 };
715 if !value_frames.is_empty() || !containers.is_empty() {
716 return Err(anyhow::anyhow!("unterminated value, array or struct"));
717 }
718
719 Ok(MethodCall {
720 method_name,
721 params,
722 })
723}
724
725#[cfg(not(target_arch = "wasm32"))]
727fn typed_value(tag: Option<&[u8]>, text: &str) -> Result<XmlRpcValue> {
728 let parse_err = |ty: &str| anyhow::anyhow!("invalid <{}> value: {:?}", ty, text);
729 Ok(match tag {
730 None | Some(b"string") => XmlRpcValue::String(text.to_string()),
732 Some(b"i4") | Some(b"int") => {
733 XmlRpcValue::Int(text.trim().parse::<i32>().map_err(|_| parse_err("int"))?)
734 }
735 Some(b"i8") => XmlRpcValue::I8(text.trim().parse::<i64>().map_err(|_| parse_err("i8"))?),
736 Some(b"boolean") => match text.trim() {
737 "1" | "true" => XmlRpcValue::Boolean(true),
738 "0" | "false" => XmlRpcValue::Boolean(false),
739 _ => return Err(parse_err("boolean")),
740 },
741 Some(b"double") => {
742 let d = text
743 .trim()
744 .parse::<f64>()
745 .map_err(|_| parse_err("double"))?;
746 if !d.is_finite() {
747 return Err(parse_err("double"));
748 }
749 XmlRpcValue::Double(d)
750 }
751 Some(b"dateTime.iso8601") => XmlRpcValue::DateTime(text.trim().to_string()),
752 Some(b"base64") => {
753 use base64::Engine;
754 XmlRpcValue::Base64(
755 base64::engine::general_purpose::STANDARD
756 .decode(text.trim())
757 .map_err(|_| parse_err("base64"))?,
758 )
759 }
760 Some(b"nil") => XmlRpcValue::Nil,
761 Some(_) => XmlRpcValue::String(text.to_string()),
762 })
763}
764
765#[cfg(not(target_arch = "wasm32"))]
767fn empty_typed_value(tag: &[u8]) -> XmlRpcValue {
768 match tag {
769 b"i4" | b"int" => XmlRpcValue::Int(0),
770 b"i8" => XmlRpcValue::I8(0),
771 b"boolean" => XmlRpcValue::Boolean(false),
772 b"double" => XmlRpcValue::Double(0.0),
773 b"base64" => XmlRpcValue::Base64(Vec::new()),
774 b"nil" => XmlRpcValue::Nil,
775 _ => XmlRpcValue::String(String::new()),
776 }
777}
778
779#[cfg(not(target_arch = "wasm32"))]
787pub fn generate_fault(code: i32, message: &str) -> String {
788 format!(
789 r#"<?xml version="1.0"?>
790<methodResponse>
791 <fault>
792 <value>
793 <struct>
794 <member>
795 <name>faultCode</name>
796 <value><int>{}</int></value>
797 </member>
798 <member>
799 <name>faultString</name>
800 <value><string>{}</string></value>
801 </member>
802 </struct>
803 </value>
804 </fault>
805</methodResponse>"#,
806 code,
807 escape_xml_text(message)
808 )
809}
810
811#[cfg(not(target_arch = "wasm32"))]
813fn escape_xml_text(text: &str) -> String {
814 let mut out = String::with_capacity(text.len());
815 for c in text.chars() {
816 match c {
817 '&' => out.push_str("&"),
818 '<' => out.push_str("<"),
819 '>' => out.push_str(">"),
820 '"' => out.push_str("""),
821 '\'' => out.push_str("'"),
822 _ => out.push(c),
823 }
824 }
825 out
826}
827
828#[cfg(not(target_arch = "wasm32"))]
830pub fn generate_success_response(value: &XmlRpcValue) -> String {
831 let mut writer = Writer::new(Cursor::new(Vec::new()));
832
833 writer
835 .write_event(XmlEvent::Decl(quick_xml::events::BytesDecl::new(
836 "1.0", None, None,
837 )))
838 .unwrap();
839
840 writer
842 .write_event(XmlEvent::Start(BytesStart::new("methodResponse")))
843 .unwrap();
844 writer
845 .write_event(XmlEvent::Start(BytesStart::new("params")))
846 .unwrap();
847 writer
848 .write_event(XmlEvent::Start(BytesStart::new("param")))
849 .unwrap();
850
851 write_value(&mut writer, value);
852
853 writer
854 .write_event(XmlEvent::End(BytesEnd::new("param")))
855 .unwrap();
856 writer
857 .write_event(XmlEvent::End(BytesEnd::new("params")))
858 .unwrap();
859 writer
860 .write_event(XmlEvent::End(BytesEnd::new("methodResponse")))
861 .unwrap();
862
863 String::from_utf8(writer.into_inner().into_inner()).unwrap()
864}
865
866#[cfg(not(target_arch = "wasm32"))]
868fn write_value(writer: &mut Writer<Cursor<Vec<u8>>>, value: &XmlRpcValue) {
869 writer
870 .write_event(XmlEvent::Start(BytesStart::new("value")))
871 .unwrap();
872
873 match value {
874 XmlRpcValue::Int(i) => {
875 writer
876 .write_event(XmlEvent::Start(BytesStart::new("int")))
877 .unwrap();
878 writer
879 .write_event(XmlEvent::Text(BytesText::new(&i.to_string())))
880 .unwrap();
881 writer
882 .write_event(XmlEvent::End(BytesEnd::new("int")))
883 .unwrap();
884 }
885 XmlRpcValue::I8(i) => {
886 writer
887 .write_event(XmlEvent::Start(BytesStart::new("i8")))
888 .unwrap();
889 writer
890 .write_event(XmlEvent::Text(BytesText::new(&i.to_string())))
891 .unwrap();
892 writer
893 .write_event(XmlEvent::End(BytesEnd::new("i8")))
894 .unwrap();
895 }
896 XmlRpcValue::Boolean(b) => {
897 writer
898 .write_event(XmlEvent::Start(BytesStart::new("boolean")))
899 .unwrap();
900 writer
901 .write_event(XmlEvent::Text(BytesText::new(if *b { "1" } else { "0" })))
902 .unwrap();
903 writer
904 .write_event(XmlEvent::End(BytesEnd::new("boolean")))
905 .unwrap();
906 }
907 XmlRpcValue::String(s) => {
908 writer
909 .write_event(XmlEvent::Start(BytesStart::new("string")))
910 .unwrap();
911 writer
912 .write_event(XmlEvent::Text(BytesText::new(s)))
913 .unwrap();
914 writer
915 .write_event(XmlEvent::End(BytesEnd::new("string")))
916 .unwrap();
917 }
918 XmlRpcValue::Double(d) => {
919 writer
920 .write_event(XmlEvent::Start(BytesStart::new("double")))
921 .unwrap();
922 writer
923 .write_event(XmlEvent::Text(BytesText::new(&d.to_string())))
924 .unwrap();
925 writer
926 .write_event(XmlEvent::End(BytesEnd::new("double")))
927 .unwrap();
928 }
929 XmlRpcValue::DateTime(dt) => {
930 writer
931 .write_event(XmlEvent::Start(BytesStart::new("dateTime.iso8601")))
932 .unwrap();
933 writer
934 .write_event(XmlEvent::Text(BytesText::new(dt)))
935 .unwrap();
936 writer
937 .write_event(XmlEvent::End(BytesEnd::new("dateTime.iso8601")))
938 .unwrap();
939 }
940 XmlRpcValue::Base64(bytes) => {
941 use base64::Engine;
942 let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
943 writer
944 .write_event(XmlEvent::Start(BytesStart::new("base64")))
945 .unwrap();
946 writer
947 .write_event(XmlEvent::Text(BytesText::new(&encoded)))
948 .unwrap();
949 writer
950 .write_event(XmlEvent::End(BytesEnd::new("base64")))
951 .unwrap();
952 }
953 XmlRpcValue::Array(arr) => {
954 writer
955 .write_event(XmlEvent::Start(BytesStart::new("array")))
956 .unwrap();
957 writer
958 .write_event(XmlEvent::Start(BytesStart::new("data")))
959 .unwrap();
960 for item in arr {
961 write_value(writer, item);
962 }
963 writer
964 .write_event(XmlEvent::End(BytesEnd::new("data")))
965 .unwrap();
966 writer
967 .write_event(XmlEvent::End(BytesEnd::new("array")))
968 .unwrap();
969 }
970 XmlRpcValue::Struct(members) => {
971 writer
972 .write_event(XmlEvent::Start(BytesStart::new("struct")))
973 .unwrap();
974 for (name, val) in members {
975 writer
976 .write_event(XmlEvent::Start(BytesStart::new("member")))
977 .unwrap();
978 writer
979 .write_event(XmlEvent::Start(BytesStart::new("name")))
980 .unwrap();
981 writer
982 .write_event(XmlEvent::Text(BytesText::new(name)))
983 .unwrap();
984 writer
985 .write_event(XmlEvent::End(BytesEnd::new("name")))
986 .unwrap();
987 write_value(writer, val);
988 writer
989 .write_event(XmlEvent::End(BytesEnd::new("member")))
990 .unwrap();
991 }
992 writer
993 .write_event(XmlEvent::End(BytesEnd::new("struct")))
994 .unwrap();
995 }
996 XmlRpcValue::Nil => {
997 writer
998 .write_event(XmlEvent::Empty(BytesStart::new("nil")))
999 .unwrap();
1000 }
1001 }
1002
1003 writer
1004 .write_event(XmlEvent::End(BytesEnd::new("value")))
1005 .unwrap();
1006}