Files
vibetorrent/backend/src/xmlrpc.rs

350 lines
8.6 KiB
Rust

use crate::scgi::{send_request, ScgiError, ScgiRequest};
use quick_xml::de::from_str;
use quick_xml::se::to_string;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum XmlRpcError {
#[error("SCGI Error: {0}")]
Scgi(#[from] ScgiError),
#[error("Serialization Error: {0}")]
Serialization(String),
#[error("Deserialization Error: {0}")]
Deserialization(#[from] quick_xml::de::DeError),
#[error("XML Parse Error: {0}")]
Parse(String),
}
// --- Request Parameters Enum ---
#[derive(Debug, Clone)]
pub enum RpcParam {
String(String),
Int(i64),
}
impl From<&str> for RpcParam {
fn from(s: &str) -> Self {
RpcParam::String(s.to_string())
}
}
impl From<String> for RpcParam {
fn from(s: String) -> Self {
RpcParam::String(s)
}
}
impl From<i64> for RpcParam {
fn from(i: i64) -> Self {
RpcParam::Int(i)
}
}
impl From<i32> for RpcParam {
fn from(i: i32) -> Self {
RpcParam::Int(i as i64)
}
}
// --- Request Models ---
#[derive(Debug, Serialize)]
#[serde(rename = "methodCall")]
struct MethodCall<'a> {
#[serde(rename = "methodName")]
method_name: &'a str,
params: RequestParams<'a>,
}
#[derive(Debug, Serialize)]
struct RequestParams<'a> {
param: Vec<RequestParam<'a>>,
}
#[derive(Debug, Serialize)]
struct RequestParam<'a> {
value: RequestValueInner<'a>,
}
#[derive(Debug, Serialize)]
struct RequestValueInner<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
string: Option<&'a str>,
// rTorrent standard is often i4 (32-bit signed int). i8 might not be supported by all XML-RPC libs.
// Casting i64 to i32 is safe for typical speed limits.
#[serde(skip_serializing_if = "Option::is_none")]
i4: Option<i32>,
}
// --- Response Models for d.multicall2 ---
#[derive(Debug, Deserialize)]
#[serde(rename = "methodResponse")]
struct MulticallResponse {
params: MulticallResponseParams,
}
#[derive(Debug, Deserialize)]
struct MulticallResponseParams {
param: MulticallResponseParam,
}
#[derive(Debug, Deserialize)]
struct MulticallResponseParam {
value: MulticallResponseValueArray,
}
#[derive(Debug, Deserialize)]
struct MulticallResponseValueArray {
array: MulticallResponseDataOuter,
}
#[derive(Debug, Deserialize)]
struct MulticallResponseDataOuter {
data: MulticallResponseDataOuterValue,
}
#[derive(Debug, Deserialize)]
struct MulticallResponseDataOuterValue {
#[serde(rename = "value", default)]
values: Vec<MulticallRowValue>,
}
#[derive(Debug, Deserialize)]
struct MulticallRowValue {
array: MulticallResponseDataInner,
}
#[derive(Debug, Deserialize)]
struct MulticallResponseDataInner {
data: MulticallResponseDataInnerValue,
}
#[derive(Debug, Deserialize)]
struct MulticallResponseDataInnerValue {
#[serde(rename = "value", default)]
values: Vec<MulticallItemValue>,
}
#[derive(Debug, Deserialize)]
struct MulticallItemValue {
#[serde(rename = "string", default)]
string: Option<String>,
#[serde(rename = "i4", default)]
i4: Option<i64>,
#[serde(rename = "i8", default)]
i8: Option<i64>,
}
impl MulticallItemValue {
fn to_string_lossy(&self) -> String {
if let Some(s) = &self.string {
s.clone()
} else if let Some(i) = self.i4 {
i.to_string()
} else if let Some(i) = self.i8 {
i.to_string()
} else {
String::new()
}
}
}
// --- Response Model for simple string ---
#[derive(Debug, Deserialize)]
#[serde(rename = "methodResponse")]
struct StringResponse {
params: StringResponseParams,
}
#[derive(Debug, Deserialize)]
struct StringResponseParams {
param: StringResponseParam,
}
#[derive(Debug, Deserialize)]
struct StringResponseParam {
value: StringResponseValue,
}
#[derive(Debug, Deserialize)]
struct StringResponseValue {
string: String,
}
// --- Response Model for simple integer (i8/i4) ---
#[derive(Debug, Deserialize)]
#[serde(rename = "methodResponse")]
struct IntegerResponse {
params: IntegerResponseParams,
}
#[derive(Debug, Deserialize)]
struct IntegerResponseParams {
param: IntegerResponseParam,
}
#[derive(Debug, Deserialize)]
struct IntegerResponseParam {
value: IntegerResponseValue,
}
#[derive(Debug, Deserialize)]
struct IntegerResponseValue {
#[serde(rename = "i8", default)]
i8: Option<i64>,
#[serde(rename = "i4", default)]
i4: Option<i64>,
#[serde(rename = "string", default)]
string: Option<String>,
}
// --- Client Implementation ---
pub struct RtorrentClient {
socket_path: String,
}
impl RtorrentClient {
pub fn new(socket_path: &str) -> Self {
Self {
socket_path: socket_path.to_string(),
}
}
/// Helper to build and serialize XML-RPC method call
fn build_method_call(&self, method: &str, params: &[RpcParam]) -> Result<String, XmlRpcError> {
let req_params = RequestParams {
param: params
.iter()
.map(|p| RequestParam {
value: match p {
RpcParam::String(s) => RequestValueInner {
string: Some(s),
i4: None,
},
RpcParam::Int(i) => RequestValueInner {
string: None,
i4: Some(*i as i32),
},
},
})
.collect(),
};
let call = MethodCall {
method_name: method,
params: req_params,
};
let xml_body = to_string(&call).map_err(|e| XmlRpcError::Serialization(e.to_string()))?;
Ok(format!("<?xml version=\"1.0\"?>\n{}", xml_body))
}
pub async fn call(&self, method: &str, params: &[RpcParam]) -> Result<String, XmlRpcError> {
let xml = self.build_method_call(method, params)?;
let req = ScgiRequest::new().body(xml.into_bytes());
let bytes = send_request(&self.socket_path, req).await?;
let s = String::from_utf8_lossy(&bytes).to_string();
Ok(s)
}
}
pub fn parse_multicall_response(xml: &str) -> Result<Vec<Vec<String>>, XmlRpcError> {
let response: MulticallResponse = from_str(xml)?;
let mut result = Vec::new();
for row in response.params.param.value.array.data.values {
let mut row_vec = Vec::new();
for item in row.array.data.values {
row_vec.push(item.to_string_lossy());
}
result.push(row_vec);
}
Ok(result)
}
pub fn parse_string_response(xml: &str) -> Result<String, XmlRpcError> {
let response: StringResponse = from_str(xml)?;
Ok(response.params.param.value.string)
}
pub fn parse_i64_response(xml: &str) -> Result<i64, XmlRpcError> {
let response: IntegerResponse = from_str(xml)?;
if let Some(val) = response.params.param.value.i8 {
Ok(val)
} else if let Some(val) = response.params.param.value.i4 {
Ok(val)
} else if let Some(ref s) = response.params.param.value.string {
s.parse()
.map_err(|_| XmlRpcError::Parse("Not an integer string".to_string()))
} else {
Err(XmlRpcError::Parse("No integer value found".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_method_call() {
let client = RtorrentClient::new("dummy");
let params = vec![
RpcParam::String("".to_string()),
RpcParam::String("main".to_string()),
RpcParam::String("d.name=".to_string()),
];
let xml = client.build_method_call("d.multicall2", &params).unwrap();
assert!(xml.contains("<methodName>d.multicall2</methodName>"));
assert!(xml.contains("<value><string>main</string></value>"));
}
#[test]
fn test_build_method_call_int() {
let client = RtorrentClient::new("dummy");
let params = vec![RpcParam::Int(1024)];
let xml = client.build_method_call("test.int", &params).unwrap();
// Should produce <value><i4>1024</i4></value>
assert!(xml.contains("<i4>1024</i4>"));
}
#[test]
fn test_parse_multicall_response() {
let xml = r#"<methodResponse>
<params>
<param>
<value>
<array>
<data>
<value>
<array>
<data>
<value><string>HASH123</string></value>
<value><string>Ubuntu ISO</string></value>
<value><i4>1024</i4></value>
</data>
</array>
</value>
</data>
</array>
</value>
</param>
</params>
</methodResponse>
"#;
let result = parse_multicall_response(xml).expect("Failed to parse");
assert_eq!(result.len(), 1);
assert_eq!(result[0][0], "HASH123");
assert_eq!(result[0][1], "Ubuntu ISO");
assert_eq!(result[0][2], "1024");
}
}