1use std::future::Future;
4
5use crate::XmlError;
6use crate::util::parse_u64;
7
8const FIRST_URL_ADDRESS: u64 = 0x0200;
11const FIRST_URL_MAX_LEN: usize = 512;
13
14pub 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
45fn 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#[derive(Debug)]
55enum UrlLocation {
56 Local { address: u64, length: usize },
58 #[allow(dead_code)]
60 LocalNamed(String),
61 Http(String),
63 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 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
84fn parse_local_url(rest: &str) -> Result<UrlLocation, XmlError> {
93 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 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 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 }
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}