1use std::future::Future;
4
5use crate::XmlError;
6use crate::util::parse_u64;
7
8const FIRST_URL_ADDRESS: u64 = 0x0200;
11const SECOND_URL_ADDRESS: u64 = 0x0400;
14const URL_MAX_LEN: usize = 512;
16
17const ZIP_MAGIC: &[u8; 4] = b"PK\x03\x04";
19
20const MAX_XML_SIZE: u64 = 64 * 1024 * 1024;
24
25pub fn decompress_if_zip(data: Vec<u8>) -> Result<Vec<u8>, XmlError> {
32 use std::io::Read;
33
34 if data.len() < 4 || &data[..4] != ZIP_MAGIC {
35 return Ok(data);
36 }
37 let cursor = std::io::Cursor::new(&data);
38 let mut archive = zip::ZipArchive::new(cursor)
39 .map_err(|e| XmlError::Invalid(format!("bad XML ZIP archive: {e}")))?;
40 if archive.is_empty() {
41 return Err(XmlError::Invalid("XML ZIP archive is empty".into()));
42 }
43 let file = archive
44 .by_index(0)
45 .map_err(|e| XmlError::Invalid(format!("cannot read XML ZIP entry: {e}")))?;
46 if file.size() > MAX_XML_SIZE {
47 return Err(XmlError::Invalid(format!(
48 "XML ZIP entry declares {} bytes, exceeding the {MAX_XML_SIZE}-byte cap",
49 file.size()
50 )));
51 }
52 let mut xml = Vec::with_capacity(file.size().min(MAX_XML_SIZE) as usize);
53 file.take(MAX_XML_SIZE + 1)
56 .read_to_end(&mut xml)
57 .map_err(|e| XmlError::Invalid(format!("XML ZIP decompression failed: {e}")))?;
58 if xml.len() as u64 > MAX_XML_SIZE {
59 return Err(XmlError::Invalid(format!(
60 "XML ZIP entry decompressed past the {MAX_XML_SIZE}-byte cap"
61 )));
62 }
63 Ok(xml)
64}
65
66pub async fn fetch_and_load_xml<F, Fut>(mut read_mem: F) -> Result<String, XmlError>
75where
76 F: FnMut(u64, usize) -> Fut,
77 Fut: Future<Output = Result<Vec<u8>, XmlError>>,
78{
79 match fetch_from_url_register(&mut read_mem, FIRST_URL_ADDRESS).await {
80 Ok(xml) => Ok(xml),
81 Err(first_err) => {
82 tracing::debug!(error = %first_err, "first URL failed, trying GevSecondURL");
83 match fetch_from_url_register(&mut read_mem, SECOND_URL_ADDRESS).await {
84 Ok(xml) => Ok(xml),
85 Err(second_err) => {
89 tracing::debug!(error = %second_err, "second URL failed as well");
90 Err(first_err)
91 }
92 }
93 }
94 }
95}
96
97async fn fetch_from_url_register<F, Fut>(
99 read_mem: &mut F,
100 url_addr: u64,
101) -> Result<String, XmlError>
102where
103 F: FnMut(u64, usize) -> Fut,
104 Fut: Future<Output = Result<Vec<u8>, XmlError>>,
105{
106 let url_bytes = read_mem(url_addr, URL_MAX_LEN).await?;
107 let url = first_cstring(&url_bytes)
108 .ok_or_else(|| XmlError::Invalid("URL register is empty".into()))?;
109 let location = UrlLocation::parse(&url)?;
110 match location {
111 UrlLocation::Local { address, length } => {
112 let xml_bytes = read_mem(address, length).await?;
113 let xml_bytes = decompress_if_zip(xml_bytes)?;
114 String::from_utf8(xml_bytes)
115 .map_err(|err| XmlError::Xml(format!("invalid UTF-8: {err}")))
116 }
117 UrlLocation::LocalNamed(name) => Err(XmlError::Unsupported(format!(
118 "named local URL '{name}' is not supported"
119 ))),
120 UrlLocation::Http(url) => Err(XmlError::Unsupported(format!(
121 "HTTP retrieval is not implemented ({url})"
122 ))),
123 UrlLocation::File(path) => Err(XmlError::Unsupported(format!(
124 "file URL '{path}' is not supported"
125 ))),
126 }
127}
128
129fn first_cstring(bytes: &[u8]) -> Option<String> {
131 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
132 let slice = &bytes[..end];
133 let value = String::from_utf8_lossy(slice).trim().to_string();
134 if value.is_empty() { None } else { Some(value) }
135}
136
137#[derive(Debug)]
139enum UrlLocation {
140 Local { address: u64, length: usize },
142 #[allow(dead_code)]
144 LocalNamed(String),
145 Http(String),
147 File(String),
149}
150
151impl UrlLocation {
152 fn parse(url: &str) -> Result<Self, XmlError> {
153 let lower = url.to_ascii_lowercase();
154 if let Some(rest) = lower.strip_prefix("local:") {
155 let rest_original = &url[url.len() - rest.len()..];
157 parse_local_url(rest_original)
158 } else if lower.starts_with("http://") || lower.starts_with("https://") {
159 Ok(UrlLocation::Http(url.to_string()))
160 } else if lower.starts_with("file://") {
161 Ok(UrlLocation::File(url.to_string()))
162 } else {
163 Err(XmlError::Unsupported(format!("unknown URL scheme: {url}")))
164 }
165 }
166}
167
168fn parse_local_url(rest: &str) -> Result<UrlLocation, XmlError> {
177 let trimmed = rest.strip_prefix("///").unwrap_or(rest).trim();
179 if trimmed.is_empty() {
180 return Err(XmlError::Invalid("empty local URL".into()));
181 }
182
183 let parts: Vec<&str> = trimmed.split(';').collect();
184
185 if parts.len() >= 3 {
187 let addr_str = parts[parts.len() - 2].trim();
188 let len_str = parts[parts.len() - 1].trim();
189 if let (Ok(address), Ok(length)) = (
190 u64::from_str_radix(addr_str, 16),
191 u64::from_str_radix(len_str, 16),
192 ) {
193 return Ok(UrlLocation::Local {
194 address,
195 length: length as usize,
196 });
197 }
198 }
199
200 let mut address = None;
202 let mut length = None;
203 for part in parts {
204 let token = part.trim();
205 if token.is_empty() {
206 continue;
207 }
208 if let Some((key, value)) = token.split_once('=') {
209 let key = key.trim().to_ascii_lowercase();
210 let value = value.trim();
211 match key.as_str() {
212 "address" | "addr" | "offset" => {
213 address = Some(parse_u64(value)?);
214 }
215 "length" | "size" => {
216 let len = parse_u64(value)?;
217 length = Some(
218 len.try_into()
219 .map_err(|_| XmlError::Invalid("length does not fit usize".into()))?,
220 );
221 }
222 _ => {}
223 }
224 } else if token.starts_with("0x") {
225 address = Some(parse_u64(token)?);
226 }
227 }
229 match (address, length) {
230 (Some(address), Some(length)) => Ok(UrlLocation::Local { address, length }),
231 _ => Err(XmlError::Invalid(format!("unsupported local URL: {rest}"))),
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 fn zip_bytes(name: &str, data: &[u8]) -> Vec<u8> {
241 use std::io::Write;
242 let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
243 let options = zip::write::SimpleFileOptions::default()
244 .compression_method(zip::CompressionMethod::Deflated);
245 writer.start_file(name, options).expect("start zip entry");
246 writer.write_all(data).expect("write zip entry");
247 writer.finish().expect("finish zip").into_inner()
248 }
249
250 #[tokio::test]
251 async fn fetch_local_zipped_xml() {
252 let url = b"Local:camera.zip;10;9999\0".to_vec();
253 let zipped = zip_bytes("camera.xml", b"<a/>");
254 let mut url_reg = url.clone();
255 url_reg.resize(URL_MAX_LEN, 0);
256 let expected_len = zipped.len();
257 let loaded = fetch_and_load_xml(|addr, len| {
258 let url_reg = url_reg.clone();
259 let zipped = zipped.clone();
260 async move {
261 if addr == FIRST_URL_ADDRESS {
262 Ok(url_reg)
263 } else if addr == 0x10 && len == 0x9999 {
264 let mut data = zipped;
267 data.resize(len.max(expected_len), 0);
268 Ok(data)
269 } else {
270 Err(XmlError::Transport("unexpected read".into()))
271 }
272 }
273 })
274 .await
275 .expect("load zipped xml");
276 assert_eq!(loaded, "<a/>");
277 }
278
279 #[tokio::test]
280 async fn falls_back_to_second_url() {
281 let url = b"local:address=0x20;length=0x4\0".to_vec();
282 let loaded = fetch_and_load_xml(|addr, len| {
283 let url = url.clone();
284 async move {
285 if addr == FIRST_URL_ADDRESS {
286 Ok(vec![0u8; URL_MAX_LEN])
288 } else if addr == SECOND_URL_ADDRESS {
289 Ok(url)
290 } else if addr == 0x20 && len == 0x4 {
291 Ok(b"<b/>".to_vec())
292 } else {
293 Err(XmlError::Transport("unexpected read".into()))
294 }
295 }
296 })
297 .await
298 .expect("load xml via second URL");
299 assert_eq!(loaded, "<b/>");
300 }
301
302 #[tokio::test]
303 async fn first_url_error_is_reported_when_both_fail() {
304 let err = fetch_and_load_xml(|_, _| async { Ok(vec![0u8; URL_MAX_LEN]) })
305 .await
306 .expect_err("both URLs empty");
307 assert!(matches!(err, XmlError::Invalid(_)));
308 }
309
310 #[test]
311 fn decompress_rejects_oversized_declared_xml() {
312 let mut zipped = zip_bytes("camera.xml", b"<a/>");
313 let huge = 0xFFFF_FFF0u32.to_le_bytes();
317 for (magic, offset) in [(b"PK\x03\x04", 22usize), (b"PK\x01\x02", 24usize)] {
318 let pos = zipped
319 .windows(4)
320 .position(|w| w == magic)
321 .expect("zip header signature");
322 zipped[pos + offset..pos + offset + 4].copy_from_slice(&huge);
323 }
324 let err = decompress_if_zip(zipped).expect_err("oversized declared XML");
325 assert!(matches!(err, XmlError::Invalid(_)), "got: {err:?}");
326 }
327
328 #[test]
329 fn decompress_passes_through_plain_xml() {
330 let data = b"<plain/>".to_vec();
331 assert_eq!(decompress_if_zip(data.clone()).unwrap(), data);
332 }
333
334 #[tokio::test]
335 async fn fetch_local_xml() {
336 let data = b"local:address=0x10;length=0x3\0".to_vec();
337 let xml_payload = b"<a/>".to_vec();
338 let loaded = fetch_and_load_xml(|addr, len| {
339 let data = data.clone();
340 let xml_payload = xml_payload.clone();
341 async move {
342 if addr == FIRST_URL_ADDRESS {
343 Ok(data)
344 } else if addr == 0x10 && len == 0x3 {
345 Ok(xml_payload)
346 } else {
347 Err(XmlError::Transport("unexpected read".into()))
348 }
349 }
350 })
351 .await
352 .expect("load xml");
353 assert_eq!(loaded, "<a/>");
354 }
355}