viva_genapi_xml/
fetch.rs

1//! URL parsing and XML document retrieval utilities.
2
3use std::future::Future;
4
5use crate::XmlError;
6use crate::util::parse_u64;
7
8/// Address of the first URL register in the GigE Vision bootstrap register map.
9/// GigE Vision spec: GevFirstURL at 0x0200 (512 bytes max).
10const FIRST_URL_ADDRESS: u64 = 0x0200;
11/// Maximum length of the first URL string.
12const FIRST_URL_MAX_LEN: usize = 512;
13
14/// Fetch the GenICam XML document using the provided memory reader closure.
15///
16/// The closure must return the requested number of bytes starting at the
17/// provided address. It can internally perform chunked transfers.
18pub async fn fetch_and_load_xml<F, Fut>(mut read_mem: F) -> Result<String, XmlError>
19where
20    F: FnMut(u64, usize) -> Fut,
21    Fut: Future<Output = Result<Vec<u8>, XmlError>>,
22{
23    let url_bytes = read_mem(FIRST_URL_ADDRESS, FIRST_URL_MAX_LEN).await?;
24    let url = first_cstring(&url_bytes)
25        .ok_or_else(|| XmlError::Invalid("FirstURL register is empty".into()))?;
26    let location = UrlLocation::parse(&url)?;
27    match location {
28        UrlLocation::Local { address, length } => {
29            let xml_bytes = read_mem(address, length).await?;
30            String::from_utf8(xml_bytes)
31                .map_err(|err| XmlError::Xml(format!("invalid UTF-8: {err}")))
32        }
33        UrlLocation::LocalNamed(name) => Err(XmlError::Unsupported(format!(
34            "named local URL '{name}' is not supported"
35        ))),
36        UrlLocation::Http(url) => Err(XmlError::Unsupported(format!(
37            "HTTP retrieval is not implemented ({url})"
38        ))),
39        UrlLocation::File(path) => Err(XmlError::Unsupported(format!(
40            "file URL '{path}' is not supported"
41        ))),
42    }
43}
44
45/// Extract the first null-terminated C string from a byte buffer.
46fn first_cstring(bytes: &[u8]) -> Option<String> {
47    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
48    let slice = &bytes[..end];
49    let value = String::from_utf8_lossy(slice).trim().to_string();
50    if value.is_empty() { None } else { Some(value) }
51}
52
53/// Parsed URL location variants.
54#[derive(Debug)]
55enum UrlLocation {
56    /// Memory-mapped local XML at a fixed address.
57    Local { address: u64, length: usize },
58    /// Named local XML resource (unsupported).
59    #[allow(dead_code)]
60    LocalNamed(String),
61    /// HTTP(S) remote URL (unsupported).
62    Http(String),
63    /// File system URL (unsupported).
64    File(String),
65}
66
67impl UrlLocation {
68    fn parse(url: &str) -> Result<Self, XmlError> {
69        let lower = url.to_ascii_lowercase();
70        if let Some(rest) = lower.strip_prefix("local:") {
71            // Use original URL (case-preserved) for the rest, at same offset.
72            let rest_original = &url[url.len() - rest.len()..];
73            parse_local_url(rest_original)
74        } else if lower.starts_with("http://") || lower.starts_with("https://") {
75            Ok(UrlLocation::Http(url.to_string()))
76        } else if lower.starts_with("file://") {
77            Ok(UrlLocation::File(url.to_string()))
78        } else {
79            Err(XmlError::Unsupported(format!("unknown URL scheme: {url}")))
80        }
81    }
82}
83
84/// Parse a `local:` URL into its components.
85///
86/// Supports two formats:
87///
88/// 1. **Key-value**: `local:address=0x10;length=0x3`
89/// 2. **GenICam standard**: `Local:///filename;hex_address;hex_length`
90///
91/// In format 2, the filename is optional (can be just `///;addr;len`).
92fn parse_local_url(rest: &str) -> Result<UrlLocation, XmlError> {
93    // Strip the `///` prefix used by the standard GenICam URL format.
94    let trimmed = rest.strip_prefix("///").unwrap_or(rest).trim();
95    if trimmed.is_empty() {
96        return Err(XmlError::Invalid("empty local URL".into()));
97    }
98
99    let parts: Vec<&str> = trimmed.split(';').collect();
100
101    // GenICam standard format: filename;hex_address;hex_length (3 semicolon-separated parts).
102    if parts.len() >= 3 {
103        let addr_str = parts[parts.len() - 2].trim();
104        let len_str = parts[parts.len() - 1].trim();
105        if let (Ok(address), Ok(length)) = (
106            u64::from_str_radix(addr_str, 16),
107            u64::from_str_radix(len_str, 16),
108        ) {
109            return Ok(UrlLocation::Local {
110                address,
111                length: length as usize,
112            });
113        }
114    }
115
116    // Fall back to key-value parsing.
117    let mut address = None;
118    let mut length = None;
119    for part in parts {
120        let token = part.trim();
121        if token.is_empty() {
122            continue;
123        }
124        if let Some((key, value)) = token.split_once('=') {
125            let key = key.trim().to_ascii_lowercase();
126            let value = value.trim();
127            match key.as_str() {
128                "address" | "addr" | "offset" => {
129                    address = Some(parse_u64(value)?);
130                }
131                "length" | "size" => {
132                    let len = parse_u64(value)?;
133                    length = Some(
134                        len.try_into()
135                            .map_err(|_| XmlError::Invalid("length does not fit usize".into()))?,
136                    );
137                }
138                _ => {}
139            }
140        } else if token.starts_with("0x") {
141            address = Some(parse_u64(token)?);
142        }
143        // Ignore unrecognized tokens (like the filename).
144    }
145    match (address, length) {
146        (Some(address), Some(length)) => Ok(UrlLocation::Local { address, length }),
147        _ => Err(XmlError::Invalid(format!("unsupported local URL: {rest}"))),
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[tokio::test]
156    async fn fetch_local_xml() {
157        let data = b"local:address=0x10;length=0x3\0".to_vec();
158        let xml_payload = b"<a/>".to_vec();
159        let loaded = fetch_and_load_xml(|addr, len| {
160            let data = data.clone();
161            let xml_payload = xml_payload.clone();
162            async move {
163                if addr == FIRST_URL_ADDRESS {
164                    Ok(data)
165                } else if addr == 0x10 && len == 0x3 {
166                    Ok(xml_payload)
167                } else {
168                    Err(XmlError::Transport("unexpected read".into()))
169                }
170            }
171        })
172        .await
173        .expect("load xml");
174        assert_eq!(loaded, "<a/>");
175    }
176}