1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Deserialize, Serialize, PartialEq)]
4#[serde(rename = "multistatus")]
5pub struct Multistatus {
6 #[serde(rename = "response", default)]
7 pub response: Vec<Response>,
8}
9
10impl Multistatus {
11 pub fn from_xml(xml: &str) -> Result<Self, quick_xml::DeError> {
13 quick_xml::de::from_str(xml)
14 }
15
16 pub fn to_xml(&self) -> Result<String, quick_xml::SeError> {
18 quick_xml::se::to_string(self)
19 }
20}
21
22#[derive(Debug, Deserialize, Serialize, PartialEq)]
23#[serde(rename = "response")]
24pub struct Response {
25 pub href: String,
26 pub propstat: PropStat,
27}
28
29impl Response {
30 pub fn is_collection(&self) -> bool {
32 self.propstat
33 .prop
34 .resourcetype
35 .as_ref()
36 .and_then(|rt| rt.collection.as_ref())
37 .is_some()
38 }
39
40 pub fn name(&self) -> &str {
42 self.href
43 .trim()
44 .trim_end_matches('/')
45 .rsplit('/')
46 .next()
47 .unwrap_or(&self.href)
48 }
49}
50
51#[derive(Debug, Deserialize, Serialize, PartialEq)]
52#[serde(rename = "propstat")]
53pub struct PropStat {
54 pub prop: Prop,
55 pub status: String,
56}
57
58#[derive(Debug, Deserialize, Serialize, PartialEq)]
59#[serde(rename = "prop")]
60pub struct Prop {
61 #[serde(rename = "displayname", default, skip_serializing_if = "Option::is_none")]
62 pub displayname: Option<String>,
63
64 #[serde(rename = "creationdate", default, skip_serializing_if = "Option::is_none")]
65 pub creationdate: Option<String>,
66
67 #[serde(rename = "getcontentlength", default, skip_serializing_if = "Option::is_none")]
68 pub getcontentlength: Option<u64>,
69
70 #[serde(rename = "getcontenttype", default, skip_serializing_if = "Option::is_none")]
71 pub getcontenttype: Option<String>,
72
73 #[serde(rename = "getetag", default, skip_serializing_if = "Option::is_none")]
74 pub getetag: Option<String>,
75
76 #[serde(rename = "getlastmodified", default, skip_serializing_if = "Option::is_none")]
77 pub getlastmodified: Option<String>,
78
79 #[serde(rename = "lockdiscovery", default, skip_serializing_if = "Option::is_none")]
80 pub lockdiscovery: Option<String>,
81
82 #[serde(rename = "resourcetype", default, skip_serializing_if = "Option::is_none")]
83 pub resourcetype: Option<ResourceType>,
84
85 #[serde(rename = "supportedlock", default, skip_serializing_if = "Option::is_none")]
86 pub supportedlock: Option<String>,
87}
88
89#[derive(Debug, Deserialize, Serialize, PartialEq)]
90#[serde(rename = "resourcetype")]
91pub struct ResourceType {
92 #[serde(rename = "collection", default, skip_serializing_if = "Option::is_none")]
93 pub collection: Option<Collection>,
94}
95
96#[derive(Debug, Deserialize, Serialize, PartialEq)]
97#[serde(rename = "collection")]
98pub struct Collection {}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use std::fs;
104 use std::path::Path;
105
106 fn test_roundtrip(xml_content: &str, test_name: &str) {
107 let multistatus1 = Multistatus::from_xml(xml_content)
109 .unwrap_or_else(|e| panic!("{}: Failed to parse XML: {}", test_name, e));
110
111 let xml_output = multistatus1
113 .to_xml()
114 .unwrap_or_else(|e| panic!("{}: Failed to serialize to XML: {}", test_name, e));
115
116 let multistatus2 = Multistatus::from_xml(&xml_output)
118 .unwrap_or_else(|e| panic!("{}: Failed to parse serialized XML: {}", test_name, e));
119
120 assert_eq!(
122 multistatus1, multistatus2,
123 "{}: Roundtrip conversion failed - objects are not equal",
124 test_name
125 );
126 }
127
128 #[test]
129 fn test_roundtrip_a_xml() {
130 let xml_path = Path::new("xml/a.xml");
131 if xml_path.exists() {
132 let xml_content = fs::read_to_string(xml_path)
133 .expect("Failed to read xml/a.xml");
134 test_roundtrip(&xml_content, "a.xml");
135 } else {
136 println!("Skipping test_roundtrip_a_xml: file not found");
137 }
138 }
139
140 #[test]
141 fn test_roundtrip_dufs_xml() {
142 let xml_path = Path::new("xml/dufs.xml");
143 if xml_path.exists() {
144 let xml_content = fs::read_to_string(xml_path)
145 .expect("Failed to read xml/dufs.xml");
146 test_roundtrip(&xml_content, "dufs.xml");
147 } else {
148 println!("Skipping test_roundtrip_dufs_xml: file not found");
149 }
150 }
151
152 #[test]
153 fn test_roundtrip_list_xml() {
154 let xml_path = Path::new("xml/list.xml");
155 if xml_path.exists() {
156 let xml_content = fs::read_to_string(xml_path)
157 .expect("Failed to read xml/list.xml");
158 test_roundtrip(&xml_content, "list.xml");
159 } else {
160 println!("Skipping test_roundtrip_list_xml: file not found");
161 }
162 }
163
164 #[test]
165 fn test_simple_roundtrip() {
166 let xml = r#"<?xml version="1.0" encoding="utf-8"?>
167<multistatus>
168 <response>
169 <href>/test.txt</href>
170 <propstat>
171 <prop>
172 <displayname>test.txt</displayname>
173 <getcontentlength>1024</getcontentlength>
174 <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
175 <resourcetype></resourcetype>
176 </prop>
177 <status>HTTP/1.1 200 OK</status>
178 </propstat>
179 </response>
180</multistatus>"#;
181
182 test_roundtrip(xml, "simple");
183 }
184
185 #[test]
186 fn test_collection_roundtrip() {
187 let xml = r#"<?xml version="1.0" encoding="utf-8"?>
188<multistatus>
189 <response>
190 <href>/folder/</href>
191 <propstat>
192 <prop>
193 <displayname>folder</displayname>
194 <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
195 <resourcetype>
196 <collection></collection>
197 </resourcetype>
198 </prop>
199 <status>HTTP/1.1 200 OK</status>
200 </propstat>
201 </response>
202</multistatus>"#;
203
204 test_roundtrip(xml, "collection");
205 }
206
207 #[test]
208 fn test_is_collection() {
209 let xml = r#"<?xml version="1.0" encoding="utf-8"?>
210<multistatus>
211 <response>
212 <href>/folder/</href>
213 <propstat>
214 <prop>
215 <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
216 <resourcetype>
217 <collection></collection>
218 </resourcetype>
219 </prop>
220 <status>HTTP/1.1 200 OK</status>
221 </propstat>
222 </response>
223 <response>
224 <href>/file.txt</href>
225 <propstat>
226 <prop>
227 <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
228 <resourcetype></resourcetype>
229 </prop>
230 <status>HTTP/1.1 200 OK</status>
231 </propstat>
232 </response>
233</multistatus>"#;
234
235 let multistatus = Multistatus::from_xml(xml).unwrap();
236 assert_eq!(multistatus.response.len(), 2);
237 assert!(multistatus.response[0].is_collection());
238 assert!(!multistatus.response[1].is_collection());
239 }
240
241 #[test]
242 fn test_name_extraction() {
243 let xml = r#"<?xml version="1.0" encoding="utf-8"?>
244<multistatus>
245 <response>
246 <href>/path/to/file.txt</href>
247 <propstat>
248 <prop>
249 <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
250 <resourcetype></resourcetype>
251 </prop>
252 <status>HTTP/1.1 200 OK</status>
253 </propstat>
254 </response>
255 <response>
256 <href>/folder/</href>
257 <propstat>
258 <prop>
259 <getlastmodified>Mon, 13 Nov 2023 14:52:27 GMT</getlastmodified>
260 <resourcetype></resourcetype>
261 </prop>
262 <status>HTTP/1.1 200 OK</status>
263 </propstat>
264 </response>
265</multistatus>"#;
266
267 let multistatus = Multistatus::from_xml(xml).unwrap();
268 assert_eq!(multistatus.response[0].name(), "file.txt");
269 assert_eq!(multistatus.response[1].name(), "folder");
270 }
271}