1use std::io;
8use std::path::Path;
9use std::sync::Arc;
10
11use bytes::Bytes;
12pub use futures_util::future::BoxFuture;
13
14#[allow(clippy::len_without_is_empty)]
16pub trait Backend: Send + Sync + 'static {
17 fn len(&self) -> BoxFuture<'_, io::Result<u64>>;
19
20 fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> BoxFuture<'a, io::Result<usize>>;
24}
25
26pub struct MemBackend(Bytes);
29
30impl From<Vec<u8>> for MemBackend {
31 fn from(data: Vec<u8>) -> MemBackend {
32 MemBackend(Bytes::from(data))
33 }
34}
35
36impl From<Bytes> for MemBackend {
37 fn from(data: Bytes) -> MemBackend {
38 MemBackend(data)
39 }
40}
41
42fn read_from_bytes(data: &[u8], offset: u64, buf: &mut [u8]) -> usize {
46 let start = usize::try_from(offset)
47 .unwrap_or(usize::MAX)
48 .min(data.len());
49 let count = buf.len().min(data.len() - start);
50 buf[..count].copy_from_slice(&data[start..start + count]);
51 count
52}
53
54impl Backend for MemBackend {
55 fn len(&self) -> BoxFuture<'_, io::Result<u64>> {
56 let total = self.0.len() as u64;
57 Box::pin(async move { Ok(total) })
58 }
59
60 fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> BoxFuture<'a, io::Result<usize>> {
61 Box::pin(async move { Ok(read_from_bytes(&self.0, offset, buf)) })
62 }
63}
64
65pub struct FileBackend {
70 file: Arc<std::fs::File>,
71 len: u64,
72}
73
74impl FileBackend {
75 pub async fn open(path: impl AsRef<Path>) -> io::Result<FileBackend> {
77 let path = path.as_ref().to_owned();
78 tokio::task::spawn_blocking(move || {
79 let file = std::fs::File::open(path)?;
80 let len = file.metadata()?.len();
81 Ok(FileBackend {
82 file: Arc::new(file),
83 len,
84 })
85 })
86 .await
87 .map_err(io::Error::other)?
88 }
89}
90
91fn positioned_read(file: &std::fs::File, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
93 #[cfg(unix)]
94 {
95 use std::os::unix::fs::FileExt;
96 file.read_at(buf, offset)
97 }
98 #[cfg(windows)]
99 {
100 use std::os::windows::fs::FileExt;
101 file.seek_read(buf, offset)
102 }
103}
104
105fn read_at_fully(file: &std::fs::File, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
108 let mut filled = 0;
109 while filled < buf.len() {
110 let count = positioned_read(file, offset + filled as u64, &mut buf[filled..])?;
111 if count == 0 {
112 break;
113 }
114 filled += count;
115 }
116 Ok(filled)
117}
118
119impl Backend for FileBackend {
120 fn len(&self) -> BoxFuture<'_, io::Result<u64>> {
121 let total = self.len;
122 Box::pin(async move { Ok(total) })
123 }
124
125 fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> BoxFuture<'a, io::Result<usize>> {
126 let file = Arc::clone(&self.file);
127 let wanted = buf.len();
128 Box::pin(async move {
129 let chunk = tokio::task::spawn_blocking(move || {
130 let mut scratch = vec![0u8; wanted];
131 let count = read_at_fully(&file, offset, &mut scratch)?;
132 scratch.truncate(count);
133 Ok::<Vec<u8>, io::Error>(scratch)
134 })
135 .await
136 .map_err(io::Error::other)??;
137 buf[..chunk.len()].copy_from_slice(&chunk);
138 Ok(chunk.len())
139 })
140 }
141}
142
143#[cfg(feature = "http")]
151pub struct HttpBackend {
152 client: reqwest::Client,
153 url: reqwest::Url,
154 len: u64,
155 full: std::sync::OnceLock<Bytes>,
156 progress: Option<Arc<dyn Fn(u64, u64) + Send + Sync>>,
157}
158
159#[cfg(feature = "http")]
160impl HttpBackend {
161 pub async fn new(url: impl reqwest::IntoUrl) -> crate::Result<HttpBackend> {
163 let url = url.into_url().map_err(|err| crate::Error::Http {
164 status: None,
165 msg: err.to_string(),
166 })?;
167 let client = reqwest::Client::new();
168 let response = client
169 .head(url.clone())
170 .send()
171 .await
172 .map_err(|err| crate::Error::Http {
173 status: err.status().map(|status| status.as_u16()),
174 msg: err.to_string(),
175 })?;
176 if !response.status().is_success() {
177 return Err(crate::Error::Http {
178 status: Some(response.status().as_u16()),
179 msg: format!("HEAD {url} failed"),
180 });
181 }
182 let len = response
183 .headers()
184 .get(reqwest::header::CONTENT_LENGTH)
185 .and_then(|value| value.to_str().ok())
186 .and_then(|value| value.parse().ok())
187 .ok_or_else(|| crate::Error::Http {
188 status: Some(response.status().as_u16()),
189 msg: format!("HEAD {url}: missing or malformed Content-Length"),
190 })?;
191 Ok(HttpBackend {
192 client,
193 url,
194 len,
195 full: std::sync::OnceLock::new(),
196 progress: None,
197 })
198 }
199
200 pub fn on_fallback_progress(
207 mut self,
208 progress: impl Fn(u64, u64) + Send + Sync + 'static,
209 ) -> HttpBackend {
210 self.progress = Some(Arc::new(progress));
211 self
212 }
213
214 async fn collect_full_body(&self, response: reqwest::Response) -> io::Result<Bytes> {
220 use futures_util::StreamExt;
221 let cap = usize::try_from(self.len).unwrap_or(usize::MAX);
222 let mut collected = Vec::new();
223 let mut chunks = response.bytes_stream();
224 if let Some(progress) = &self.progress {
225 progress(0, self.len);
226 }
227 while collected.len() < cap {
228 let chunk = match chunks.next().await {
229 Some(Ok(chunk)) => chunk,
230 Some(Err(err)) => {
231 return Err(http_io_error(crate::error::TransportMarker {
232 status: None,
233 msg: format!("GET {}: {err}", self.url),
234 }))
235 }
236 None => break, };
238 let take = (cap - collected.len()).min(chunk.len());
239 collected.extend_from_slice(&chunk[..take]);
240 if let Some(progress) = &self.progress {
241 progress(collected.len() as u64, self.len);
242 }
243 }
244 Ok(Bytes::from(collected))
245 }
246}
247
248#[cfg(feature = "http")]
252fn http_io_error(marker: crate::error::TransportMarker) -> io::Error {
253 io::Error::other(marker)
254}
255
256#[cfg(feature = "http")]
257impl Backend for HttpBackend {
258 fn len(&self) -> BoxFuture<'_, io::Result<u64>> {
259 let total = self.len;
260 Box::pin(async move { Ok(total) })
261 }
262
263 fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> BoxFuture<'a, io::Result<usize>> {
264 Box::pin(async move {
265 if offset >= self.len || buf.is_empty() {
266 return Ok(0);
267 }
268 if let Some(body) = self.full.get() {
269 return Ok(read_from_bytes(body, offset, buf));
270 }
271 let last = (offset + buf.len() as u64 - 1).min(self.len - 1);
272 let mut attempt = 0;
278 let response = loop {
279 let response = self
280 .client
281 .get(self.url.clone())
282 .header(reqwest::header::RANGE, format!("bytes={offset}-{last}"))
283 .send()
284 .await
285 .map_err(|err| {
286 http_io_error(crate::error::TransportMarker {
287 status: err.status().map(|status| status.as_u16()),
288 msg: format!("GET {} range {offset}-{last}: {err}", self.url),
289 })
290 })?;
291 if !response.status().is_server_error() || attempt == 2 {
292 break response;
293 }
294 attempt += 1;
295 tokio::time::sleep(std::time::Duration::from_millis(250 * (1 << attempt))).await;
296 };
297 match response.status().as_u16() {
298 206 => {}
299 200 => {
300 let collected = self.collect_full_body(response).await?;
307 let body = self.full.get_or_init(|| collected);
308 return Ok(read_from_bytes(body, offset, buf));
309 }
310 status => {
311 return Err(http_io_error(crate::error::TransportMarker {
312 status: Some(status),
313 msg: format!("GET {} range {offset}-{last} failed", self.url),
314 }));
315 }
316 }
317 use futures_util::StreamExt;
327 let mut chunks = response.bytes_stream();
328 let mut filled = 0usize;
329 while filled < buf.len() {
330 let chunk = match chunks.next().await {
331 Some(Ok(chunk)) => chunk,
332 Some(Err(err)) => {
333 return Err(http_io_error(crate::error::TransportMarker {
334 status: None,
335 msg: format!("GET {} range {offset}-{last}: {err}", self.url),
336 }))
337 }
338 None => break, };
340 let take = (buf.len() - filled).min(chunk.len());
341 buf[filled..filled + take].copy_from_slice(&chunk[..take]);
342 filled += take;
343 }
344 Ok(filled)
345 })
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[tokio::test]
354 async fn mem_backend_reads_and_reports_length() {
355 let backend = MemBackend::from(b"hello world".to_vec());
356 assert_eq!(backend.len().await.unwrap(), 11);
357 let mut buf = [0u8; 5];
358 assert_eq!(backend.read_at(6, &mut buf).await.unwrap(), 5);
359 assert_eq!(&buf, b"world");
360 }
361
362 #[tokio::test]
363 async fn mem_backend_short_reads_only_at_eof() {
364 let backend = MemBackend::from(bytes::Bytes::from_static(b"abcdef"));
365 let mut buf = [0u8; 10];
366 assert_eq!(backend.read_at(4, &mut buf).await.unwrap(), 2);
367 assert_eq!(&buf[..2], b"ef");
368 assert_eq!(backend.read_at(6, &mut buf).await.unwrap(), 0);
369 assert_eq!(backend.read_at(999, &mut buf).await.unwrap(), 0);
370 }
371
372 #[tokio::test]
373 async fn backend_is_object_safe() {
374 let boxed: std::sync::Arc<dyn Backend> =
375 std::sync::Arc::new(MemBackend::from(b"xyz".to_vec()));
376 assert_eq!(boxed.len().await.unwrap(), 3);
377 }
378
379 #[tokio::test]
380 async fn file_backend_positioned_reads() {
381 let path = std::env::temp_dir().join(format!(
382 "pdfboss-aio-backend-test-{}.bin",
383 std::process::id()
384 ));
385 std::fs::write(&path, b"0123456789abcdef").unwrap();
386 let backend = FileBackend::open(&path).await.unwrap();
387 assert_eq!(backend.len().await.unwrap(), 16);
388 let mut buf = [0u8; 4];
389 assert_eq!(backend.read_at(10, &mut buf).await.unwrap(), 4);
390 assert_eq!(&buf, b"abcd");
391 assert_eq!(backend.read_at(0, &mut buf).await.unwrap(), 4);
394 assert_eq!(&buf, b"0123");
395 let mut long = [0u8; 32];
397 assert_eq!(backend.read_at(12, &mut long).await.unwrap(), 4);
398 assert_eq!(&long[..4], b"cdef");
399 std::fs::remove_file(&path).ok();
400 }
401
402 #[tokio::test]
403 async fn file_backend_open_missing_file_errors() {
404 let missing = std::env::temp_dir().join("pdfboss-aio-backend-test-missing.bin");
405 assert!(FileBackend::open(&missing).await.is_err());
406 }
407
408 #[cfg(feature = "http")]
409 #[test]
410 fn transport_marker_round_trips_through_io_error() {
411 let failed = http_io_error(crate::error::TransportMarker {
412 status: Some(503),
413 msg: "unavailable".to_string(),
414 });
415 assert!(matches!(
416 crate::Error::from(failed),
417 crate::Error::Http {
418 status: Some(503),
419 ..
420 }
421 ));
422 }
423}