use debversion::Version;
use lazy_regex::regex_is_match;
use maplit::hashmap;
use crate::BugId;
use std::collections::HashMap;
use xmltree::{Element, XMLNode};
#[allow(dead_code)]
pub const XMLNS_SOAP: &str = "http://xml.apache.org/xml-soap";
pub const XMLNS_SOAPENV: &str = "http://schemas.xmlsoap.org/soap/envelope/";
pub const XMLNS_SOAPENC: &str = "http://schemas.xmlsoap.org/soap/encoding/";
pub const XMLNS_XSI: &str = "http://www.w3.org/1999/XMLSchema-instance";
pub const XMLNS_XSD: &str = "http://www.w3.org/1999/XMLSchema";
pub const XMLNS_DEBBUGS: &str = "Debbugs/SOAP";
#[derive(Debug, PartialEq)]
pub struct Fault {
pub faultcode: String,
pub faultstring: String,
pub faultactor: Option<String>,
pub detail: Option<String>,
}
impl std::fmt::Display for Fault {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{{ faultcode: {}, faultstring: {}, faultactor: {:?}, detail: {:?} }}",
self.faultcode, self.faultstring, self.faultactor, self.detail
)
}
}
fn parse_bool(s: &str) -> Result<bool, String> {
match s {
"1" => Ok(true),
"0" => Ok(false),
_ => Err(format!("Invalid boolean value: {}", s)),
}
}
pub(crate) fn parse_fault(input: &str) -> Result<Fault, String> {
// Parse the input XML string into an Element
let root = Element::parse(input.as_bytes()).map_err(|e| e.to_string())?;
if root.name != "Envelope" || root.namespace.as_deref() != Some(XMLNS_SOAPENV) {
return Err("Root element is not a valid soap:Envelope".into());
}
let body_elem = root.get_child("Body").ok_or("soap:Body not found")?;
if body_elem.namespace.as_deref() != Some(XMLNS_SOAPENV) {
return Err(format!(
"Namespace for soap:Body is incorrect: {:?}",
body_elem.namespace
));
}
let fault = body_elem.get_child("Fault").ok_or("soap:Fault not found")?;
let faultcode = fault.get_child("faultcode").ok_or("faultcode not found")?;
let faultstring = fault
.get_child("faultstring")
.ok_or("faultstring not found")?;
let faultactor = fault.get_child("faultactor");
let detail = fault.get_child("detail");
Ok(Fault {
faultcode: faultcode
.get_text()
.ok_or("faultcode has no text")?
.into_owned(),
faultstring: faultstring
.get_text()
.ok_or("faultstring has no text")?
.into_owned(),
faultactor: faultactor
.and_then(|s| s.get_text())
.map(|s| s.into_owned()),
detail: detail.and_then(|s| s.get_text()).map(|s| s.into_owned()),
})
}
fn build_request_envelope(name: &str, arguments: Vec<Element>) -> xmltree::Element {
let mut namespace = xmltree::Namespace::empty();
namespace.put("soap", XMLNS_SOAPENV);
namespace.put("xsi", XMLNS_XSI);
namespace.put("xsd", XMLNS_XSD);
Element {
name: "Envelope".to_string(),
prefix: Some("soap".to_string()),
namespaces: Some(namespace.clone()),
namespace: Some(XMLNS_SOAPENV.to_string()),
attributes: hashmap! {},
children: vec![
XMLNode::Element(Element {
name: "Header".to_string(),
prefix: Some("soap".to_string()),
namespaces: Some(namespace.clone()),
namespace: Some(XMLNS_SOAPENV.to_string()),
attributes: hashmap![],
children: vec![],
}),
XMLNode::Element(Element {
name: "Body".to_string(),
prefix: Some("soap".to_string()),
namespaces: Some(namespace.clone()),
namespace: Some(XMLNS_SOAPENV.to_string()),
attributes: hashmap![],
children: vec![XMLNode::Element(Element {
name: name.to_string(),
namespaces: None,
children: arguments.into_iter().map(XMLNode::Element).collect(),
prefix: None,
namespace: None,
attributes: hashmap! {},
})],
}),
],
}
}
pub(crate) fn newest_bugs_request(amount: i32) -> xmltree::Element {
build_request_envelope(
"newest_bugs",
vec![Element {
name: "amount".to_string(),
namespaces: None,
children: vec![XMLNode::Text(amount.to_string())],
prefix: None,
namespace: None,
attributes: hashmap! {},
}],
)
}
pub(crate) fn get_bug_log_request(bugid: i32) -> xmltree::Element {
let mut namespace = xmltree::Namespace::empty();
namespace.put("xsi", XMLNS_XSI);
namespace.put("xsd", XMLNS_XSD);
build_request_envelope(
"get_bug_log",
vec![Element {
name: "bugnumber".to_string(),
namespaces: Some(namespace.clone()),
children: vec![XMLNode::Text(bugid.to_string())],
prefix: None,
namespace: None,
attributes: hashmap! {
"xsi:type".into() => "xsd:int".into(),
},
}],
)
}
#[test]
fn test_newest_bugs_request_serialize() {
let request = newest_bugs_request(10);
assert_eq!(request.name, "Envelope");
assert_eq!(request.namespace.as_deref(), Some(XMLNS_SOAPENV));
assert_eq!(request.children.len(), 2);
let header = request.children[0].as_element().unwrap();
assert_eq!(header.name, "Header");
let body = request.children[1].as_element().unwrap();
assert_eq!(body.name, "Body");
assert_eq!(body.namespace.as_deref(), Some(XMLNS_SOAPENV));
assert_eq!(body.children.len(), 1);
let newest_bugs = body.children[0].as_element().unwrap();
assert_eq!(newest_bugs.name, "newest_bugs");
assert_eq!(newest_bugs.namespace, None);
assert_eq!(newest_bugs.children.len(), 1);
let amount = newest_bugs.children[0].as_element().unwrap();
assert_eq!(amount.name, "amount");
assert_eq!(amount.namespace, None);
assert_eq!(amount.children.len(), 1);
assert_eq!(amount.children[0].as_text().unwrap(), "10");
}
fn parse_response_envelope(input: &str, name: &str) -> Result<xmltree::Element, String> {
// Parse the input XML string into an Element
let root = Element::parse(input.as_bytes()).map_err(|e| e.to_string())?;
if root.name != "Envelope" || root.namespace.as_deref() != Some(XMLNS_SOAPENV) {
return Err("Root element is not a valid soap:Envelope".into());
}
let body_elem = root.get_child("Body").ok_or("soap:Body not found")?;
if body_elem.namespace.as_deref() != Some(XMLNS_SOAPENV) {
return Err(format!(
"Namespace for soap:Body is incorrect: {:?}",
body_elem.namespace
));
}
let elem_name = format!("{}Response", name);
body_elem
.get_child(elem_name.as_str())
.ok_or(format!("{} not found", elem_name))
.cloned()
}
pub(crate) fn parse_newest_bugs_response(input: &str) -> Result<Vec<i32>, String> {
let response_elem = parse_response_envelope(input, "newest_bugs")?;
let array_elem = response_elem
.get_child("Array")
.ok_or("soapenc:Array not found")?;
if array_elem.namespace.as_deref() != Some(XMLNS_SOAPENC) {
return Err(format!(
"Namespace for soapenc:Array is incorrect: {:?}",
array_elem.namespace
));
}
match array_elem.attributes.get("arrayType") {
None => return Err("soapenc:Array does not have soapenc:arrayType attribute".to_string()),
Some(value) => {
if !regex_is_match!(r"xsd:int\[[0-9]+\]", value) && value != "xsd:anyType[0]" {
return Err(format!(
"soapenc:Array has incorrect soapenc:arrayType attribute: {}",
value
));
}
}
}
// Extract the integers from the item elements
let mut integers = Vec::new();
for item in array_elem.children.iter() {
if let xmltree::XMLNode::Element(e) = item {
if e.name == "item" {
if let Some(text) = e.get_text() {
if let Ok(num) = text.parse() {
integers.push(num);
}
}
}
}
}
Ok(integers)
}
#[test]
fn test_parse_newest_bugs_response() {
let text = r###"<?xml version="1.0" encoding="UTF-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soap:Body><newest_bugsResponse xmlns="Debbugs/SOAP"><soapenc:Array soapenc:arrayType="xsd:int[10]" xsi:type="soapenc:Array"><item xsi:type="xsd:int">66320</item><item xsi:type="xsd:int">66321</item><item xsi:type="xsd:int">66322</item><item xsi:type="xsd:int">66323</item><item xsi:type="xsd:int">66324</item><item xsi:type="xsd:int">66325</item><item xsi:type="xsd:int">66326</item><item xsi:type="xsd:int">66327</item><item xsi:type="xsd:int">66328</item><item xsi:type="xsd:int">66329</item></soapenc:Array></newest_bugsResponse></soap:Body></soap:Envelope>"###;
let integers = parse_newest_bugs_response(text).unwrap();
assert_eq!(
integers,
vec![66320, 66321, 66322, 66323, 66324, 66325, 66326, 66327, 66328, 66329]
);
}
#[test]
fn test_get_bug_log_request() {
let request = get_bug_log_request(123456);
assert_eq!(request.name, "Envelope");
assert_eq!(request.namespace.as_deref(), Some(XMLNS_SOAPENV));
let body = request.children[1].as_element().unwrap();
let get_bug_log = body.children[0].as_element().unwrap();
assert_eq!(get_bug_log.name, "get_bug_log");
let bugnumber = get_bug_log.children[0].as_element().unwrap();
assert_eq!(bugnumber.name, "bugnumber");
assert_eq!(bugnumber.children[0].as_text().unwrap(), "123456");
assert_eq!(bugnumber.attributes.get("xsi:type").unwrap(), "xsd:int");
}
#[test]
fn test_get_bugs_request() {
let query = SearchQuery {
package: Some("test-package"),
owner: Some("test@example.com"),
..Default::default()
};
let request = get_bugs_request(&query);
assert_eq!(request.name, "Envelope");
let body = request.children[1].as_element().unwrap();
let get_bugs = body.children[0].as_element().unwrap();
assert_eq!(get_bugs.name, "get_bugs");
// Check that arguments are present (package + owner = 4 args: "package", value, "owner", value)
let args: Vec<&Element> = get_bugs
.children
.iter()
.filter_map(|c| c.as_element())
.collect();
assert_eq!(args.len(), 4); // Should have package key, package value, owner key, owner value
}
#[test]
fn test_get_status_request_single() {
let request = get_status_request(&[123456]);
assert_eq!(request.name, "Envelope");
let body = request.children[1].as_element().unwrap();
let get_status = body.children[0].as_element().unwrap();
assert_eq!(get_status.name, "get_status");
let bugnumbers = get_status.children[0].as_element().unwrap();
assert_eq!(bugnumbers.name, "arg0"); // The array is passed as the first argument
let item = bugnumbers.children[0].as_element().unwrap();
assert_eq!(item.name, "item");
assert_eq!(item.children[0].as_text().unwrap(), "123456");
}
#[test]
fn test_get_status_request_multiple() {
let request = get_status_request(&[123, 456, 789]);
let body = request.children[1].as_element().unwrap();
let get_status = body.children[0].as_element().unwrap();
let bugnumbers = get_status.children[0].as_element().unwrap();
assert_eq!(bugnumbers.name, "arg0");
let items: Vec<i32> = bugnumbers
.children
.iter()
.filter_map(|c| c.as_element())
.filter_map(|e| e.children[0].as_text())
.filter_map(|t| t.parse().ok())
.collect();
assert_eq!(items, vec![123, 456, 789]);
}
#[test]
fn test_get_usertag_request() {
let request = get_usertag_request("user@example.com", &["tag1", "tag2"]);
assert_eq!(request.name, "Envelope");
let body = request.children[1].as_element().unwrap();
let get_usertag = body.children[0].as_element().unwrap();
assert_eq!(get_usertag.name, "get_usertag");
// First arg should be email
let email_arg = get_usertag.children[0].as_element().unwrap();
assert_eq!(email_arg.children[0].as_text().unwrap(), "user@example.com");
// Following args should be tags
let tag1_arg = get_usertag.children[1].as_element().unwrap();
assert_eq!(tag1_arg.children[0].as_text().unwrap(), "tag1");
let tag2_arg = get_usertag.children[2].as_element().unwrap();
assert_eq!(tag2_arg.children[0].as_text().unwrap(), "tag2");
}
#[test]
fn test_to_arg_xml_string() {
let s = "test string";
let elem = s.to_arg_xml("test_arg".to_string());
assert_eq!(elem.name, "test_arg");
assert_eq!(elem.children[0].as_text().unwrap(), "test string");
}
#[test]
fn test_to_arg_xml_str_array() {
let v = ["a", "b"];
let slice: &[&str] = &v;
let elem = slice.to_arg_xml("test_array".to_string());
assert_eq!(elem.name, "test_array");
assert!(elem.attributes.get("xsi:type").unwrap().contains("Array"));
let items: Vec<String> = elem
.children
.iter()
.filter_map(|c| c.as_element())
.filter_map(|e| e.children[0].as_text())
.map(|t| t.to_string())
.collect();
assert_eq!(items, vec!["a", "b"]);
}
#[test]
fn test_add_arg_xml() {
let mut params = Vec::new();
add_arg_xml(&mut params, "test value");
assert_eq!(params.len(), 1);
assert_eq!(params[0].name, "arg0");
assert_eq!(params[0].children[0].as_text().unwrap(), "test value");
add_arg_xml(&mut params, "second value");
assert_eq!(params.len(), 2);
assert_eq!(params[1].name, "arg1");
assert_eq!(params[1].children[0].as_text().unwrap(), "second value");
}
#[test]
fn test_parse_fault() {
let fault_xml = r###"<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>Client</faultcode>
<faultstring>Invalid request</faultstring>
<faultactor>http://bugs.debian.org</faultactor>
<detail>Bug ID not found</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>"###;
let fault = parse_fault(fault_xml).unwrap();
assert_eq!(fault.faultcode, "Client");
assert_eq!(fault.faultstring, "Invalid request");
assert_eq!(fault.faultactor, Some("http://bugs.debian.org".to_string()));
assert_eq!(fault.detail, Some("Bug ID not found".to_string()));
}
#[test]
fn test_parse_fault_minimal() {
let fault_xml = r###"<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>Server</faultcode>
<faultstring>Internal error</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>"###;
let fault = parse_fault(fault_xml).unwrap();
assert_eq!(fault.faultcode, "Server");
assert_eq!(fault.faultstring, "Internal error");
assert_eq!(fault.faultactor, None);
assert_eq!(fault.detail, None);
}
#[test]
fn test_parse_get_bug_log_response() {
let xml = r###"<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Body>
<get_bug_logResponse xmlns="Debbugs/SOAP">
<soapenc:Array soapenc:arrayType="xsd:ur-type[1]" xsi:type="soapenc:Array">
<item>
<header>Subject: Test bug</header>
<body>This is a test bug report.</body>
<msg_num>1</msg_num>
</item>
</soapenc:Array>
</get_bug_logResponse>
</soap:Body>
</soap:Envelope>"###;
let bug_log = parse_get_bug_log_response(xml).unwrap();
assert_eq!(bug_log.len(), 1);
assert_eq!(bug_log[0].header, "Subject: Test bug");
assert_eq!(bug_log[0].body, "This is a test bug report.");
assert_eq!(bug_log[0].msgnum, 1);
}
#[test]
fn test_parse_get_bugs_response() {
let xml = r###"<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Body>
<get_bugsResponse xmlns="Debbugs/SOAP">
<soapenc:Array soapenc:arrayType="xsd:int[3]" xsi:type="soapenc:Array">
<item xsi:type="xsd:int">123</item>
<item xsi:type="xsd:int">456</item>
<item xsi:type="xsd:int">789</item>
</soapenc:Array>
</get_bugsResponse>
</soap:Body>
</soap:Envelope>"###;
let bug_ids = parse_get_bugs_response(xml).unwrap();
assert_eq!(bug_ids, vec![123, 456, 789]);
}
#[test]
fn test_parse_get_status_response() {
let xml = r###"<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<get_statusResponse xmlns="Debbugs/SOAP">
<s-gensym3>
<item>
<key>123</key>
<value>
<pending>pending</pending>
<severity>normal</severity>
<package>test-package</package>
<subject>Test subject</subject>
</value>
</item>
</s-gensym3>
</get_statusResponse>
</soap:Body>
</soap:Envelope>"###;
let statuses = parse_get_status_response(xml).unwrap();
assert_eq!(statuses.len(), 1);
assert!(statuses.contains_key(&123));
let bug_report = &statuses[&123];
assert_eq!(bug_report.severity, Some("normal".to_string()));
assert_eq!(bug_report.package, Some("test-package".to_string()));
assert_eq!(bug_report.subject, Some("Test subject".to_string()));
}
#[test]
fn test_parse_get_usertag_response() {
let xml = r###"<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<get_usertagResponse xmlns="Debbugs/SOAP">
<s-gensym3>
<tag1>
<item>123</item>
<item>456</item>
</tag1>
<tag2>
<item>789</item>
</tag2>
</s-gensym3>
</get_usertagResponse>
</soap:Body>
</soap:Envelope>"###;
let usertags = parse_get_usertag_response(xml).unwrap();
assert_eq!(usertags.len(), 2);
assert_eq!(usertags["tag1"], vec![123, 456]);
assert_eq!(usertags["tag2"], vec![789]);
}
#[test]
fn test_parse_response_envelope_invalid() {
let invalid_xml = r###"<?xml version="1.0" encoding="UTF-8"?>
<invalid>
<body>test</body>
</invalid>"###;
let result = parse_response_envelope(invalid_xml, "test");
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Root element is not a valid soap:Envelope"));
}
#[test]
fn test_parse_bool() {
assert_eq!(parse_bool("1").unwrap(), true);
assert_eq!(parse_bool("0").unwrap(), false);
assert!(parse_bool("invalid").is_err());
assert!(parse_bool("true").is_err());
}
#[test]
fn test_bug_report_from_xml_minimal() {
let xml_str = r###"
<value>
<bug_num>123456</bug_num>
<subject>Test bug subject</subject>
<severity>normal</severity>
<package>test-package</package>
</value>
"###;
let element = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
let bug_report = BugReport::from(&element);
assert_eq!(bug_report.bug_num, Some(123456));
assert_eq!(bug_report.subject, Some("Test bug subject".to_string()));
assert_eq!(bug_report.severity, Some("normal".to_string()));
assert_eq!(bug_report.package, Some("test-package".to_string()));
}
#[test]
fn test_bug_report_from_xml_comprehensive() {
let xml_str = r###"
<value>
<bug_num>123456</bug_num>
<subject>Test bug subject</subject>
<severity>important</severity>
<package>test-package</package>
<last_modified>1234567890</last_modified>
<tags>patch,security</tags>
<pending>pending</pending>
<done>fixed in version 1.2</done>
<archived>0</archived>
<unarchived>1</unarchived>
<forwarded>https://example.com/bug/123</forwarded>
<mergedwith>789 101112</mergedwith>
<blockedby>456</blockedby>
<blocks>789</blocks>
<summary>Short summary of the bug</summary>
<affects>affected-package</affects>
<log_modified>1234567900</log_modified>
<location>main</location>
<source>source-package</source>
<owner>maintainer@example.com</owner>
<originator>user@example.com</originator>
<msgid><message-id@example.com></msgid>
<found_versions>
<item>1.0</item>
<item>1.1</item>
</found_versions>
<fixed_versions>
<item>1.2</item>
</fixed_versions>
</value>
"###;
let element = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
let bug_report = BugReport::from(&element);
assert_eq!(bug_report.bug_num, Some(123456));
assert_eq!(bug_report.subject, Some("Test bug subject".to_string()));
assert_eq!(bug_report.severity, Some("important".to_string()));
assert_eq!(bug_report.package, Some("test-package".to_string()));
assert_eq!(bug_report.last_modified, Some(1234567890));
assert_eq!(bug_report.tags, Some("patch,security".to_string())); // Tags are stored as a comma-separated string
assert_eq!(bug_report.pending, Some(crate::Pending::Pending));
assert_eq!(bug_report.done, Some("fixed in version 1.2".to_string())); // done field content
assert_eq!(bug_report.archived, Some(false));
assert_eq!(bug_report.unarchived, Some(true));
assert_eq!(
bug_report.forwarded,
Some("https://example.com/bug/123".to_string())
);
assert_eq!(bug_report.mergedwith, Some(vec![789, 101112]));
assert_eq!(bug_report.blockedby, Some("456".to_string())); // Stored as string
assert_eq!(bug_report.blocks, Some("789".to_string())); // Stored as string
assert_eq!(
bug_report.summary,
Some("Short summary of the bug".to_string())
);
assert_eq!(bug_report.affects, Some("affected-package".to_string()));
assert_eq!(bug_report.log_modified, Some(1234567900));
assert_eq!(bug_report.location, Some("main".to_string()));
assert_eq!(bug_report.source, Some("source-package".to_string()));
assert_eq!(bug_report.owner, Some("maintainer@example.com".to_string()));
assert_eq!(bug_report.originator, Some("user@example.com".to_string())); // submitter -> originator
assert_eq!(
bug_report.msgid,
Some("<message-id@example.com>".to_string())
);
// Note: found_versions and fixed_versions have complex types, testing basic presence
assert!(bug_report.found_versions.is_some());
assert!(bug_report.fixed_versions.is_some());
}
#[test]
fn test_bug_report_from_xml_empty() {
let xml_str = r###"<value></value>"###;
let element = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
let bug_report = BugReport::from(&element);
// All fields should be None for empty XML
assert_eq!(bug_report.bug_num, None);
assert_eq!(bug_report.subject, None);
assert_eq!(bug_report.severity, None);
assert_eq!(bug_report.package, None);
assert_eq!(bug_report.last_modified, None);
assert_eq!(bug_report.tags, None);
assert_eq!(bug_report.pending, None);
assert_eq!(bug_report.done, None);
assert_eq!(bug_report.archived, None);
}
#[test]
fn test_bug_report_from_xml_invalid_values() {
let xml_str = r###"
<value>
<bug_num>not-a-number</bug_num>
<last_modified>invalid-date</last_modified>
<archived>sometimes</archived>
<pending>unknown-status</pending>
</value>
"###;
let element = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
let bug_report = BugReport::from(&element);
// Invalid values should result in None
assert_eq!(bug_report.bug_num, None);
assert_eq!(bug_report.last_modified, None);
assert_eq!(bug_report.archived, None);
assert_eq!(bug_report.pending, None);
// done field accepts any string value
assert!(bug_report.done.is_none()); // Not provided in test XML
}
#[derive(Debug)]
/// Detailed information about a bug report
///
/// Contains comprehensive metadata about a bug including its status, severity,
/// package information, and related bugs.
pub struct BugReport {
/// The pending status of the bug
pub pending: Option<crate::Pending>,
/// Message ID of the initial bug report email
pub msgid: Option<String>,
/// Email address of the person who currently owns/is working on this bug
pub owner: Option<String>,
/// Keywords associated with the bug (deprecated, use `tags` instead)
#[deprecated = "Use tags instead"]
pub keywords: Option<String>,
/// Packages that are affected by this bug (in addition to the primary package)
pub affects: Option<String>,
/// Whether the bug has been unarchived and can be archived again
pub unarchived: Option<bool>,
/// Email address or URL where the bug has been forwarded to upstream
pub forwarded: Option<String>,
/// Short summary description of the bug
pub summary: Option<String>,
/// The unique bug number identifier
pub bug_num: Option<BugId>,
/// Whether the bug has been archived (old/resolved bugs)
pub archived: Option<bool>,
/// Versions of the package where this bug was found to exist
pub found_versions: Option<Vec<Version>>,
/// Email address of the person who marked this bug as done/resolved
pub done: Option<String>,
/// Severity level of the bug (e.g., "serious", "important", "normal", "minor", "wishlist")
pub severity: Option<String>,
/// Name of the package this bug affects
pub package: Option<String>,
/// Versions of the package where this bug has been fixed
pub fixed_versions: Option<Vec<(Option<String>, Option<Version>)>>,
/// Email address of the person who originally reported this bug
pub originator: Option<String>,
/// Comma-separated list of bug IDs that this bug blocks
pub blocks: Option<String>,
/// Dates when the bug was found in specific versions (deprecated, currently empty)
#[deprecated(note = "empty for now")]
pub found_date: Option<Vec<u32>>,
/// Free-form text describing the outlook for fixing this bug
pub outlook: Option<String>,
/// Legacy bug ID field (deprecated, use `bug_num` instead)
#[deprecated(note = "use bug_num")]
pub id: Option<BugId>,
/// Whether the bug has been found in any versions
pub found: bool,
/// Whether the bug has been fixed in any versions
pub fixed: bool,
/// Unix timestamp of when the bug was last modified
pub last_modified: Option<u32>,
/// Space-separated list of tags associated with this bug
pub tags: Option<String>,
/// The title/subject line of the bug report
pub subject: Option<String>,
/// Physical location or context information for the bug
pub location: Option<String>,
/// List of bug IDs that this bug has been merged with
pub mergedwith: Option<Vec<BugId>>,
/// Comma-separated list of bug IDs that block this bug
pub blockedby: Option<String>,
/// Dates when the bug was fixed in specific versions (deprecated, currently empty)
#[deprecated(note = "empty for now")]
pub fixed_date: Option<Vec<u32>>,
/// Unix timestamp of when the bug log was last modified
pub log_modified: Option<u32>,
/// Source package name (for binary packages built from source)
pub source: Option<String>,
}
impl std::fmt::Display for BugReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(bug_num) = self.bug_num {
write!(f, "Bug #{}", bug_num)?;
} else {
write!(f, "Bug #?")?;
}
if let Some(package) = &self.package {
write!(f, " in {}", package)?;
}
if let Some(summary) = &self.summary {
write!(f, ": {}", summary)?;
}
Ok(())
}
}
fn parse_version(input: &str) -> (Option<String>, Option<Version>) {
match input.split_once('/') {
None => (None, input.parse().ok()),
Some((package, version)) => (Some(package.to_string()), version.parse().ok()),
}
}
// Hide the deprecated warnings, since we intentionally still populate the deprecated fields
#[allow(deprecated)]
impl From<&xmltree::Element> for BugReport {
fn from(item: &xmltree::Element) -> Self {
// Helper function to get text and convert to owned String
fn get_text_owned(element: Option<&xmltree::Element>) -> Option<String> {
element.and_then(|e| e.get_text()).map(|s| s.into_owned())
}
// Helper function to parse a child element's text content
fn parse_child<T: std::str::FromStr>(
item: &xmltree::Element,
child_name: &str,
) -> Option<T> {
item.get_child(child_name)
.and_then(|e| e.get_text())
.and_then(|text| text.parse().ok())
}
// Helper function to parse items from a list element
fn parse_list_items<T, F>(element: Option<&xmltree::Element>, parser: F) -> Option<Vec<T>>
where
F: Fn(&str) -> Option<T>,
{
element.map(|e| {
e.children
.iter()
.filter_map(|c| c.as_element())
.filter(|c| c.name == "item")
.filter_map(|c| c.get_text().and_then(|text| parser(text.as_ref())))
.collect()
})
}
Self {
pending: parse_child(item, "pending"),
msgid: get_text_owned(item.get_child("msgid")),
owner: get_text_owned(item.get_child("owner")),
keywords: get_text_owned(item.get_child("keywords")),
affects: get_text_owned(item.get_child("affects")),
unarchived: item
.get_child("unarchived")
.and_then(|e| e.get_text())
.and_then(|s| parse_bool(s.as_ref()).ok()),
blocks: get_text_owned(item.get_child("blocks")),
found_date: parse_list_items(item.get_child("found_date"), |s| s.parse().ok()),
fixed_versions: parse_list_items(item.get_child("fixed_versions"), |s| {
Some(parse_version(s))
}),
outlook: get_text_owned(item.get_child("outlook")),
done: get_text_owned(item.get_child("done")),
forwarded: get_text_owned(item.get_child("forwarded")),
summary: get_text_owned(item.get_child("summary")),
bug_num: parse_child(item, "bug_num"),
id: parse_child(item, "id"),
archived: item
.get_child("archived")
.and_then(|e| e.get_text())
.and_then(|t| parse_bool(t.as_ref()).ok()),
found_versions: parse_list_items(item.get_child("found_versions"), |s| {
s.parse::<Version>().ok()
}),
found: item.get_child("found").is_some(),
fixed: item.get_child("fixed").is_some(),
last_modified: parse_child(item, "last_modified"),
tags: get_text_owned(item.get_child("tags")),
subject: get_text_owned(item.get_child("subject")),
source: get_text_owned(item.get_child("source")),
originator: get_text_owned(item.get_child("originator")),
package: get_text_owned(item.get_child("package")),
location: get_text_owned(item.get_child("location")),
log_modified: parse_child(item, "log_modified"),
mergedwith: item
.get_child("mergedwith")
.and_then(|e| e.get_text())
.map(|s| {
s.split_whitespace()
.filter_map(|i| i.parse().ok())
.collect()
}),
severity: get_text_owned(item.get_child("severity")),
blockedby: get_text_owned(item.get_child("blockedby")),
fixed_date: parse_list_items(item.get_child("fixed_date"), |s| s.parse().ok()),
}
}
}
#[derive(Debug, Clone)]
/// A log entry (email message) from a bug's communication history
///
/// Represents a single email message in the bug's conversation thread,
/// including the initial bug report and all subsequent correspondence.
pub struct BugLog {
/// The email headers as a raw string (From, To, Subject, Date, etc.)
pub header: String,
/// Sequential message number within this bug's history
pub msgnum: BugId,
/// The email message body content
pub body: String,
}
impl BugLog {
#[cfg(feature = "mailparse")]
pub fn headers(&self) -> Result<Vec<mailparse::MailHeader<'_>>, mailparse::MailParseError> {
let (headers, _ix_body) = mailparse::parse_headers(self.header.as_bytes())?;
Ok(headers)
}
}
fn parse_buglog(item: &xmltree::Element) -> Result<BugLog, String> {
let mut header = None;
let mut msgnum = None;
let mut body = None;
for child in item.children.iter() {
if let xmltree::XMLNode::Element(e) = child {
match e.name.as_str() {
"header" => {
header = e.get_text().map(|s| s.into_owned());
}
"msg_num" => {
msgnum = e.get_text().and_then(|s| s.parse().ok());
}
"body" => {
body = e.get_text().map(|s| s.into_owned());
}
"attachments" => {
if !e.children.is_empty() {
log::warn!("Attachments found but not supported (apparently not implemented on the server side)");
}
}
n => {
return Err(format!("Unknown element: {}", n));
}
}
}
}
Ok(BugLog {
header: header.ok_or("Missing header element")?,
msgnum: msgnum.ok_or("Missing or invalid msg_num element")?,
body: body.ok_or("Missing body element")?,
})
}
pub(crate) fn parse_get_bug_log_response(input: &str) -> Result<Vec<BugLog>, String> {
let response_elem = parse_response_envelope(input, "get_bug_log")?;
let array_elem = response_elem
.get_child("Array")
.ok_or("soapenc:Array not found")?;
if array_elem.namespace.as_deref() != Some(XMLNS_SOAPENC) {
return Err(format!(
"Namespace for soapenc:Array is incorrect: {:?}",
array_elem.namespace
));
}
match array_elem.attributes.get("arrayType") {
None => return Err("soapenc:Array does not have soapenc:arrayType attribute".to_string()),
Some(value) => {
if !regex_is_match!(r"xsd:ur-type\[[0-9]+\]", value) && value != "xsd:anyType[0]" {
return Err(format!(
"soapenc:Array has incorrect soapenc:arrayType attribute: {}",
value
));
}
}
}
if array_elem.attributes.get("type") != Some(&"soapenc:Array".to_string()) {
return Err(format!(
"soapenc:Array does not have xsi:type attribute: {:?}",
array_elem.attributes.get("type")
));
}
let mut ret = vec![];
for item in array_elem.children.iter() {
if let xmltree::XMLNode::Element(e) = item {
if e.name == "item" {
ret.push(parse_buglog(e)?);
}
}
}
Ok(ret)
}
trait ToArgXml {
fn to_arg_xml(&self, name: String) -> xmltree::Element;
}
impl ToArgXml for &str {
fn to_arg_xml(&self, name: String) -> xmltree::Element {
xmltree::Element {
prefix: None,
namespace: None,
namespaces: None,
name,
attributes: HashMap::new(),
children: vec![xmltree::XMLNode::Text(self.to_string())],
}
}
}
impl ToArgXml for &[&str] {
fn to_arg_xml(&self, name: String) -> xmltree::Element {
let mut namespace = xmltree::Namespace::empty();
namespace.put("xsi", XMLNS_XSI);
namespace.put("soapenc", XMLNS_SOAPENC);
namespace.put("xsd", XMLNS_XSD);
let children: Vec<_> = self
.iter()
.map(|s| {
xmltree::XMLNode::Element(xmltree::Element {
prefix: None,
namespace: None,
namespaces: None,
name: "item".to_string(),
attributes: hashmap! {
"xsi:type".to_string() => "xsd:string".to_string(),
},
children: vec![xmltree::XMLNode::Text(s.to_string())],
})
})
.collect();
xmltree::Element {
prefix: None,
namespace: None,
namespaces: Some(namespace),
name,
attributes: hashmap! {
"xsi:type".to_string() => "soapenc:Array".to_string(),
"soapenc:arrayType".to_string() => "xsd:string[]".to_string(),
},
children,
}
}
}
impl ToArgXml for &[BugId] {
fn to_arg_xml(&self, name: String) -> xmltree::Element {
let mut namespace = xmltree::Namespace::empty();
namespace.put("xsi", XMLNS_XSI);
namespace.put("soapenc", XMLNS_SOAPENC);
namespace.put("xsd", XMLNS_XSD);
let children: Vec<_> = self
.iter()
.map(|bug_id| {
xmltree::XMLNode::Element(xmltree::Element {
prefix: None,
namespace: None,
namespaces: None,
name: "item".to_string(),
attributes: hashmap! {
"xsi:type".to_string() => "xsd:int".to_string(),
},
children: vec![xmltree::XMLNode::Text(bug_id.to_string())],
})
})
.collect();
xmltree::Element {
prefix: None,
namespace: None,
namespaces: Some(namespace),
name,
attributes: hashmap! {
"xsi:type".to_string() => "soapenc:Array".to_string(),
"soapenc:arrayType".to_string() => format!("xsd:int[{}]", self.len()),
},
children,
}
}
}
fn add_arg_xml<T: ToArgXml>(params: &mut Vec<xmltree::Element>, arg: T) {
params.push(arg.to_arg_xml(format!("arg{}", params.len())));
}
#[derive(Debug, Default, Clone)]
/// Search criteria for finding bugs matching specific conditions
///
/// All fields are optional - only specify the criteria you want to filter by.
/// Multiple criteria are combined with AND logic (all must match).
///
/// # Examples
///
/// ```no_run
/// use debbugs::SearchQuery;
///
/// // Find all serious bugs in the rust-debbugs package
/// let query = SearchQuery {
/// package: Some("rust-debbugs"),
/// severity: Some("serious"),
/// ..Default::default()
/// };
///
/// // Find bugs owned by a specific person
/// let query = SearchQuery {
/// owner: Some("maintainer@example.com"),
/// ..Default::default()
/// };
/// ```
pub struct SearchQuery<'a> {
/// Package name to search for bugs in
pub package: Option<&'a str>,
/// Specific bug IDs to retrieve (useful for batch operations)
pub bug_ids: Option<&'a [BugId]>,
/// Email address of the person who submitted the bug
pub submitter: Option<&'a str>,
/// Email address of the package maintainer
pub maintainer: Option<&'a str>,
/// Source package name (for bugs affecting source packages)
pub src: Option<&'a str>,
/// Severity level (e.g., "critical", "serious", "important", "normal", "minor", "wishlist")
pub severity: Option<&'a str>,
/// Current status of the bug (open, done, forwarded)
pub status: Option<crate::BugStatus>,
/// Email address of the person currently owning/working on the bug
pub owner: Option<&'a str>,
/// Email address of someone who has participated in the bug discussion
pub correspondent: Option<&'a str>,
/// Whether to include archived bugs, non-archived bugs, or both
pub archive: Option<crate::Archived>,
/// Tags to filter by (bugs must have all specified tags)
pub tag: Option<&'a [&'a str]>,
}
pub(crate) fn get_bugs_request(query: &SearchQuery) -> xmltree::Element {
let mut params = Vec::new();
if let Some(package) = query.package {
add_arg_xml(&mut params, "package");
add_arg_xml(&mut params, package);
}
if let Some(bug_ids) = query.bug_ids {
add_arg_xml(&mut params, "bugs");
add_arg_xml(&mut params, bug_ids);
}
if let Some(submitter) = query.submitter {
add_arg_xml(&mut params, "submitter");
add_arg_xml(&mut params, submitter);
}
if let Some(maintainer) = query.maintainer {
add_arg_xml(&mut params, "maint");
add_arg_xml(&mut params, maintainer);
}
if let Some(src) = query.src {
add_arg_xml(&mut params, "src");
add_arg_xml(&mut params, src);
}
if let Some(severity) = query.severity {
add_arg_xml(&mut params, "severity");
add_arg_xml(&mut params, severity);
}
if let Some(status) = query.status {
add_arg_xml(&mut params, "status");
add_arg_xml(&mut params, status.to_string().as_str());
}
if let Some(owner) = query.owner {
add_arg_xml(&mut params, "owner");
add_arg_xml(&mut params, owner);
}
if let Some(correspondent) = query.correspondent {
add_arg_xml(&mut params, "correspondent");
add_arg_xml(&mut params, correspondent);
}
if let Some(archive) = query.archive {
add_arg_xml(&mut params, "archive");
add_arg_xml(&mut params, archive.to_string().as_str());
}
if let Some(tag) = query.tag {
add_arg_xml(&mut params, "tag");
add_arg_xml(&mut params, tag);
}
build_request_envelope("get_bugs", params)
}
pub(crate) fn parse_get_bugs_response(input: &str) -> Result<Vec<crate::BugId>, String> {
let response_elem = parse_response_envelope(input, "get_bugs")?;
let array_elem = response_elem
.get_child("Array")
.ok_or("soapenc:Array not found")?;
if array_elem.namespace.as_deref() != Some(XMLNS_SOAPENC) {
return Err(format!(
"Namespace for soapenc:Array is incorrect: {:?}",
array_elem.namespace
));
}
match array_elem.attributes.get("arrayType") {
None => return Err("soapenc:Array does not have soapenc:arrayType attribute".to_string()),
Some(value) => {
if !regex_is_match!(r"xsd:int\[[0-9]+\]", value) && value != "xsd:anyType[0]" {
return Err(format!(
"soapenc:Array has incorrect soapenc:arrayType attribute: {}",
value
));
}
}
}
// Extract the integers from the item elements
let mut integers = Vec::new();
for item in array_elem.children.iter() {
if let xmltree::XMLNode::Element(e) = item {
if e.name == "item" {
if let Some(text) = e.get_text() {
if let Ok(num) = text.parse() {
integers.push(num);
}
}
}
}
}
Ok(integers)
}
pub(crate) fn get_status_request(bug_ids: &[BugId]) -> xmltree::Element {
let mut params = Vec::new();
add_arg_xml(&mut params, bug_ids);
build_request_envelope("get_status", params)
}
pub(crate) fn parse_get_status_response(input: &str) -> Result<HashMap<BugId, BugReport>, String> {
let response_elem = parse_response_envelope(input, "get_status")?;
if response_elem.namespace.as_deref() != Some(XMLNS_DEBBUGS) {
return Err(format!(
"Namespace for get_statusResponse is incorrect: {:?}",
response_elem.namespace
));
}
let container = response_elem
.get_child("s-gensym3")
.ok_or("s-gensym3 not found")?;
let mut ret = HashMap::new();
for item in container.children.iter() {
if let xmltree::XMLNode::Element(e) = item {
if e.name == "item" {
if e.namespace.as_deref() != Some(XMLNS_DEBBUGS) {
return Err(format!(
"Namespace for item is incorrect: {:?}",
e.namespace
));
}
let key = e
.get_child("key")
.ok_or("key not found")?
.get_text()
.ok_or("key has no text")?
.parse::<BugId>()
.map_err(|_| "Invalid BugId format")?;
let value = BugReport::from(e.get_child("value").ok_or("value not found")?);
ret.insert(key, value);
}
}
}
Ok(ret)
}
pub(crate) fn get_usertag_request(email: &str, tags: &[&str]) -> xmltree::Element {
let mut params = Vec::new();
add_arg_xml(&mut params, email);
for tag in tags {
add_arg_xml(&mut params, *tag);
}
build_request_envelope("get_usertag", params)
}
pub(crate) fn parse_get_usertag_response(
input: &str,
) -> Result<HashMap<String, Vec<crate::BugId>>, String> {
let response_elem = parse_response_envelope(input, "get_usertag")?;
let container = response_elem
.get_child("s-gensym3")
.ok_or("s-gensym3 not found")?;
let mut ret = HashMap::new();
for child in container.children.iter() {
if let Some(e) = child.as_element() {
let mut ids = vec![];
for item in e.children.iter() {
if let xmltree::XMLNode::Element(e) = item {
if e.name == "item" {
if let Some(text) = e.get_text() {
if let Ok(id) = text.parse::<BugId>() {
ids.push(id);
}
}
}
}
}
ret.insert(e.name.clone(), ids);
}
}
Ok(ret)
}